mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #338 from sharelatex/ja-track-changes
Ja track changes
This commit is contained in:
commit
c689937297
15 changed files with 575 additions and 146 deletions
|
@ -65,7 +65,7 @@ block content
|
|||
.ui-layout-center
|
||||
include ./editor/editor
|
||||
include ./editor/binary-file
|
||||
include ./editor/track-changes
|
||||
include ./editor/history
|
||||
include ./editor/publish-template
|
||||
|
||||
.ui-layout-east
|
||||
|
|
|
@ -70,13 +70,13 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
|
|||
ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']"
|
||||
)
|
||||
|
||||
li(ng-show="deletedDocs.length > 0 && ui.view == 'track-changes'")
|
||||
li(ng-show="deletedDocs.length > 0 && ui.view == 'history'")
|
||||
h3 #{translate("deleted_files")}
|
||||
li(
|
||||
ng-class="{ 'selected': entity.selected }",
|
||||
ng-repeat="entity in deletedDocs | orderBy:'name'",
|
||||
ng-controller="FileTreeEntityController",
|
||||
ng-show="ui.view == 'track-changes'"
|
||||
ng-show="ui.view == 'history'"
|
||||
)
|
||||
.entity
|
||||
.entity-name(
|
||||
|
|
|
@ -102,8 +102,8 @@ div(ng-if="!shouldABTestHeaderLabels")
|
|||
i.fa.fa-fw.fa-group
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="toggleTrackChanges()",
|
||||
ng-class="{ active: (ui.view == 'track-changes') }"
|
||||
ng-click="toggleHistory()",
|
||||
ng-class="{ active: (ui.view == 'history') }"
|
||||
tooltip="#{translate('recent_changes')}",
|
||||
tooltip-placement="bottom",
|
||||
)
|
||||
|
@ -232,8 +232,8 @@ div(ng-if="shouldABTestHeaderLabels")
|
|||
i.fa.fa-fw.fa-group
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="toggleTrackChanges(); trackABTestConversion('history');",
|
||||
ng-class="{ active: (ui.view == 'track-changes') }"
|
||||
ng-click="toggleHistory(); trackABTestConversion('history');",
|
||||
ng-class="{ active: (ui.view == 'history') }"
|
||||
tooltip="#{translate('recent_changes')}",
|
||||
tooltip-placement="bottom",
|
||||
sixpack-convert="editor-header"
|
||||
|
@ -357,8 +357,8 @@ div(ng-if="shouldABTestHeaderLabels")
|
|||
p.toolbar-label #{translate("share")}
|
||||
a.btn.btn-full-height(
|
||||
href,
|
||||
ng-click="toggleTrackChanges(); trackABTestConversion('history');",
|
||||
ng-class="{ active: (ui.view == 'track-changes') }",
|
||||
ng-click="toggleHistory(); trackABTestConversion('history');",
|
||||
ng-class="{ active: (ui.view == 'history') }",
|
||||
sixpack-convert="editor-header"
|
||||
)
|
||||
i.fa.fa-fw.fa-history
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
div#trackChanges(ng-show="ui.view == 'track-changes'")
|
||||
span(ng-controller="TrackChangesPremiumPopup")
|
||||
div#history(ng-show="ui.view == 'history'")
|
||||
span(ng-controller="HistoryPremiumPopup")
|
||||
.upgrade-prompt(ng-show="!project.features.versioning")
|
||||
.message(ng-show="project.owner._id == user.id")
|
||||
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
|
||||
|
@ -33,29 +33,29 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
|||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('track-changes')"
|
||||
ng-click="startFreeTrial('history')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
|
||||
.message(ng-show="project.owner._id != user.id")
|
||||
p #{translate("ask_proj_owner_to_upgrade_for_history")}
|
||||
p
|
||||
a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
|
||||
a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
|
||||
|
||||
aside.change-list(
|
||||
ng-controller="TrackChangesListController"
|
||||
ng-controller="HistoryListController"
|
||||
infinite-scroll="loadMore()"
|
||||
infinite-scroll-disabled="trackChanges.loading || trackChanges.atEnd"
|
||||
infinite-scroll-initialize="ui.view == 'track-changes'"
|
||||
infinite-scroll-disabled="history.loading || history.atEnd"
|
||||
infinite-scroll-initialize="ui.view == 'history'"
|
||||
)
|
||||
.infinite-scroll-inner
|
||||
ul.list-unstyled(
|
||||
ng-class="{\
|
||||
'hover-state': trackChanges.hoveringOverListSelectors\
|
||||
'hover-state': history.hoveringOverListSelectors\
|
||||
}"
|
||||
)
|
||||
li.change(
|
||||
ng-repeat="update in trackChanges.updates"
|
||||
ng-repeat="update in history.updates"
|
||||
ng-class="{\
|
||||
'first-in-day': update.meta.first_in_day,\
|
||||
'selected': update.inSelection,\
|
||||
|
@ -65,7 +65,7 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
|||
'hover-selected-to': update.hoverSelectedTo,\
|
||||
'hover-selected-from': update.hoverSelectedFrom,\
|
||||
}"
|
||||
ng-controller="TrackChangesListItemController"
|
||||
ng-controller="HistoryListItemController"
|
||||
)
|
||||
|
||||
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
|
||||
|
@ -108,55 +108,55 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
|||
.color-square(style="background-color: hsl(100, 100%, 50%)")
|
||||
span #{translate("anonymous")}
|
||||
|
||||
.loading(ng-show="trackChanges.loading")
|
||||
.loading(ng-show="history.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
|
||||
.diff-panel.full-size(ng-controller="TrackChangesDiffController")
|
||||
.diff-panel.full-size(ng-controller="HistoryDiffController")
|
||||
.diff(
|
||||
ng-show="!!trackChanges.diff && !trackChanges.diff.loading && !trackChanges.diff.deleted && !trackChanges.diff.error"
|
||||
ng-show="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error"
|
||||
)
|
||||
.toolbar.toolbar-alt
|
||||
span.name
|
||||
| <strong>{{trackChanges.diff.highlights.length}} </strong>
|
||||
| <strong>{{history.diff.highlights.length}} </strong>
|
||||
ng-pluralize(
|
||||
count="trackChanges.diff.highlights.length",
|
||||
count="history.diff.highlights.length",
|
||||
when="{\
|
||||
'one': 'change',\
|
||||
'other': 'changes'\
|
||||
}"
|
||||
)
|
||||
| in <strong>{{trackChanges.diff.doc.name}}</strong>
|
||||
| in <strong>{{history.diff.doc.name}}</strong>
|
||||
.toolbar-right
|
||||
a.btn.btn-danger.btn-sm(
|
||||
href,
|
||||
ng-click="openRestoreDiffModal()"
|
||||
) #{translate("restore_to_before_these_changes")}
|
||||
.diff-editor.hide-ace-cursor(
|
||||
ace-editor="track-changes",
|
||||
ace-editor="history",
|
||||
theme="settings.theme",
|
||||
font-size="settings.fontSize",
|
||||
text="trackChanges.diff.text",
|
||||
highlights="trackChanges.diff.highlights",
|
||||
text="history.diff.text",
|
||||
highlights="history.diff.highlights",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
navigate-highlights="true"
|
||||
)
|
||||
.diff-deleted.text-centered(
|
||||
ng-show="trackChanges.diff.deleted && !trackChanges.diff.restoreDeletedSuccess"
|
||||
ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
|
||||
)
|
||||
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ trackChanges.diff.doc.name }} "})}
|
||||
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
|
||||
p
|
||||
a.btn.btn-primary.btn-lg(
|
||||
href,
|
||||
ng-click="restoreDeletedDoc()",
|
||||
ng-disabled="trackChanges.diff.restoreInProgress"
|
||||
ng-disabled="history.diff.restoreInProgress"
|
||||
) #{translate("restore")}
|
||||
|
||||
.diff-deleted.text-centered(
|
||||
ng-show="trackChanges.diff.deleted && trackChanges.diff.restoreDeletedSuccess"
|
||||
ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
|
||||
)
|
||||
p.text-serif #{translate("file_restored", {filename:"{{ trackChanges.diff.doc.name }} "})}
|
||||
p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
|
||||
p.text-serif #{translate("file_restored_back_to_editor")}
|
||||
p
|
||||
a.btn.btn-default(
|
||||
|
@ -164,13 +164,13 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
|||
ng-click="backToEditorAfterRestore()",
|
||||
) #{translate("file_restored_back_to_editor_btn")}
|
||||
|
||||
.loading-panel(ng-show="trackChanges.diff.loading")
|
||||
.loading-panel(ng-show="history.diff.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
.error-panel(ng-show="trackChanges.diff.error")
|
||||
.error-panel(ng-show="history.diff.error")
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
||||
|
||||
script(type="text/ng-template", id="trackChangesRestoreDiffModalTemplate")
|
||||
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
|
@ -4,7 +4,7 @@ define [
|
|||
"ide/connection/ConnectionManager"
|
||||
"ide/editor/EditorManager"
|
||||
"ide/online-users/OnlineUsersManager"
|
||||
"ide/track-changes/TrackChangesManager"
|
||||
"ide/history/HistoryManager"
|
||||
"ide/permissions/PermissionsManager"
|
||||
"ide/pdf/PdfManager"
|
||||
"ide/binary-files/BinaryFilesManager"
|
||||
|
@ -36,7 +36,7 @@ define [
|
|||
ConnectionManager
|
||||
EditorManager
|
||||
OnlineUsersManager
|
||||
TrackChangesManager
|
||||
HistoryManager
|
||||
PermissionsManager
|
||||
PdfManager
|
||||
BinaryFilesManager
|
||||
|
@ -116,7 +116,7 @@ define [
|
|||
ide.fileTreeManager = new FileTreeManager(ide, $scope)
|
||||
ide.editorManager = new EditorManager(ide, $scope)
|
||||
ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
|
||||
ide.trackChangesManager = new TrackChangesManager(ide, $scope)
|
||||
ide.historyManager = new HistoryManager(ide, $scope)
|
||||
ide.pdfManager = new PdfManager(ide, $scope)
|
||||
ide.permissionsManager = new PermissionsManager(ide, $scope)
|
||||
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
|
||||
|
|
|
@ -7,7 +7,8 @@ define [
|
|||
"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
|
||||
"ide/editor/directives/aceEditor/highlights/HighlightsManager"
|
||||
"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
|
||||
], (App, Ace, SearchBox, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager) ->
|
||||
"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
|
||||
], (App, Ace, SearchBox, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager) ->
|
||||
EditSession = ace.require('ace/edit_session').EditSession
|
||||
|
||||
# set the path for ace workers if using a CDN (from editor.jade)
|
||||
|
@ -69,6 +70,9 @@ define [
|
|||
undoManager = new UndoManager(scope, editor, element)
|
||||
highlightsManager = new HighlightsManager(scope, editor, element)
|
||||
cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
|
||||
trackChangesManager = new TrackChangesManager(scope, editor, element)
|
||||
if window.location.search.match /tcon=true/ # track changes on
|
||||
trackChangesManager.enabled = true
|
||||
|
||||
# Prevert Ctrl|Cmd-S from triggering save dialog
|
||||
editor.commands.addCommand
|
||||
|
@ -222,7 +226,9 @@ define [
|
|||
sharejs_doc.on "remoteop.recordForUndo", () =>
|
||||
undoManager.nextUpdateIsRemote = true
|
||||
|
||||
editor.initing = true
|
||||
sharejs_doc.attachToAce(editor)
|
||||
editor.initing = false
|
||||
# need to set annotations after attaching because attaching
|
||||
# deletes and then inserts document content
|
||||
session.setAnnotations scope.annotations
|
||||
|
|
|
@ -0,0 +1,410 @@
|
|||
define [
|
||||
"ace/ace"
|
||||
"utils/EventEmitter"
|
||||
], (_, EventEmitter) ->
|
||||
class TrackChangesManager
|
||||
Range = ace.require("ace/range").Range
|
||||
|
||||
constructor: (@$scope, @editor, @element) ->
|
||||
@changesTracker = new ChangesTracker()
|
||||
@changeIdToMarkerIdMap = {}
|
||||
@enabled = false
|
||||
|
||||
@changesTracker.on "insert:added", (change) =>
|
||||
@_onInsertAdded(change)
|
||||
@changesTracker.on "insert:removed", (change) =>
|
||||
@_onInsertRemoved(change)
|
||||
@changesTracker.on "delete:added", (change) =>
|
||||
@_onDeleteAdded(change)
|
||||
@changesTracker.on "delete:removed", (change) =>
|
||||
@_onDeleteRemoved(change)
|
||||
@changesTracker.on "changes:moved", (changes) =>
|
||||
@_onChangesMoved(changes)
|
||||
|
||||
onChange = (e) =>
|
||||
if !@editor.initing and @enabled
|
||||
@applyChange(e)
|
||||
setTimeout () =>
|
||||
@checkMapping()
|
||||
, 100
|
||||
|
||||
@editor.on "changeSession", (e) =>
|
||||
e.oldSession?.getDocument().off "change", onChange
|
||||
e.session.getDocument().on "change", onChange
|
||||
@editor.getSession().getDocument().on "change", onChange
|
||||
|
||||
checkMapping: () ->
|
||||
session = @editor.getSession()
|
||||
|
||||
# Make a copy of session.getMarkers() so we can modify it
|
||||
markers = {}
|
||||
for marker_id, marker of session.getMarkers()
|
||||
markers[marker_id] = marker
|
||||
|
||||
for change in @changesTracker.changes
|
||||
op = change.op
|
||||
marker_id = @changeIdToMarkerIdMap[change.id]
|
||||
|
||||
start = @_shareJsOffsetToAcePosition(op.p)
|
||||
if op.i?
|
||||
end = @_shareJsOffsetToAcePosition(op.p + op.i.length)
|
||||
else if op.d?
|
||||
end = start
|
||||
|
||||
marker = markers[marker_id]
|
||||
delete markers[marker_id]
|
||||
if marker.range.start.row != start.row or
|
||||
marker.range.start.column != start.column or
|
||||
marker.range.end.row != end.row or
|
||||
marker.range.end.column != end.column
|
||||
console.error "Change doesn't match marker anymore", {change, marker, start, end}
|
||||
|
||||
for marker_id, marker of markers
|
||||
if marker.clazz.match("track-changes")
|
||||
console.error "Orphaned ace marker", marker
|
||||
|
||||
applyChange: (delta) ->
|
||||
op = @_aceChangeToShareJs(delta)
|
||||
console.log "Applying change", delta, op
|
||||
@changesTracker.applyOp(op)
|
||||
|
||||
_onInsertAdded: (change) ->
|
||||
start = @_shareJsOffsetToAcePosition(change.op.p)
|
||||
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
|
||||
session = @editor.getSession()
|
||||
doc = session.getDocument()
|
||||
ace_range = new Range(start.row, start.column, end.row, end.column)
|
||||
marker_id = session.addMarker(ace_range, "track-changes-added-marker", "text")
|
||||
@changeIdToMarkerIdMap[change.id] = marker_id
|
||||
|
||||
_onDeleteAdded: (change) ->
|
||||
position = @_shareJsOffsetToAcePosition(change.op.p)
|
||||
session = @editor.getSession()
|
||||
doc = session.getDocument()
|
||||
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.
|
||||
# This is the code we need to trick:
|
||||
# var range = marker.range.clipRows(config.firstRow, config.lastRow);
|
||||
# if (range.isEmpty()) continue;
|
||||
_clipRows = ace_range.clipRows
|
||||
ace_range.clipRows = (args...) ->
|
||||
range = _clipRows.apply(ace_range, args)
|
||||
range.isEmpty = () ->
|
||||
false
|
||||
return range
|
||||
|
||||
marker_id = session.addMarker(ace_range, "track-changes-deleted-marker", "text")
|
||||
@changeIdToMarkerIdMap[change.id] = marker_id
|
||||
|
||||
_onInsertRemoved: (change) ->
|
||||
marker_id = @changeIdToMarkerIdMap[change.id]
|
||||
session = @editor.getSession()
|
||||
session.removeMarker marker_id
|
||||
|
||||
_onDeleteRemoved: (change) ->
|
||||
marker_id = @changeIdToMarkerIdMap[change.id]
|
||||
session = @editor.getSession()
|
||||
session.removeMarker marker_id
|
||||
|
||||
_aceChangeToShareJs: (delta) ->
|
||||
start = delta.start
|
||||
lines = @editor.getSession().getDocument().getLines 0, start.row
|
||||
offset = 0
|
||||
for line, i in lines
|
||||
offset += if i < start.row
|
||||
line.length
|
||||
else
|
||||
start.column
|
||||
offset += start.row # Include newlines
|
||||
|
||||
text = delta.lines.join('\n')
|
||||
switch delta.action
|
||||
when 'insert'
|
||||
return { i: text, p: offset }
|
||||
when 'remove'
|
||||
return { d: text, p: offset }
|
||||
else throw new Error "unknown action: #{delta.action}"
|
||||
|
||||
_shareJsOffsetToAcePosition: (offset) ->
|
||||
lines = @editor.getSession().getDocument().getAllLines()
|
||||
row = 0
|
||||
for line, row in lines
|
||||
break if offset <= line.length
|
||||
offset -= lines[row].length + 1 # + 1 for newline char
|
||||
return {row:row, column:offset}
|
||||
|
||||
_onChangesMoved: (changes) ->
|
||||
session = @editor.getSession()
|
||||
markers = session.getMarkers()
|
||||
for change in changes
|
||||
start = @_shareJsOffsetToAcePosition(change.op.p)
|
||||
if change.op.i?
|
||||
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
|
||||
else
|
||||
end = start
|
||||
marker_id = @changeIdToMarkerIdMap[change.id]
|
||||
marker = markers[marker_id]
|
||||
console.log "moving marker", {marker, start, end, change}
|
||||
marker.range.start = start
|
||||
marker.range.end = end
|
||||
|
||||
class ChangesTracker extends EventEmitter
|
||||
# The purpose of this class is to track a set of inserts and deletes to a document, like
|
||||
# track changes in Word. We store these as a set of ShareJs style ranges:
|
||||
# {i: "foo", p: 42} # Insert 'foo' at offset 42
|
||||
# {d: "bar", p: 37} # Delete 'bar' at offset 37
|
||||
# We only track the inserts and deletes, not the whole document, but by being given all
|
||||
# updates that are applied to a document, we can update these appropriately.
|
||||
#
|
||||
# Note that the set of inserts and deletes we store applies to the document as-is at the moment.
|
||||
# So inserts correspond to text which is in the document, while deletes correspond to text which
|
||||
# is no longer there, so their lengths do not affect the position of later offsets.
|
||||
# E.g.
|
||||
# this is the current text of the document
|
||||
# |-----| |
|
||||
# {i: "current ", p:12} -^ ^- {d: "old ", p: 31}
|
||||
#
|
||||
# Track changes rules (should be consistent with Word):
|
||||
# * When text is inserted at a delete, the text goes to the left of the delete
|
||||
# I.e. "foo|bar" -> "foobaz|bar", where | is the delete, and 'baz' is inserted
|
||||
# * Deleting content flagged as 'inserted' does not create a new delete marker, it only
|
||||
# removes the insert marker. E.g.
|
||||
# * "abdefghijkl" -> "abfghijkl" when 'de' is deleted. No delete marker added
|
||||
# |---| <- inserted |-| <- inserted
|
||||
# * Deletes overlapping regular text and inserted text will insert a delete marker for the
|
||||
# regular text:
|
||||
# "abcdefghijkl" -> "abcdejkl" when 'fghi' is deleted
|
||||
# |----| |--||
|
||||
# ^- inserted 'bcdefg' \ ^- deleted 'hi'
|
||||
# \--inserted 'bcde'
|
||||
# * Deletes overlapping other deletes are merged. E.g.
|
||||
# "abcghijkl" -> "ahijkl" when 'bcg is deleted'
|
||||
# | <- delete 'def' | <- delete 'bcdefg'
|
||||
constructor: () ->
|
||||
# Change objects have the following structure:
|
||||
# {
|
||||
# id: ... # Uniquely generated by us
|
||||
# op: { # ShareJs style op tracking the offset (p) and content inserted (i) or deleted (d)
|
||||
# i: "..."
|
||||
# p: 42
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# Ids are used to uniquely identify a change, e.g. for updating it in the database, or keeping in
|
||||
# sync with Ace ranges.
|
||||
@changes = []
|
||||
@id = 0
|
||||
|
||||
applyOp: (op) ->
|
||||
# Apply an op that has been applied to the document to our changes to keep them up to date
|
||||
if op.i?
|
||||
@applyInsert(op)
|
||||
else if op.d?
|
||||
@applyDelete(op)
|
||||
|
||||
applyInsert: (op) ->
|
||||
op_start = op.p
|
||||
op_length = op.i.length
|
||||
op_end = op.p + op_length
|
||||
|
||||
already_merged = false
|
||||
previous_change = null
|
||||
moved_changes = []
|
||||
for change in @changes
|
||||
change_start = change.op.p
|
||||
|
||||
if change.op.d?
|
||||
# Shift any deletes after this along by the length of this insert
|
||||
if op_start <= change_start
|
||||
change.op.p += op_length
|
||||
moved_changes.push change
|
||||
else if change.op.i?
|
||||
change_end = change_start + change.op.i.length
|
||||
is_change_overlapping = (op_start >= change_start and op_start <= change_end)
|
||||
|
||||
# If there is a delete at the start of the insert, and we're inserting
|
||||
# at the start, we SHOULDN'T merge since the delete acts as a partition.
|
||||
# The previous op will be the delete, but it's already been shifted by this insert
|
||||
#
|
||||
# I.e.
|
||||
# Originally: |-- existing insert --|
|
||||
# | <- existing delete at same offset
|
||||
#
|
||||
# Now: |-- existing insert --| <- not shifted yet
|
||||
# |-- this insert --|| <- existing delete shifted along to end of this op
|
||||
#
|
||||
# After: |-- existing insert --|
|
||||
# |-- this insert --|| <- existing delete
|
||||
#
|
||||
# Without the delete, the inserts would be merged.
|
||||
is_insert_blocked_by_delete = (previous_change? and previous_change.op.d? and previous_change.op.p == op_end)
|
||||
|
||||
# If the insert is overlapping another insert, either at the beginning in the middle or touching the end,
|
||||
# then we merge them into one.
|
||||
if is_change_overlapping and
|
||||
!is_insert_blocked_by_delete and
|
||||
!already_merged # With the way we order our changes, there should only ever be one candidate to merge
|
||||
# with since changes don't overlap. However, this flag just adds a little bit of protection
|
||||
offset = op_start - change_start
|
||||
change.op.i = change.op.i.slice(0, offset) + op.i + change.op.i.slice(offset)
|
||||
already_merged = true
|
||||
moved_changes.push change
|
||||
else if op_start <= change_start
|
||||
# If we're fully before the other insert we can just shift the other insert by our length.
|
||||
# If they are touching, and should have been merged, they will have been above.
|
||||
# If not merged above, then it must be blocked by a delete, and will be after this insert, so we shift it along as well
|
||||
change.op.p += op_length
|
||||
moved_changes.push change
|
||||
previous_change = change
|
||||
|
||||
if !already_merged
|
||||
@_addOp op
|
||||
|
||||
if moved_changes.length > 0
|
||||
@emit "changes:moved", moved_changes
|
||||
|
||||
applyDelete: (op) ->
|
||||
op_start = op.p
|
||||
op_length = op.d.length
|
||||
op_end = op.p + op_length
|
||||
remove_changes = []
|
||||
moved_changes = []
|
||||
|
||||
# We might end up modifying our delete op if it merges with existing deletes, or cancels out
|
||||
# with an existing insert. Since we might do multiple modifications, we record them and do
|
||||
# all the modifications after looping through the existing changes, so as not to mess up the
|
||||
# offset indexes as we go.
|
||||
op_modifications = []
|
||||
for change in @changes
|
||||
if change.op.i?
|
||||
change_start = change.op.p
|
||||
change_end = change_start + change.op.i.length
|
||||
if op_end <= change_start
|
||||
# Shift ops after us back by our length
|
||||
change.op.p -= op_length
|
||||
moved_changes.push change
|
||||
else if op_start >= change_end
|
||||
# Delete is after insert, nothing to do
|
||||
else
|
||||
# When the new delete overlaps an insert, we should remove the part of the insert that
|
||||
# is now deleted, and also remove the part of the new delete that overlapped. I.e.
|
||||
# the two cancel out where they overlap.
|
||||
if op_start >= change_start
|
||||
# |-- existing insert --|
|
||||
# insert_remaining_before -> |.....||-- new delete --|
|
||||
delete_remaining_before = ""
|
||||
insert_remaining_before = change.op.i.slice(0, op_start - change_start)
|
||||
else
|
||||
# delete_remaining_before -> |.....||-- existing insert --|
|
||||
# |-- new delete --|
|
||||
delete_remaining_before = op.d.slice(0, change_start - op_start)
|
||||
insert_remaining_before = ""
|
||||
|
||||
if op_end <= change_end
|
||||
# |-- existing insert --|
|
||||
# |-- new delete --||.....| <- insert_remaining_after
|
||||
delete_remaining_after = ""
|
||||
insert_remaining_after = change.op.i.slice(op_end - change_start)
|
||||
else
|
||||
# |-- existing insert --||.....| <- delete_remaining_after
|
||||
# |-- new delete --|
|
||||
delete_remaining_after = op.d.slice(change_end - op_start)
|
||||
insert_remaining_after = ""
|
||||
|
||||
insert_remaining = insert_remaining_before + insert_remaining_after
|
||||
if insert_remaining.length > 0
|
||||
change.op.i = insert_remaining
|
||||
change.op.p = Math.min(change_start, op_start)
|
||||
moved_changes.push change
|
||||
else
|
||||
remove_changes.push change
|
||||
|
||||
# We know what we want to preserve of our delete op before (delete_remaining_before) and what we want to preserve
|
||||
# afterwards (delete_remaining_before). Now we need to turn that into a modification which deletes the
|
||||
# chunk in the middle not covered by these.
|
||||
delete_removed_length = op.d.length - delete_remaining_before.length - delete_remaining_after.length
|
||||
delete_removed_start = delete_remaining_before.length
|
||||
modification = {
|
||||
d: op.d.slice(delete_removed_start, delete_removed_start + delete_removed_length)
|
||||
p: delete_removed_start
|
||||
}
|
||||
if modification.d.length > 0
|
||||
op_modifications.push modification
|
||||
else if change.op.d?
|
||||
change_start = change.op.p
|
||||
if op_end < change_start
|
||||
# Shift ops after us (but not touching) back by our length
|
||||
change.op.p -= op_length
|
||||
moved_changes.push change
|
||||
else if op_start <= change_start <= op_end
|
||||
# If we overlap a delete, add it in our content, and delete the existing change
|
||||
offset = change_start - op_start
|
||||
op_modifications.push { i: change.op.d, p: offset }
|
||||
remove_changes.push change
|
||||
|
||||
op.d = @_applyOpModifications(op.d, op_modifications)
|
||||
if op.d.length > 0
|
||||
@_addOp op
|
||||
|
||||
for change in remove_changes
|
||||
@_removeChange change
|
||||
|
||||
if moved_changes.length > 0
|
||||
@emit "changes:moved", moved_changes
|
||||
|
||||
_newId: () ->
|
||||
@id++
|
||||
|
||||
_addOp: (op) ->
|
||||
change = {
|
||||
id: @_newId()
|
||||
op: op
|
||||
}
|
||||
@changes.push change
|
||||
|
||||
# Keep ops in order of offset, with deletes before inserts
|
||||
@changes.sort (c1, c2) ->
|
||||
result = c1.op.p - c2.op.p
|
||||
if result != 0
|
||||
return result
|
||||
else if c1.op.i? and c2.op.d?
|
||||
return 1
|
||||
else
|
||||
return -1
|
||||
|
||||
if op.d?
|
||||
@emit "delete:added", change
|
||||
else if op.i?
|
||||
@emit "insert:added", change
|
||||
|
||||
_removeChange: (change) ->
|
||||
@changes = @changes.filter (c) -> c.id != change.id
|
||||
if change.op.d?
|
||||
@emit "delete:removed", change
|
||||
else if change.op.i?
|
||||
@emit "insert:removed", change
|
||||
|
||||
_applyOpModifications: (content, op_modifications) ->
|
||||
# Put in descending position order, with deleting first if at the same offset
|
||||
# (Inserting first would modify the content that the delete will delete)
|
||||
op_modifications.sort (a, b) ->
|
||||
result = b.p - a.p
|
||||
if result != 0
|
||||
return result
|
||||
else if a.i? and b.d?
|
||||
return 1
|
||||
else
|
||||
return -1
|
||||
|
||||
for modification in op_modifications
|
||||
if modification.i?
|
||||
content = content.slice(0, modification.p) + modification.i + content.slice(modification.p)
|
||||
else if modification.d?
|
||||
if content.slice(modification.p, modification.p + modification.d.length) != modification.d
|
||||
throw new Error("deleted content does not match. content: #{JSON.stringify(content)}; modification: #{JSON.stringify(modification)}")
|
||||
content = content.slice(0, modification.p) + content.slice(modification.p + modification.d.length)
|
||||
return content
|
||||
|
||||
return TrackChangesManager
|
|
@ -1,31 +1,31 @@
|
|||
define [
|
||||
"moment"
|
||||
"ide/track-changes/controllers/TrackChangesListController"
|
||||
"ide/track-changes/controllers/TrackChangesDiffController"
|
||||
"ide/track-changes/directives/infiniteScroll"
|
||||
"ide/history/controllers/HistoryListController"
|
||||
"ide/history/controllers/HistoryDiffController"
|
||||
"ide/history/directives/infiniteScroll"
|
||||
], (moment) ->
|
||||
class TrackChangesManager
|
||||
class HistoryManager
|
||||
constructor: (@ide, @$scope) ->
|
||||
@reset()
|
||||
|
||||
@$scope.toggleTrackChanges = () =>
|
||||
if @$scope.ui.view == "track-changes"
|
||||
@$scope.toggleHistory = () =>
|
||||
if @$scope.ui.view == "history"
|
||||
@hide()
|
||||
else
|
||||
@show()
|
||||
|
||||
@$scope.$watch "trackChanges.selection.updates", (updates) =>
|
||||
@$scope.$watch "history.selection.updates", (updates) =>
|
||||
if updates? and updates.length > 0
|
||||
@_selectDocFromUpdates()
|
||||
@reloadDiff()
|
||||
|
||||
@$scope.$on "entity:selected", (event, entity) =>
|
||||
if (@$scope.ui.view == "track-changes") and (entity.type == "doc")
|
||||
@$scope.trackChanges.selection.doc = entity
|
||||
if (@$scope.ui.view == "history") and (entity.type == "doc")
|
||||
@$scope.history.selection.doc = entity
|
||||
@reloadDiff()
|
||||
|
||||
show: () ->
|
||||
@$scope.ui.view = "track-changes"
|
||||
@$scope.ui.view = "history"
|
||||
@reset()
|
||||
|
||||
hide: () ->
|
||||
|
@ -34,7 +34,7 @@ define [
|
|||
@$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity()
|
||||
|
||||
reset: () ->
|
||||
@$scope.trackChanges = {
|
||||
@$scope.history = {
|
||||
updates: []
|
||||
nextBeforeTimestamp: null
|
||||
atEnd: false
|
||||
|
@ -52,36 +52,36 @@ define [
|
|||
}
|
||||
|
||||
autoSelectRecentUpdates: () ->
|
||||
return if @$scope.trackChanges.updates.length == 0
|
||||
return if @$scope.history.updates.length == 0
|
||||
|
||||
@$scope.trackChanges.updates[0].selectedTo = true
|
||||
@$scope.history.updates[0].selectedTo = true
|
||||
|
||||
indexOfLastUpdateNotByMe = 0
|
||||
for update, i in @$scope.trackChanges.updates
|
||||
for update, i in @$scope.history.updates
|
||||
if @_updateContainsUserId(update, @$scope.user.id)
|
||||
break
|
||||
indexOfLastUpdateNotByMe = i
|
||||
|
||||
@$scope.trackChanges.updates[indexOfLastUpdateNotByMe].selectedFrom = true
|
||||
@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
|
||||
|
||||
BATCH_SIZE: 10
|
||||
fetchNextBatchOfUpdates: () ->
|
||||
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
|
||||
if @$scope.trackChanges.nextBeforeTimestamp?
|
||||
url += "&before=#{@$scope.trackChanges.nextBeforeTimestamp}"
|
||||
@$scope.trackChanges.loading = true
|
||||
if @$scope.history.nextBeforeTimestamp?
|
||||
url += "&before=#{@$scope.history.nextBeforeTimestamp}"
|
||||
@$scope.history.loading = true
|
||||
@ide.$http
|
||||
.get(url)
|
||||
.success (data) =>
|
||||
@_loadUpdates(data.updates)
|
||||
@$scope.trackChanges.nextBeforeTimestamp = data.nextBeforeTimestamp
|
||||
@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
|
||||
if !data.nextBeforeTimestamp?
|
||||
@$scope.trackChanges.atEnd = true
|
||||
@$scope.trackChanges.loading = false
|
||||
@$scope.history.atEnd = true
|
||||
@$scope.history.loading = false
|
||||
|
||||
reloadDiff: () ->
|
||||
diff = @$scope.trackChanges.diff
|
||||
{updates, doc} = @$scope.trackChanges.selection
|
||||
diff = @$scope.history.diff
|
||||
{updates, doc} = @$scope.history.selection
|
||||
{fromV, toV, start_ts, end_ts} = @_calculateRangeFromSelection()
|
||||
|
||||
return if !doc?
|
||||
|
@ -91,7 +91,7 @@ define [
|
|||
diff.fromV == fromV and
|
||||
diff.toV == toV
|
||||
|
||||
@$scope.trackChanges.diff = diff = {
|
||||
@$scope.history.diff = diff = {
|
||||
fromV: fromV
|
||||
toV: toV
|
||||
start_ts: start_ts
|
||||
|
@ -184,7 +184,7 @@ define [
|
|||
return {text, highlights}
|
||||
|
||||
_loadUpdates: (updates = []) ->
|
||||
previousUpdate = @$scope.trackChanges.updates[@$scope.trackChanges.updates.length - 1]
|
||||
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
|
||||
|
||||
for update in updates
|
||||
for doc_id, doc of update.docs or {}
|
||||
|
@ -203,19 +203,19 @@ define [
|
|||
|
||||
previousUpdate = update
|
||||
|
||||
firstLoad = @$scope.trackChanges.updates.length == 0
|
||||
firstLoad = @$scope.history.updates.length == 0
|
||||
|
||||
@$scope.trackChanges.updates =
|
||||
@$scope.trackChanges.updates.concat(updates)
|
||||
@$scope.history.updates =
|
||||
@$scope.history.updates.concat(updates)
|
||||
|
||||
@autoSelectRecentUpdates() if firstLoad
|
||||
|
||||
_calculateRangeFromSelection: () ->
|
||||
fromV = toV = start_ts = end_ts = null
|
||||
|
||||
selected_doc_id = @$scope.trackChanges.selection.doc?.id
|
||||
selected_doc_id = @$scope.history.selection.doc?.id
|
||||
|
||||
for update in @$scope.trackChanges.selection.updates or []
|
||||
for update in @$scope.history.selection.updates or []
|
||||
for doc_id, doc of update.docs
|
||||
if doc_id == selected_doc_id
|
||||
if fromV? and toV?
|
||||
|
@ -237,11 +237,11 @@ define [
|
|||
# then prefer this one if present.
|
||||
_selectDocFromUpdates: () ->
|
||||
affected_docs = {}
|
||||
for update in @$scope.trackChanges.selection.updates
|
||||
for update in @$scope.history.selection.updates
|
||||
for doc_id, doc of update.docs
|
||||
affected_docs[doc_id] = doc.entity
|
||||
|
||||
selected_doc = @$scope.trackChanges.selection.doc
|
||||
selected_doc = @$scope.history.selection.doc
|
||||
if selected_doc? and affected_docs[selected_doc.id]?
|
||||
# Selected doc is already open
|
||||
else
|
||||
|
@ -249,7 +249,7 @@ define [
|
|||
selected_doc = doc
|
||||
break
|
||||
|
||||
@$scope.trackChanges.selection.doc = selected_doc
|
||||
@$scope.history.selection.doc = selected_doc
|
||||
@ide.fileTreeManager.selectEntity(selected_doc)
|
||||
|
||||
_updateContainsUserId: (update, user_id) ->
|
|
@ -0,0 +1,46 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "HistoryDiffController", ($scope, $modal, ide, event_tracking) ->
|
||||
$scope.restoreDeletedDoc = () ->
|
||||
event_tracking.sendMB "history-restore-deleted"
|
||||
$scope.history.diff.restoreInProgress = true
|
||||
ide.historyManager
|
||||
.restoreDeletedDoc(
|
||||
$scope.history.diff.doc
|
||||
)
|
||||
.success (response) ->
|
||||
$scope.history.diff.restoredDocNewId = response.doc_id
|
||||
$scope.history.diff.restoreInProgress = false
|
||||
$scope.history.diff.restoreDeletedSuccess = true
|
||||
|
||||
$scope.openRestoreDiffModal = () ->
|
||||
event_tracking.sendMB "history-restore-modal"
|
||||
$modal.open {
|
||||
templateUrl: "historyRestoreDiffModalTemplate"
|
||||
controller: "HistoryRestoreDiffModalController"
|
||||
resolve:
|
||||
diff: () -> $scope.history.diff
|
||||
}
|
||||
|
||||
$scope.backToEditorAfterRestore = () ->
|
||||
ide.editorManager.openDoc({ id: $scope.history.diff.restoredDocNewId })
|
||||
|
||||
App.controller "HistoryRestoreDiffModalController", ($scope, $modalInstance, diff, ide, event_tracking) ->
|
||||
$scope.state =
|
||||
inflight: false
|
||||
|
||||
$scope.diff = diff
|
||||
|
||||
$scope.restore = () ->
|
||||
event_tracking.sendMB "history-restored"
|
||||
$scope.state.inflight = true
|
||||
ide.historyManager
|
||||
.restoreDiff(diff)
|
||||
.success () ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
ide.editorManager.openDoc(diff.doc)
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss()
|
|
@ -2,26 +2,26 @@ define [
|
|||
"base"
|
||||
], (App) ->
|
||||
|
||||
App.controller "TrackChangesPremiumPopup", ($scope, ide, sixpack)->
|
||||
App.controller "HistoryPremiumPopup", ($scope, ide, sixpack)->
|
||||
$scope.$watch "ui.view", ->
|
||||
if $scope.ui.view == "track-changes"
|
||||
if $scope.ui.view == "history"
|
||||
if $scope.project?.features?.versioning
|
||||
$scope.versioningPopupType = "default"
|
||||
else if $scope.ui.view == "track-changes"
|
||||
sixpack.participate 'track-changes-discount', ['default', 'discount'], (chosenVariation, rawResponse)->
|
||||
else if $scope.ui.view == "history"
|
||||
sixpack.participate 'history-discount', ['default', 'discount'], (chosenVariation, rawResponse)->
|
||||
$scope.versioningPopupType = chosenVariation
|
||||
|
||||
App.controller "TrackChangesListController", ["$scope", "ide", ($scope, ide) ->
|
||||
App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
|
||||
$scope.hoveringOverListSelectors = false
|
||||
|
||||
$scope.loadMore = () =>
|
||||
ide.trackChangesManager.fetchNextBatchOfUpdates()
|
||||
ide.historyManager.fetchNextBatchOfUpdates()
|
||||
|
||||
$scope.recalculateSelectedUpdates = () ->
|
||||
beforeSelection = true
|
||||
afterSelection = false
|
||||
$scope.trackChanges.selection.updates = []
|
||||
for update in $scope.trackChanges.updates
|
||||
$scope.history.selection.updates = []
|
||||
for update in $scope.history.updates
|
||||
if update.selectedTo
|
||||
inSelection = true
|
||||
beforeSelection = false
|
||||
|
@ -31,7 +31,7 @@ define [
|
|||
update.afterSelection = afterSelection
|
||||
|
||||
if inSelection
|
||||
$scope.trackChanges.selection.updates.push update
|
||||
$scope.history.selection.updates.push update
|
||||
|
||||
if update.selectedFrom
|
||||
inSelection = false
|
||||
|
@ -40,7 +40,7 @@ define [
|
|||
$scope.recalculateHoveredUpdates = () ->
|
||||
hoverSelectedFrom = false
|
||||
hoverSelectedTo = false
|
||||
for update in $scope.trackChanges.updates
|
||||
for update in $scope.history.updates
|
||||
# Figure out whether the to or from selector is hovered over
|
||||
if update.hoverSelectedFrom
|
||||
hoverSelectedFrom = true
|
||||
|
@ -50,7 +50,7 @@ define [
|
|||
if hoverSelectedFrom
|
||||
# We want to 'hover select' everything between hoverSelectedFrom and selectedTo
|
||||
inHoverSelection = false
|
||||
for update in $scope.trackChanges.updates
|
||||
for update in $scope.history.updates
|
||||
if update.selectedTo
|
||||
update.hoverSelectedTo = true
|
||||
inHoverSelection = true
|
||||
|
@ -60,7 +60,7 @@ define [
|
|||
if hoverSelectedTo
|
||||
# We want to 'hover select' everything between hoverSelectedTo and selectedFrom
|
||||
inHoverSelection = false
|
||||
for update in $scope.trackChanges.updates
|
||||
for update in $scope.history.updates
|
||||
if update.hoverSelectedTo
|
||||
inHoverSelection = true
|
||||
update.inHoverSelection = inHoverSelection
|
||||
|
@ -69,49 +69,49 @@ define [
|
|||
inHoverSelection = false
|
||||
|
||||
$scope.resetHoverState = () ->
|
||||
for update in $scope.trackChanges.updates
|
||||
for update in $scope.history.updates
|
||||
delete update.hoverSelectedFrom
|
||||
delete update.hoverSelectedTo
|
||||
delete update.inHoverSelection
|
||||
|
||||
$scope.$watch "trackChanges.updates.length", () ->
|
||||
$scope.$watch "history.updates.length", () ->
|
||||
$scope.recalculateSelectedUpdates()
|
||||
]
|
||||
|
||||
App.controller "TrackChangesListItemController", ["$scope", "event_tracking", ($scope, event_tracking) ->
|
||||
App.controller "HistoryListItemController", ["$scope", "event_tracking", ($scope, event_tracking) ->
|
||||
$scope.$watch "update.selectedFrom", (selectedFrom, oldSelectedFrom) ->
|
||||
if selectedFrom
|
||||
for update in $scope.trackChanges.updates
|
||||
for update in $scope.history.updates
|
||||
update.selectedFrom = false unless update == $scope.update
|
||||
$scope.recalculateSelectedUpdates()
|
||||
|
||||
$scope.$watch "update.selectedTo", (selectedTo, oldSelectedTo) ->
|
||||
if selectedTo
|
||||
for update in $scope.trackChanges.updates
|
||||
for update in $scope.history.updates
|
||||
update.selectedTo = false unless update == $scope.update
|
||||
$scope.recalculateSelectedUpdates()
|
||||
|
||||
$scope.select = () ->
|
||||
event_tracking.sendMB "track-changes-view-change"
|
||||
event_tracking.sendMB "history-view-change"
|
||||
$scope.update.selectedTo = true
|
||||
$scope.update.selectedFrom = true
|
||||
|
||||
$scope.mouseOverSelectedFrom = () ->
|
||||
$scope.trackChanges.hoveringOverListSelectors = true
|
||||
$scope.history.hoveringOverListSelectors = true
|
||||
$scope.update.hoverSelectedFrom = true
|
||||
$scope.recalculateHoveredUpdates()
|
||||
|
||||
$scope.mouseOutSelectedFrom = () ->
|
||||
$scope.trackChanges.hoveringOverListSelectors = false
|
||||
$scope.history.hoveringOverListSelectors = false
|
||||
$scope.resetHoverState()
|
||||
|
||||
$scope.mouseOverSelectedTo = () ->
|
||||
$scope.trackChanges.hoveringOverListSelectors = true
|
||||
$scope.history.hoveringOverListSelectors = true
|
||||
$scope.update.hoverSelectedTo = true
|
||||
$scope.recalculateHoveredUpdates()
|
||||
|
||||
$scope.mouseOutSelectedTo = () ->
|
||||
$scope.trackChanges.hoveringOverListSelectors = false
|
||||
$scope.history.hoveringOverListSelectors = false
|
||||
$scope.resetHoverState()
|
||||
|
||||
$scope.displayName = (user) ->
|
|
@ -1,46 +0,0 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "TrackChangesDiffController", ($scope, $modal, ide, event_tracking) ->
|
||||
$scope.restoreDeletedDoc = () ->
|
||||
event_tracking.sendMB "track-changes-restore-deleted"
|
||||
$scope.trackChanges.diff.restoreInProgress = true
|
||||
ide.trackChangesManager
|
||||
.restoreDeletedDoc(
|
||||
$scope.trackChanges.diff.doc
|
||||
)
|
||||
.success (response) ->
|
||||
$scope.trackChanges.diff.restoredDocNewId = response.doc_id
|
||||
$scope.trackChanges.diff.restoreInProgress = false
|
||||
$scope.trackChanges.diff.restoreDeletedSuccess = true
|
||||
|
||||
$scope.openRestoreDiffModal = () ->
|
||||
event_tracking.sendMB "track-changes-restore-modal"
|
||||
$modal.open {
|
||||
templateUrl: "trackChangesRestoreDiffModalTemplate"
|
||||
controller: "TrackChangesRestoreDiffModalController"
|
||||
resolve:
|
||||
diff: () -> $scope.trackChanges.diff
|
||||
}
|
||||
|
||||
$scope.backToEditorAfterRestore = () ->
|
||||
ide.editorManager.openDoc({ id: $scope.trackChanges.diff.restoredDocNewId })
|
||||
|
||||
App.controller "TrackChangesRestoreDiffModalController", ($scope, $modalInstance, diff, ide, event_tracking) ->
|
||||
$scope.state =
|
||||
inflight: false
|
||||
|
||||
$scope.diff = diff
|
||||
|
||||
$scope.restore = () ->
|
||||
event_tracking.sendMB "track-changes-restored"
|
||||
$scope.state.inflight = true
|
||||
ide.trackChangesManager
|
||||
.restoreDiff(diff)
|
||||
.success () ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
ide.editorManager.openDoc(diff.doc)
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss()
|
|
@ -30,4 +30,6 @@ define [], () ->
|
|||
trigger: (event, args...) ->
|
||||
@events ||= {}
|
||||
for callback in @events[event] or []
|
||||
callback.callback(args...)
|
||||
callback.callback(args...)
|
||||
|
||||
emit: (args...) -> @trigger(args...)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@import "./editor/file-tree.less";
|
||||
@import "./editor/track-changes.less";
|
||||
@import "./editor/history.less";
|
||||
@import "./editor/toolbar.less";
|
||||
@import "./editor/left-menu.less";
|
||||
@import "./editor/pdf.less";
|
||||
|
@ -146,6 +146,17 @@
|
|||
background-repeat: repeat-x;
|
||||
background-position: bottom left;
|
||||
}
|
||||
.track-changes-added-marker {
|
||||
border-radius: 0;
|
||||
position: absolute;
|
||||
background-color: hsl(100, 70%, 70%);
|
||||
}
|
||||
.track-changes-deleted-marker {
|
||||
border-radius: 0;
|
||||
position: absolute;
|
||||
border-left: 2px dotted red;
|
||||
margin-left: -1px;
|
||||
}
|
||||
.remote-cursor {
|
||||
position: absolute;
|
||||
border-left: 2px solid transparent;
|
||||
|
@ -422,4 +433,4 @@
|
|||
.dropbox-teaser-video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
@range-bar-color: @link-color;
|
||||
@range-bar-selected-offset: 14px;
|
||||
|
||||
#trackChanges {
|
||||
#history {
|
||||
.upgrade-prompt {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -272,7 +272,7 @@
|
|||
}
|
||||
|
||||
.editor-dark {
|
||||
#trackChanges {
|
||||
#history {
|
||||
aside.change-list {
|
||||
border-color: @editor-dark-toolbar-border-color;
|
||||
|
Loading…
Reference in a new issue