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
|
.ui-layout-center
|
||||||
include ./editor/editor
|
include ./editor/editor
|
||||||
include ./editor/binary-file
|
include ./editor/binary-file
|
||||||
include ./editor/track-changes
|
include ./editor/history
|
||||||
include ./editor/publish-template
|
include ./editor/publish-template
|
||||||
|
|
||||||
.ui-layout-east
|
.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']"
|
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")}
|
h3 #{translate("deleted_files")}
|
||||||
li(
|
li(
|
||||||
ng-class="{ 'selected': entity.selected }",
|
ng-class="{ 'selected': entity.selected }",
|
||||||
ng-repeat="entity in deletedDocs | orderBy:'name'",
|
ng-repeat="entity in deletedDocs | orderBy:'name'",
|
||||||
ng-controller="FileTreeEntityController",
|
ng-controller="FileTreeEntityController",
|
||||||
ng-show="ui.view == 'track-changes'"
|
ng-show="ui.view == 'history'"
|
||||||
)
|
)
|
||||||
.entity
|
.entity
|
||||||
.entity-name(
|
.entity-name(
|
||||||
|
|
|
@ -102,8 +102,8 @@ div(ng-if="!shouldABTestHeaderLabels")
|
||||||
i.fa.fa-fw.fa-group
|
i.fa.fa-fw.fa-group
|
||||||
a.btn.btn-full-height(
|
a.btn.btn-full-height(
|
||||||
href,
|
href,
|
||||||
ng-click="toggleTrackChanges()",
|
ng-click="toggleHistory()",
|
||||||
ng-class="{ active: (ui.view == 'track-changes') }"
|
ng-class="{ active: (ui.view == 'history') }"
|
||||||
tooltip="#{translate('recent_changes')}",
|
tooltip="#{translate('recent_changes')}",
|
||||||
tooltip-placement="bottom",
|
tooltip-placement="bottom",
|
||||||
)
|
)
|
||||||
|
@ -232,8 +232,8 @@ div(ng-if="shouldABTestHeaderLabels")
|
||||||
i.fa.fa-fw.fa-group
|
i.fa.fa-fw.fa-group
|
||||||
a.btn.btn-full-height(
|
a.btn.btn-full-height(
|
||||||
href,
|
href,
|
||||||
ng-click="toggleTrackChanges(); trackABTestConversion('history');",
|
ng-click="toggleHistory(); trackABTestConversion('history');",
|
||||||
ng-class="{ active: (ui.view == 'track-changes') }"
|
ng-class="{ active: (ui.view == 'history') }"
|
||||||
tooltip="#{translate('recent_changes')}",
|
tooltip="#{translate('recent_changes')}",
|
||||||
tooltip-placement="bottom",
|
tooltip-placement="bottom",
|
||||||
sixpack-convert="editor-header"
|
sixpack-convert="editor-header"
|
||||||
|
@ -357,8 +357,8 @@ div(ng-if="shouldABTestHeaderLabels")
|
||||||
p.toolbar-label #{translate("share")}
|
p.toolbar-label #{translate("share")}
|
||||||
a.btn.btn-full-height(
|
a.btn.btn-full-height(
|
||||||
href,
|
href,
|
||||||
ng-click="toggleTrackChanges(); trackABTestConversion('history');",
|
ng-click="toggleHistory(); trackABTestConversion('history');",
|
||||||
ng-class="{ active: (ui.view == 'track-changes') }",
|
ng-class="{ active: (ui.view == 'history') }",
|
||||||
sixpack-convert="editor-header"
|
sixpack-convert="editor-header"
|
||||||
)
|
)
|
||||||
i.fa.fa-fw.fa-history
|
i.fa.fa-fw.fa-history
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
div#trackChanges(ng-show="ui.view == 'track-changes'")
|
div#history(ng-show="ui.view == 'history'")
|
||||||
span(ng-controller="TrackChangesPremiumPopup")
|
span(ng-controller="HistoryPremiumPopup")
|
||||||
.upgrade-prompt(ng-show="!project.features.versioning")
|
.upgrade-prompt(ng-show="!project.features.versioning")
|
||||||
.message(ng-show="project.owner._id == user.id")
|
.message(ng-show="project.owner._id == user.id")
|
||||||
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
|
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(
|
a.btn.btn-success(
|
||||||
href
|
href
|
||||||
ng-class="buttonClass"
|
ng-class="buttonClass"
|
||||||
ng-click="startFreeTrial('track-changes')"
|
ng-click="startFreeTrial('history')"
|
||||||
) #{translate("start_free_trial")}
|
) #{translate("start_free_trial")}
|
||||||
|
|
||||||
|
|
||||||
.message(ng-show="project.owner._id != user.id")
|
.message(ng-show="project.owner._id != user.id")
|
||||||
p #{translate("ask_proj_owner_to_upgrade_for_history")}
|
p #{translate("ask_proj_owner_to_upgrade_for_history")}
|
||||||
p
|
p
|
||||||
a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
|
a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
|
||||||
|
|
||||||
aside.change-list(
|
aside.change-list(
|
||||||
ng-controller="TrackChangesListController"
|
ng-controller="HistoryListController"
|
||||||
infinite-scroll="loadMore()"
|
infinite-scroll="loadMore()"
|
||||||
infinite-scroll-disabled="trackChanges.loading || trackChanges.atEnd"
|
infinite-scroll-disabled="history.loading || history.atEnd"
|
||||||
infinite-scroll-initialize="ui.view == 'track-changes'"
|
infinite-scroll-initialize="ui.view == 'history'"
|
||||||
)
|
)
|
||||||
.infinite-scroll-inner
|
.infinite-scroll-inner
|
||||||
ul.list-unstyled(
|
ul.list-unstyled(
|
||||||
ng-class="{\
|
ng-class="{\
|
||||||
'hover-state': trackChanges.hoveringOverListSelectors\
|
'hover-state': history.hoveringOverListSelectors\
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
li.change(
|
li.change(
|
||||||
ng-repeat="update in trackChanges.updates"
|
ng-repeat="update in history.updates"
|
||||||
ng-class="{\
|
ng-class="{\
|
||||||
'first-in-day': update.meta.first_in_day,\
|
'first-in-day': update.meta.first_in_day,\
|
||||||
'selected': update.inSelection,\
|
'selected': update.inSelection,\
|
||||||
|
@ -65,7 +65,7 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
||||||
'hover-selected-to': update.hoverSelectedTo,\
|
'hover-selected-to': update.hoverSelectedTo,\
|
||||||
'hover-selected-from': update.hoverSelectedFrom,\
|
'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 }}
|
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%)")
|
.color-square(style="background-color: hsl(100, 100%, 50%)")
|
||||||
span #{translate("anonymous")}
|
span #{translate("anonymous")}
|
||||||
|
|
||||||
.loading(ng-show="trackChanges.loading")
|
.loading(ng-show="history.loading")
|
||||||
i.fa.fa-spin.fa-refresh
|
i.fa.fa-spin.fa-refresh
|
||||||
| #{translate("loading")}...
|
| #{translate("loading")}...
|
||||||
|
|
||||||
.diff-panel.full-size(ng-controller="TrackChangesDiffController")
|
.diff-panel.full-size(ng-controller="HistoryDiffController")
|
||||||
.diff(
|
.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
|
.toolbar.toolbar-alt
|
||||||
span.name
|
span.name
|
||||||
| <strong>{{trackChanges.diff.highlights.length}} </strong>
|
| <strong>{{history.diff.highlights.length}} </strong>
|
||||||
ng-pluralize(
|
ng-pluralize(
|
||||||
count="trackChanges.diff.highlights.length",
|
count="history.diff.highlights.length",
|
||||||
when="{\
|
when="{\
|
||||||
'one': 'change',\
|
'one': 'change',\
|
||||||
'other': 'changes'\
|
'other': 'changes'\
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
| in <strong>{{trackChanges.diff.doc.name}}</strong>
|
| in <strong>{{history.diff.doc.name}}</strong>
|
||||||
.toolbar-right
|
.toolbar-right
|
||||||
a.btn.btn-danger.btn-sm(
|
a.btn.btn-danger.btn-sm(
|
||||||
href,
|
href,
|
||||||
ng-click="openRestoreDiffModal()"
|
ng-click="openRestoreDiffModal()"
|
||||||
) #{translate("restore_to_before_these_changes")}
|
) #{translate("restore_to_before_these_changes")}
|
||||||
.diff-editor.hide-ace-cursor(
|
.diff-editor.hide-ace-cursor(
|
||||||
ace-editor="track-changes",
|
ace-editor="history",
|
||||||
theme="settings.theme",
|
theme="settings.theme",
|
||||||
font-size="settings.fontSize",
|
font-size="settings.fontSize",
|
||||||
text="trackChanges.diff.text",
|
text="history.diff.text",
|
||||||
highlights="trackChanges.diff.highlights",
|
highlights="history.diff.highlights",
|
||||||
read-only="true",
|
read-only="true",
|
||||||
resize-on="layout:main:resize",
|
resize-on="layout:main:resize",
|
||||||
navigate-highlights="true"
|
navigate-highlights="true"
|
||||||
)
|
)
|
||||||
.diff-deleted.text-centered(
|
.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
|
p
|
||||||
a.btn.btn-primary.btn-lg(
|
a.btn.btn-primary.btn-lg(
|
||||||
href,
|
href,
|
||||||
ng-click="restoreDeletedDoc()",
|
ng-click="restoreDeletedDoc()",
|
||||||
ng-disabled="trackChanges.diff.restoreInProgress"
|
ng-disabled="history.diff.restoreInProgress"
|
||||||
) #{translate("restore")}
|
) #{translate("restore")}
|
||||||
|
|
||||||
.diff-deleted.text-centered(
|
.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.text-serif #{translate("file_restored_back_to_editor")}
|
||||||
p
|
p
|
||||||
a.btn.btn-default(
|
a.btn.btn-default(
|
||||||
|
@ -164,13 +164,13 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
|
||||||
ng-click="backToEditorAfterRestore()",
|
ng-click="backToEditorAfterRestore()",
|
||||||
) #{translate("file_restored_back_to_editor_btn")}
|
) #{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
|
i.fa.fa-spin.fa-refresh
|
||||||
| #{translate("loading")}...
|
| #{translate("loading")}...
|
||||||
.error-panel(ng-show="trackChanges.diff.error")
|
.error-panel(ng-show="history.diff.error")
|
||||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
||||||
|
|
||||||
script(type="text/ng-template", id="trackChangesRestoreDiffModalTemplate")
|
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
|
||||||
.modal-header
|
.modal-header
|
||||||
button.close(
|
button.close(
|
||||||
type="button"
|
type="button"
|
|
@ -4,7 +4,7 @@ define [
|
||||||
"ide/connection/ConnectionManager"
|
"ide/connection/ConnectionManager"
|
||||||
"ide/editor/EditorManager"
|
"ide/editor/EditorManager"
|
||||||
"ide/online-users/OnlineUsersManager"
|
"ide/online-users/OnlineUsersManager"
|
||||||
"ide/track-changes/TrackChangesManager"
|
"ide/history/HistoryManager"
|
||||||
"ide/permissions/PermissionsManager"
|
"ide/permissions/PermissionsManager"
|
||||||
"ide/pdf/PdfManager"
|
"ide/pdf/PdfManager"
|
||||||
"ide/binary-files/BinaryFilesManager"
|
"ide/binary-files/BinaryFilesManager"
|
||||||
|
@ -36,7 +36,7 @@ define [
|
||||||
ConnectionManager
|
ConnectionManager
|
||||||
EditorManager
|
EditorManager
|
||||||
OnlineUsersManager
|
OnlineUsersManager
|
||||||
TrackChangesManager
|
HistoryManager
|
||||||
PermissionsManager
|
PermissionsManager
|
||||||
PdfManager
|
PdfManager
|
||||||
BinaryFilesManager
|
BinaryFilesManager
|
||||||
|
@ -116,7 +116,7 @@ define [
|
||||||
ide.fileTreeManager = new FileTreeManager(ide, $scope)
|
ide.fileTreeManager = new FileTreeManager(ide, $scope)
|
||||||
ide.editorManager = new EditorManager(ide, $scope)
|
ide.editorManager = new EditorManager(ide, $scope)
|
||||||
ide.onlineUsersManager = new OnlineUsersManager(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.pdfManager = new PdfManager(ide, $scope)
|
||||||
ide.permissionsManager = new PermissionsManager(ide, $scope)
|
ide.permissionsManager = new PermissionsManager(ide, $scope)
|
||||||
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
|
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
|
||||||
|
|
|
@ -7,7 +7,8 @@ define [
|
||||||
"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
|
"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
|
||||||
"ide/editor/directives/aceEditor/highlights/HighlightsManager"
|
"ide/editor/directives/aceEditor/highlights/HighlightsManager"
|
||||||
"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
|
"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
|
EditSession = ace.require('ace/edit_session').EditSession
|
||||||
|
|
||||||
# set the path for ace workers if using a CDN (from editor.jade)
|
# set the path for ace workers if using a CDN (from editor.jade)
|
||||||
|
@ -69,6 +70,9 @@ define [
|
||||||
undoManager = new UndoManager(scope, editor, element)
|
undoManager = new UndoManager(scope, editor, element)
|
||||||
highlightsManager = new HighlightsManager(scope, editor, element)
|
highlightsManager = new HighlightsManager(scope, editor, element)
|
||||||
cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
|
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
|
# Prevert Ctrl|Cmd-S from triggering save dialog
|
||||||
editor.commands.addCommand
|
editor.commands.addCommand
|
||||||
|
@ -222,7 +226,9 @@ define [
|
||||||
sharejs_doc.on "remoteop.recordForUndo", () =>
|
sharejs_doc.on "remoteop.recordForUndo", () =>
|
||||||
undoManager.nextUpdateIsRemote = true
|
undoManager.nextUpdateIsRemote = true
|
||||||
|
|
||||||
|
editor.initing = true
|
||||||
sharejs_doc.attachToAce(editor)
|
sharejs_doc.attachToAce(editor)
|
||||||
|
editor.initing = false
|
||||||
# need to set annotations after attaching because attaching
|
# need to set annotations after attaching because attaching
|
||||||
# deletes and then inserts document content
|
# deletes and then inserts document content
|
||||||
session.setAnnotations scope.annotations
|
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 [
|
define [
|
||||||
"moment"
|
"moment"
|
||||||
"ide/track-changes/controllers/TrackChangesListController"
|
"ide/history/controllers/HistoryListController"
|
||||||
"ide/track-changes/controllers/TrackChangesDiffController"
|
"ide/history/controllers/HistoryDiffController"
|
||||||
"ide/track-changes/directives/infiniteScroll"
|
"ide/history/directives/infiniteScroll"
|
||||||
], (moment) ->
|
], (moment) ->
|
||||||
class TrackChangesManager
|
class HistoryManager
|
||||||
constructor: (@ide, @$scope) ->
|
constructor: (@ide, @$scope) ->
|
||||||
@reset()
|
@reset()
|
||||||
|
|
||||||
@$scope.toggleTrackChanges = () =>
|
@$scope.toggleHistory = () =>
|
||||||
if @$scope.ui.view == "track-changes"
|
if @$scope.ui.view == "history"
|
||||||
@hide()
|
@hide()
|
||||||
else
|
else
|
||||||
@show()
|
@show()
|
||||||
|
|
||||||
@$scope.$watch "trackChanges.selection.updates", (updates) =>
|
@$scope.$watch "history.selection.updates", (updates) =>
|
||||||
if updates? and updates.length > 0
|
if updates? and updates.length > 0
|
||||||
@_selectDocFromUpdates()
|
@_selectDocFromUpdates()
|
||||||
@reloadDiff()
|
@reloadDiff()
|
||||||
|
|
||||||
@$scope.$on "entity:selected", (event, entity) =>
|
@$scope.$on "entity:selected", (event, entity) =>
|
||||||
if (@$scope.ui.view == "track-changes") and (entity.type == "doc")
|
if (@$scope.ui.view == "history") and (entity.type == "doc")
|
||||||
@$scope.trackChanges.selection.doc = entity
|
@$scope.history.selection.doc = entity
|
||||||
@reloadDiff()
|
@reloadDiff()
|
||||||
|
|
||||||
show: () ->
|
show: () ->
|
||||||
@$scope.ui.view = "track-changes"
|
@$scope.ui.view = "history"
|
||||||
@reset()
|
@reset()
|
||||||
|
|
||||||
hide: () ->
|
hide: () ->
|
||||||
|
@ -34,7 +34,7 @@ define [
|
||||||
@$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity()
|
@$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity()
|
||||||
|
|
||||||
reset: () ->
|
reset: () ->
|
||||||
@$scope.trackChanges = {
|
@$scope.history = {
|
||||||
updates: []
|
updates: []
|
||||||
nextBeforeTimestamp: null
|
nextBeforeTimestamp: null
|
||||||
atEnd: false
|
atEnd: false
|
||||||
|
@ -52,36 +52,36 @@ define [
|
||||||
}
|
}
|
||||||
|
|
||||||
autoSelectRecentUpdates: () ->
|
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
|
indexOfLastUpdateNotByMe = 0
|
||||||
for update, i in @$scope.trackChanges.updates
|
for update, i in @$scope.history.updates
|
||||||
if @_updateContainsUserId(update, @$scope.user.id)
|
if @_updateContainsUserId(update, @$scope.user.id)
|
||||||
break
|
break
|
||||||
indexOfLastUpdateNotByMe = i
|
indexOfLastUpdateNotByMe = i
|
||||||
|
|
||||||
@$scope.trackChanges.updates[indexOfLastUpdateNotByMe].selectedFrom = true
|
@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
|
||||||
|
|
||||||
BATCH_SIZE: 10
|
BATCH_SIZE: 10
|
||||||
fetchNextBatchOfUpdates: () ->
|
fetchNextBatchOfUpdates: () ->
|
||||||
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
|
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
|
||||||
if @$scope.trackChanges.nextBeforeTimestamp?
|
if @$scope.history.nextBeforeTimestamp?
|
||||||
url += "&before=#{@$scope.trackChanges.nextBeforeTimestamp}"
|
url += "&before=#{@$scope.history.nextBeforeTimestamp}"
|
||||||
@$scope.trackChanges.loading = true
|
@$scope.history.loading = true
|
||||||
@ide.$http
|
@ide.$http
|
||||||
.get(url)
|
.get(url)
|
||||||
.success (data) =>
|
.success (data) =>
|
||||||
@_loadUpdates(data.updates)
|
@_loadUpdates(data.updates)
|
||||||
@$scope.trackChanges.nextBeforeTimestamp = data.nextBeforeTimestamp
|
@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
|
||||||
if !data.nextBeforeTimestamp?
|
if !data.nextBeforeTimestamp?
|
||||||
@$scope.trackChanges.atEnd = true
|
@$scope.history.atEnd = true
|
||||||
@$scope.trackChanges.loading = false
|
@$scope.history.loading = false
|
||||||
|
|
||||||
reloadDiff: () ->
|
reloadDiff: () ->
|
||||||
diff = @$scope.trackChanges.diff
|
diff = @$scope.history.diff
|
||||||
{updates, doc} = @$scope.trackChanges.selection
|
{updates, doc} = @$scope.history.selection
|
||||||
{fromV, toV, start_ts, end_ts} = @_calculateRangeFromSelection()
|
{fromV, toV, start_ts, end_ts} = @_calculateRangeFromSelection()
|
||||||
|
|
||||||
return if !doc?
|
return if !doc?
|
||||||
|
@ -91,7 +91,7 @@ define [
|
||||||
diff.fromV == fromV and
|
diff.fromV == fromV and
|
||||||
diff.toV == toV
|
diff.toV == toV
|
||||||
|
|
||||||
@$scope.trackChanges.diff = diff = {
|
@$scope.history.diff = diff = {
|
||||||
fromV: fromV
|
fromV: fromV
|
||||||
toV: toV
|
toV: toV
|
||||||
start_ts: start_ts
|
start_ts: start_ts
|
||||||
|
@ -184,7 +184,7 @@ define [
|
||||||
return {text, highlights}
|
return {text, highlights}
|
||||||
|
|
||||||
_loadUpdates: (updates = []) ->
|
_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 update in updates
|
||||||
for doc_id, doc of update.docs or {}
|
for doc_id, doc of update.docs or {}
|
||||||
|
@ -203,19 +203,19 @@ define [
|
||||||
|
|
||||||
previousUpdate = update
|
previousUpdate = update
|
||||||
|
|
||||||
firstLoad = @$scope.trackChanges.updates.length == 0
|
firstLoad = @$scope.history.updates.length == 0
|
||||||
|
|
||||||
@$scope.trackChanges.updates =
|
@$scope.history.updates =
|
||||||
@$scope.trackChanges.updates.concat(updates)
|
@$scope.history.updates.concat(updates)
|
||||||
|
|
||||||
@autoSelectRecentUpdates() if firstLoad
|
@autoSelectRecentUpdates() if firstLoad
|
||||||
|
|
||||||
_calculateRangeFromSelection: () ->
|
_calculateRangeFromSelection: () ->
|
||||||
fromV = toV = start_ts = end_ts = null
|
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
|
for doc_id, doc of update.docs
|
||||||
if doc_id == selected_doc_id
|
if doc_id == selected_doc_id
|
||||||
if fromV? and toV?
|
if fromV? and toV?
|
||||||
|
@ -237,11 +237,11 @@ define [
|
||||||
# then prefer this one if present.
|
# then prefer this one if present.
|
||||||
_selectDocFromUpdates: () ->
|
_selectDocFromUpdates: () ->
|
||||||
affected_docs = {}
|
affected_docs = {}
|
||||||
for update in @$scope.trackChanges.selection.updates
|
for update in @$scope.history.selection.updates
|
||||||
for doc_id, doc of update.docs
|
for doc_id, doc of update.docs
|
||||||
affected_docs[doc_id] = doc.entity
|
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]?
|
if selected_doc? and affected_docs[selected_doc.id]?
|
||||||
# Selected doc is already open
|
# Selected doc is already open
|
||||||
else
|
else
|
||||||
|
@ -249,7 +249,7 @@ define [
|
||||||
selected_doc = doc
|
selected_doc = doc
|
||||||
break
|
break
|
||||||
|
|
||||||
@$scope.trackChanges.selection.doc = selected_doc
|
@$scope.history.selection.doc = selected_doc
|
||||||
@ide.fileTreeManager.selectEntity(selected_doc)
|
@ide.fileTreeManager.selectEntity(selected_doc)
|
||||||
|
|
||||||
_updateContainsUserId: (update, user_id) ->
|
_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"
|
"base"
|
||||||
], (App) ->
|
], (App) ->
|
||||||
|
|
||||||
App.controller "TrackChangesPremiumPopup", ($scope, ide, sixpack)->
|
App.controller "HistoryPremiumPopup", ($scope, ide, sixpack)->
|
||||||
$scope.$watch "ui.view", ->
|
$scope.$watch "ui.view", ->
|
||||||
if $scope.ui.view == "track-changes"
|
if $scope.ui.view == "history"
|
||||||
if $scope.project?.features?.versioning
|
if $scope.project?.features?.versioning
|
||||||
$scope.versioningPopupType = "default"
|
$scope.versioningPopupType = "default"
|
||||||
else if $scope.ui.view == "track-changes"
|
else if $scope.ui.view == "history"
|
||||||
sixpack.participate 'track-changes-discount', ['default', 'discount'], (chosenVariation, rawResponse)->
|
sixpack.participate 'history-discount', ['default', 'discount'], (chosenVariation, rawResponse)->
|
||||||
$scope.versioningPopupType = chosenVariation
|
$scope.versioningPopupType = chosenVariation
|
||||||
|
|
||||||
App.controller "TrackChangesListController", ["$scope", "ide", ($scope, ide) ->
|
App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
|
||||||
$scope.hoveringOverListSelectors = false
|
$scope.hoveringOverListSelectors = false
|
||||||
|
|
||||||
$scope.loadMore = () =>
|
$scope.loadMore = () =>
|
||||||
ide.trackChangesManager.fetchNextBatchOfUpdates()
|
ide.historyManager.fetchNextBatchOfUpdates()
|
||||||
|
|
||||||
$scope.recalculateSelectedUpdates = () ->
|
$scope.recalculateSelectedUpdates = () ->
|
||||||
beforeSelection = true
|
beforeSelection = true
|
||||||
afterSelection = false
|
afterSelection = false
|
||||||
$scope.trackChanges.selection.updates = []
|
$scope.history.selection.updates = []
|
||||||
for update in $scope.trackChanges.updates
|
for update in $scope.history.updates
|
||||||
if update.selectedTo
|
if update.selectedTo
|
||||||
inSelection = true
|
inSelection = true
|
||||||
beforeSelection = false
|
beforeSelection = false
|
||||||
|
@ -31,7 +31,7 @@ define [
|
||||||
update.afterSelection = afterSelection
|
update.afterSelection = afterSelection
|
||||||
|
|
||||||
if inSelection
|
if inSelection
|
||||||
$scope.trackChanges.selection.updates.push update
|
$scope.history.selection.updates.push update
|
||||||
|
|
||||||
if update.selectedFrom
|
if update.selectedFrom
|
||||||
inSelection = false
|
inSelection = false
|
||||||
|
@ -40,7 +40,7 @@ define [
|
||||||
$scope.recalculateHoveredUpdates = () ->
|
$scope.recalculateHoveredUpdates = () ->
|
||||||
hoverSelectedFrom = false
|
hoverSelectedFrom = false
|
||||||
hoverSelectedTo = 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
|
# Figure out whether the to or from selector is hovered over
|
||||||
if update.hoverSelectedFrom
|
if update.hoverSelectedFrom
|
||||||
hoverSelectedFrom = true
|
hoverSelectedFrom = true
|
||||||
|
@ -50,7 +50,7 @@ define [
|
||||||
if hoverSelectedFrom
|
if hoverSelectedFrom
|
||||||
# We want to 'hover select' everything between hoverSelectedFrom and selectedTo
|
# We want to 'hover select' everything between hoverSelectedFrom and selectedTo
|
||||||
inHoverSelection = false
|
inHoverSelection = false
|
||||||
for update in $scope.trackChanges.updates
|
for update in $scope.history.updates
|
||||||
if update.selectedTo
|
if update.selectedTo
|
||||||
update.hoverSelectedTo = true
|
update.hoverSelectedTo = true
|
||||||
inHoverSelection = true
|
inHoverSelection = true
|
||||||
|
@ -60,7 +60,7 @@ define [
|
||||||
if hoverSelectedTo
|
if hoverSelectedTo
|
||||||
# We want to 'hover select' everything between hoverSelectedTo and selectedFrom
|
# We want to 'hover select' everything between hoverSelectedTo and selectedFrom
|
||||||
inHoverSelection = false
|
inHoverSelection = false
|
||||||
for update in $scope.trackChanges.updates
|
for update in $scope.history.updates
|
||||||
if update.hoverSelectedTo
|
if update.hoverSelectedTo
|
||||||
inHoverSelection = true
|
inHoverSelection = true
|
||||||
update.inHoverSelection = inHoverSelection
|
update.inHoverSelection = inHoverSelection
|
||||||
|
@ -69,49 +69,49 @@ define [
|
||||||
inHoverSelection = false
|
inHoverSelection = false
|
||||||
|
|
||||||
$scope.resetHoverState = () ->
|
$scope.resetHoverState = () ->
|
||||||
for update in $scope.trackChanges.updates
|
for update in $scope.history.updates
|
||||||
delete update.hoverSelectedFrom
|
delete update.hoverSelectedFrom
|
||||||
delete update.hoverSelectedTo
|
delete update.hoverSelectedTo
|
||||||
delete update.inHoverSelection
|
delete update.inHoverSelection
|
||||||
|
|
||||||
$scope.$watch "trackChanges.updates.length", () ->
|
$scope.$watch "history.updates.length", () ->
|
||||||
$scope.recalculateSelectedUpdates()
|
$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) ->
|
$scope.$watch "update.selectedFrom", (selectedFrom, oldSelectedFrom) ->
|
||||||
if selectedFrom
|
if selectedFrom
|
||||||
for update in $scope.trackChanges.updates
|
for update in $scope.history.updates
|
||||||
update.selectedFrom = false unless update == $scope.update
|
update.selectedFrom = false unless update == $scope.update
|
||||||
$scope.recalculateSelectedUpdates()
|
$scope.recalculateSelectedUpdates()
|
||||||
|
|
||||||
$scope.$watch "update.selectedTo", (selectedTo, oldSelectedTo) ->
|
$scope.$watch "update.selectedTo", (selectedTo, oldSelectedTo) ->
|
||||||
if selectedTo
|
if selectedTo
|
||||||
for update in $scope.trackChanges.updates
|
for update in $scope.history.updates
|
||||||
update.selectedTo = false unless update == $scope.update
|
update.selectedTo = false unless update == $scope.update
|
||||||
$scope.recalculateSelectedUpdates()
|
$scope.recalculateSelectedUpdates()
|
||||||
|
|
||||||
$scope.select = () ->
|
$scope.select = () ->
|
||||||
event_tracking.sendMB "track-changes-view-change"
|
event_tracking.sendMB "history-view-change"
|
||||||
$scope.update.selectedTo = true
|
$scope.update.selectedTo = true
|
||||||
$scope.update.selectedFrom = true
|
$scope.update.selectedFrom = true
|
||||||
|
|
||||||
$scope.mouseOverSelectedFrom = () ->
|
$scope.mouseOverSelectedFrom = () ->
|
||||||
$scope.trackChanges.hoveringOverListSelectors = true
|
$scope.history.hoveringOverListSelectors = true
|
||||||
$scope.update.hoverSelectedFrom = true
|
$scope.update.hoverSelectedFrom = true
|
||||||
$scope.recalculateHoveredUpdates()
|
$scope.recalculateHoveredUpdates()
|
||||||
|
|
||||||
$scope.mouseOutSelectedFrom = () ->
|
$scope.mouseOutSelectedFrom = () ->
|
||||||
$scope.trackChanges.hoveringOverListSelectors = false
|
$scope.history.hoveringOverListSelectors = false
|
||||||
$scope.resetHoverState()
|
$scope.resetHoverState()
|
||||||
|
|
||||||
$scope.mouseOverSelectedTo = () ->
|
$scope.mouseOverSelectedTo = () ->
|
||||||
$scope.trackChanges.hoveringOverListSelectors = true
|
$scope.history.hoveringOverListSelectors = true
|
||||||
$scope.update.hoverSelectedTo = true
|
$scope.update.hoverSelectedTo = true
|
||||||
$scope.recalculateHoveredUpdates()
|
$scope.recalculateHoveredUpdates()
|
||||||
|
|
||||||
$scope.mouseOutSelectedTo = () ->
|
$scope.mouseOutSelectedTo = () ->
|
||||||
$scope.trackChanges.hoveringOverListSelectors = false
|
$scope.history.hoveringOverListSelectors = false
|
||||||
$scope.resetHoverState()
|
$scope.resetHoverState()
|
||||||
|
|
||||||
$scope.displayName = (user) ->
|
$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...) ->
|
trigger: (event, args...) ->
|
||||||
@events ||= {}
|
@events ||= {}
|
||||||
for callback in @events[event] or []
|
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/file-tree.less";
|
||||||
@import "./editor/track-changes.less";
|
@import "./editor/history.less";
|
||||||
@import "./editor/toolbar.less";
|
@import "./editor/toolbar.less";
|
||||||
@import "./editor/left-menu.less";
|
@import "./editor/left-menu.less";
|
||||||
@import "./editor/pdf.less";
|
@import "./editor/pdf.less";
|
||||||
|
@ -146,6 +146,17 @@
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
background-position: bottom left;
|
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 {
|
.remote-cursor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-left: 2px solid transparent;
|
border-left: 2px solid transparent;
|
||||||
|
@ -422,4 +433,4 @@
|
||||||
.dropbox-teaser-video {
|
.dropbox-teaser-video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
@range-bar-color: @link-color;
|
@range-bar-color: @link-color;
|
||||||
@range-bar-selected-offset: 14px;
|
@range-bar-selected-offset: 14px;
|
||||||
|
|
||||||
#trackChanges {
|
#history {
|
||||||
.upgrade-prompt {
|
.upgrade-prompt {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -272,7 +272,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-dark {
|
.editor-dark {
|
||||||
#trackChanges {
|
#history {
|
||||||
aside.change-list {
|
aside.change-list {
|
||||||
border-color: @editor-dark-toolbar-border-color;
|
border-color: @editor-dark-toolbar-border-color;
|
||||||
|
|
Loading…
Reference in a new issue