diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee
index cb58399d80..9d7e8c3b14 100644
--- a/services/web/Gruntfile.coffee
+++ b/services/web/Gruntfile.coffee
@@ -37,15 +37,13 @@ module.exports = (grunt) ->
join: true
files:
"public/js/libs/sharejs.js": [
- "public/coffee/editor/ShareJSHeader.coffee"
- "public/coffee/editor/sharejs/types/helpers.coffee"
- "public/coffee/editor/sharejs/types/text.coffee"
- "public/coffee/editor/sharejs/types/text-api.coffee"
- "public/coffee/editor/sharejs/types/json.coffee"
- "public/coffee/editor/sharejs/types/json-api.coffee"
- "public/coffee/editor/sharejs/client/microevent.coffee"
- "public/coffee/editor/sharejs/client/doc.coffee"
- "public/coffee/editor/sharejs/client/ace.coffee"
+ "public/coffee/ide/editor/sharejs/header.coffee"
+ "public/coffee/ide/editor/sharejs/vendor/types/helpers.coffee"
+ "public/coffee/ide/editor/sharejs/vendor/types/text.coffee"
+ "public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee"
+ "public/coffee/ide/editor/sharejs/vendor/client/microevent.coffee"
+ "public/coffee/ide/editor/sharejs/vendor/client/doc.coffee"
+ "public/coffee/ide/editor/sharejs/vendor/client/ace.coffee"
]
client:
@@ -89,12 +87,8 @@ module.exports = (grunt) ->
inlineText: false
preserveLicenseComments: false
paths:
- "underscore": "libs/underscore"
- "jquery": "libs/jquery"
- "moment": "libs/moment"
+ "moment": "libs/moment-2.7.0"
shim:
- "libs/backbone":
- deps: ["libs/underscore"]
"libs/pdfListView/PdfListView":
deps: ["libs/pdf"]
"libs/pdf":
@@ -104,16 +98,12 @@ module.exports = (grunt) ->
modules: [
{
name: "main",
- exclude: ["jquery"]
+ exclude: ["libs"]
}, {
name: "ide",
- exclude: ["jquery"]
+ exclude: ["libs", "libs/jquery-layout"]
}, {
- name: "home",
- exclude: ["jquery"]
- }, {
- name: "list",
- exclude: ["jquery"]
+ name: "libs"
}
]
diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade
index c0c5847faa..576e0d8d76 100644
--- a/services/web/app/views/layout.jade
+++ b/services/web/app/views/layout.jade
@@ -30,11 +30,8 @@ html(itemscope, itemtype='http://schema.org/Product')
script(type="text/javascript").
window.csrfToken = "#{csrfToken}";
- script(src=jsPath+'libs/jquery.js')
- script(src=jsPath+'libs/angular-1.2.17.js')
- script(src=jsPath+'libs/moment-2.4.0.js')
- script(src=jsPath+'libs/underscore-1.3.3.js')
- block scripts
+ script(src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js")
+ script(src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js")
body
- if(typeof(suppressNavbar) == "undefined")
@@ -61,10 +58,13 @@ html(itemscope, itemtype='http://schema.org/Product')
- if(typeof(suppressFooter) == "undefined")
script(type='text/javascript').
window.requirejs = {
- "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'app/main.js')}"
+ "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'app/main.js')}",
+ "paths" : {
+ "moment": "libs/moment-2.7.0"
+ }
};
script(
- data-main=jsPath+'app/main.js',
+ data-main=jsPath+'main.js',
baseurl=jsPath,
src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js')
)
diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade
index 2f0665b2f0..15a593f044 100644
--- a/services/web/app/views/project/editor.jade
+++ b/services/web/app/views/project/editor.jade
@@ -5,13 +5,6 @@ block vars
- var suppressFooter = true
- var suppressDefaultJs = true
-block scripts
- //- Only use the native bootstrap on the editor page,
- //- since we use the Angular-based bootstrap elsewhere.
- //- script(src=jsPath+'libs/bootstrap-3.1.1.js')
- script(src=jsPath+'libs/jquery-layout.js')
- script(src=jsPath+'libs/jquery.storage.js')
-
block content
.editor(ng-controller="IdeController")
.loading-screen(ng-show="state.loading")
@@ -92,19 +85,12 @@ block content
window.csrfToken = "!{csrfToken}";
window.requirejs = {
"paths" : {
- "underscore": "../libs/underscore-1.3.3",
"mathjax": "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML",
- "moment": "libs/moment-2.4.0",
- "ace": "#{jsPath}ace",
- "libs": "#{jsPath}libs",
- "text": "#{jsPath}text"
+ "moment": "libs/moment-2.7.0"
},
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}",
"waitSeconds": 0,
"shim": {
- "libs/backbone": {
- deps: ["libs/underscore-1.3.3"]
- },
"libs/pdfListView/PdfListView": {
deps: ["libs/pdf"]
},
@@ -125,7 +111,7 @@ block content
window.sharelatex.pdfJsWorkerPath = "#{pdfJsWorkerPath}"
script(
- data-main=jsPath+'app/ide.js',
+ data-main=jsPath+'ide.js',
baseurl=jsPath,
data-ace-base=jsPath+'ace',
src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js')
diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade
index e75b5091fb..821e9e7ee6 100644
--- a/services/web/app/views/project/list.jade
+++ b/services/web/app/views/project/list.jade
@@ -1,6 +1,6 @@
extends ../layout
-block scripts
+block content
script(type="text/javascript").
window.data = {
projects: !{JSON.stringify(projects)},
@@ -13,7 +13,6 @@ block scripts
}
};
-block content
.content.content-alt(ng-controller="ProjectPageController")
.container
.row
diff --git a/services/web/public/coffee/SubscriptionGroupsManager.coffee b/services/web/public/coffee/SubscriptionGroupsManager.coffee
deleted file mode 100644
index 053eca14b6..0000000000
--- a/services/web/public/coffee/SubscriptionGroupsManager.coffee
+++ /dev/null
@@ -1,88 +0,0 @@
-require [
- "libs/mustache"
- "./main"
- "underscore"
-], (m)->
- $(document).ready ->
-
- tableRowTemplate = '''
-
- |
- {{ email }} |
- {{ first_name }} {{ last_name }} |
- {{ !holdingAccount }} |
-
-
- |
-
- '''
-
- window.temp = tableRowTemplate
-
- $form = $('form#addUserToGroup')
-
- addUser = (e)->
-
- parseEmails = (emailsString)->
- regexBySpaceOrComma = /[\s,]+/
- emails = emailsString.split(regexBySpaceOrComma)
- emails = _.map emails, (email)->
- email = email.trim()
- emails = _.select emails, (email)->
- email.indexOf("@") != -1
- return emails
-
-
- sendNewUserToServer = (email)->
- $.ajax
- url: "/subscription/group/user"
- type: 'POST'
- data:
- email: email
- _csrf: csrfToken
- success: (data)->
- if data.limitReached
- alert("You have reached your maximum number of members")
- else
- renderNewUserInList data.user
-
- renderNewUserInList = (user)->
- html = Mustache.to_html(tableRowTemplate, user)
- $('#userList').append(html)
-
- e.preventDefault()
- val = $form.find("input[name=email]").val()
- emails = parseEmails(val)
- emails.forEach (email)->
- sendNewUserToServer(email)
- $form.find("input").val('')
-
- removeUsers = (e)->
- selectedUserRows = $('td input.select-one:checked').closest('tr').find(".user_id").toArray()
- do deleteNext = () ->
- row = selectedUserRows.pop()
- if row?
- user_id = $(row).val()
- $.ajax
- url: "/subscription/group/user/#{user_id}"
- type: 'DELETE'
- data:
- _csrf: csrfToken
- success: ->
- $(row).parents("tr").fadeOut(250)
- deleteNext()
-
- $form.on 'keypress', (e)->
- if(e.keyCode == 13)
- addUser(e)
-
- $form.find(".addUser").on 'click', addUser
-
- $('#deleteUsers').on 'click', removeUsers
-
- $('input.select-all').on "change", () ->
- if $(@).is(":checked")
- $("input.select-one").prop( "checked", true )
- else
- $("input.select-one").prop( "checked", false )
-
diff --git a/services/web/public/coffee/account/AccountManager.coffee b/services/web/public/coffee/account/AccountManager.coffee
deleted file mode 100644
index 5329d36de3..0000000000
--- a/services/web/public/coffee/account/AccountManager.coffee
+++ /dev/null
@@ -1,70 +0,0 @@
-define [
- "utils/Modal"
-], (Modal) ->
-
-
-
- AccountManager =
- askToUpgrade: (ide, options = {}) ->
- options.why ||= "to use this feature"
- if ide.project.get("owner") == ide.user
- if ide.user.get("subscription").freeTrial.allowed
- @showCreditCardFreeTrialModal(options)
- else
- @showUpgradeDialog(ide, options)
- else
- @showAskOwnerDialog(ide, options)
-
- showCreditCardFreeTrialModal: (options) ->
- Modal.createModal
- title: "Start your free trial"
- message: "You need to upgrade your account #{options.why}. Would you like to start a 30 day free trial? You can cancel at any point."
- buttons: [{
- text: "Cancel"
- class: ""
- },{
- text: "Enter Billing Information"
- class: "btn-primary"
- callback: () =>
- options.onUpgrade?()
- @gotoSubscriptionsPage()
- }]
-
- gotoSubscriptionsPage: () ->
- window.open("/user/subscription/new?planCode=student_free_trial")
- Modal.createModal
- title: "Please refresh"
- message: "Please refresh this page after starting your free trial. This will make sure all of your features are enabled."
- buttons: [{
- text: "OK"
- class: ""
- }]
-
- showUpgradeDialog: (ide, options = {}) ->
- options.message ||= """
- Sorry, you need to upgrade your account #{options.why}.
- You can do this on your account settings page,
- accessible in the top right hand corner.
- """
- Modal.createModal
- title: "Please upgrade your account"
- message: options.message
- buttons: [{
- text: "OK"
- class: "btn"
- callback: () -> options.onCancel() if options.onCancel
- },{
- text: "See Plans"
- class: "btn-success"
- callback: () -> window.open("/user/subscription/plans")
- }]
-
- showAskOwnerDialog: (ide, options = {}) ->
- Modal.createModal
- title: "Owner needs an upgraded account"
- message: "Please ask the owner of this project to upgrade their account #{options.why}."
- buttons: [{
- text: "OK"
- class: "btn-primary"
- callback: () -> options.onCancel() if options.onCancel
- }]
diff --git a/services/web/public/coffee/admin.coffee b/services/web/public/coffee/admin.coffee
deleted file mode 100644
index 364d179f9c..0000000000
--- a/services/web/public/coffee/admin.coffee
+++ /dev/null
@@ -1,33 +0,0 @@
-require [
- "main"
- "libs/jquery.tablesorter"
-], ()->
- $(document).ready ()->
- $('#connected-users').tablesorter()
-
- $('button#disconnectAll').click (event)->
- event.preventDefault()
- $.ajax
- url: "/admin/dissconectAllUsers",
- type:'POST',
- data:
- _csrf: $(@).data("csrf")
- success: (data)->
-
- $('button#closeEditor').click (event)->
- event.preventDefault()
- $.ajax
- url: "/admin/closeEditor",
- type:'POST',
- data:
- _csrf: $(@).data("csrf")
- success: (data)->
-
- $('button#pollTpds').click (event)->
- event.preventDefault()
- $.ajax
- url: "/admin/pollUsersWithDropbox",
- type:'POST',
- data:
- _csrf: $(@).data("csrf")
- success: (data)->
diff --git a/services/web/public/coffee/analytics/AnalyticsManager.coffee b/services/web/public/coffee/analytics/AnalyticsManager.coffee
deleted file mode 100644
index e06cf264c3..0000000000
--- a/services/web/public/coffee/analytics/AnalyticsManager.coffee
+++ /dev/null
@@ -1,33 +0,0 @@
-define [
- "libs/md5"
-], () ->
- class AnalyticsManager
- constructor: (@ide) ->
- @ide.editor.on "update:doc", () =>
- @updateCount ||= 0
- @updateCount++
- if @updateCount == 100
- ga('send', 'event', 'editor-interaction', 'multi-doc-update')
-
- @ide.pdfManager.on "compile:pdf", () =>
- @compileCount ||= 0
- @compileCount++
- if @compileCount == 1
- ga('send', 'event', 'editor-interaction', 'single-compile')
- if @compileCount == 3
- ga('send', 'event', 'editor-interaction', 'multi-compile')
-
- getABTestBucket: (test_name, buckets = []) ->
- hash = CryptoJS.MD5("#{@ide.user.get("id")}:#{test_name}")
- bucketIndex = parseInt(hash.toString().slice(0,2), 16) % buckets.length
- return buckets[bucketIndex]
-
- startABTest: (test_name, buckets = []) ->
- value = @getABTestBucket(test_name, buckets)
- ga('send', 'event', 'ab_tests', test_name, "viewed-#{value}")
- return value
-
- endABTest: (test_name, buckets = []) ->
- value = @getABTestBucket(test_name, buckets)
- ga('send', 'event', 'ab_tests', test_name, "converted-#{value}")
- return value
\ No newline at end of file
diff --git a/services/web/public/coffee/app/base.coffee b/services/web/public/coffee/app/base.coffee
deleted file mode 100644
index 704b6c9285..0000000000
--- a/services/web/public/coffee/app/base.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-define [
- "../libs/angular-autocomplete/angular-autocomplete"
- "../libs/ui-bootstrap"
- "modules/recursionHelper"
- "../libs/ng-context-menu-0.1.4"
-], () ->
- App = angular.module("SharelatexApp", [
- "ui.bootstrap"
- "autocomplete"
- "RecursionHelper"
- "ng-context-menu"
- ])
-
- return App
\ No newline at end of file
diff --git a/services/web/public/coffee/app/directives/asyncForm.coffee b/services/web/public/coffee/app/directives/asyncForm.coffee
deleted file mode 100644
index 72702cf347..0000000000
--- a/services/web/public/coffee/app/directives/asyncForm.coffee
+++ /dev/null
@@ -1,63 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive "asyncForm", ($http) ->
- return {
- link: (scope, element, attrs) ->
- formName = attrs.asyncForm
-
- scope[attrs.name].response = response = {}
-
- element.on "submit", (e) ->
- e.preventDefault()
-
- formData = {}
- for data in element.serializeArray()
- formData[data.name] = data.value
-
- $http
- .post(element.attr('action'), formData)
- .success (data, status, headers, config) ->
- response.success = true
- response.error = false
-
- if data.redir?
- ga('send', 'event', formName, 'success')
- window.location = data.redir
- else if data.message?
- response.message = data.message
-
- if data.message.type == "error"
- response.success = false
- response.error = true
- ga('send', 'event', formName, 'failure', data.message)
- else
- ga('send', 'event', formName, 'success')
-
- .error (data, status, headers, config) ->
- response.success = false
- response.error = true
- response.message =
- text: data.message or "Something went wrong talking to the server :(. Please try again."
- type: 'error'
- ga('send', 'event', formName, 'failure', data.message)
- }
-
- App.directive "formMessages", () ->
- return {
- restrict: "E"
- template: """
-
- {{form.response.message.text}}
-
-
- """
- transclude: true
- scope: {
- form: "=for"
- }
-
- }
\ No newline at end of file
diff --git a/services/web/public/coffee/app/directives/equals.coffee b/services/web/public/coffee/app/directives/equals.coffee
deleted file mode 100644
index e41a39a322..0000000000
--- a/services/web/public/coffee/app/directives/equals.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-define [
- "base"
-], (App) ->
-
- App.directive 'equals', () ->
- return {
- require: "ngModel",
- link: (scope, element, attrs, ngModel) ->
- scope.$watch attrs.ngModel, () -> validate()
- attrs.$observe 'equals', () -> validate()
-
- validate = () ->
- equal = (attrs.equals == ngModel.$viewValue)
- ngModel.$setValidity('areEqual', equal)
- }
diff --git a/services/web/public/coffee/app/directives/fineUpload.coffee b/services/web/public/coffee/app/directives/fineUpload.coffee
deleted file mode 100644
index b7cb5ffc3b..0000000000
--- a/services/web/public/coffee/app/directives/fineUpload.coffee
+++ /dev/null
@@ -1,67 +0,0 @@
-define [
- "base"
- "../../libs/fineuploader"
-], (App) ->
- App.directive 'fineUpload', ($timeout) ->
- return {
- scope: {
- multiple: "="
- endpoint: "@"
- waitingForResponseText: "@"
- failedUploadText: "@"
- uploadButtonText: "@"
- dragAreaText: "@"
- hintText: "@"
- allowedExtensions: "="
- onCompleteCallback: "="
- onUploadCallback: "="
- params: "="
- }
- link: (scope, element, attrs) ->
- multiple = scope.multiple or false
- endpoint = scope.endpoint
- if scope.allowedExtensions?
- validation =
- allowedExtensions: scope.allowedExtensions
- else
- validation = {}
- text =
- waitingForResponse: scope.waitingForResponseText or "Processing..."
- failUpload: scope.failedUploadText or "Failed :("
- uploadButton: scope.uploadButtonText or "Upload"
- dragAreaText = scope.dragAreaText or "drag here"
- hintText = scope.hintText or ""
-
- onComplete = scope.onCompleteCallback or () ->
- onUpload = scope.onUploadCallback or () ->
- params = scope.params or {}
- params._csrf = window.csrfToken
-
- new qq.FineUploader
- element: element[0]
- multiple: multiple
- disabledCancelForFormUploads: true
- validation: validation
- request:
- endpoint: endpoint
- forceMultipart: true
- params: params
- paramsInBody: false
- callbacks:
- onComplete: onComplete
- onUpload: onUpload
- text: text
- template: """
-
-
{dragZoneText}
-
-
or
-
#{dragAreaText}
-
{dropProcessingText}
-
#{hintText}
-
-
- """
- }
\ No newline at end of file
diff --git a/services/web/public/coffee/app/directives/focus.coffee b/services/web/public/coffee/app/directives/focus.coffee
deleted file mode 100644
index db742f155f..0000000000
--- a/services/web/public/coffee/app/directives/focus.coffee
+++ /dev/null
@@ -1,67 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive "focusWhen", ($timeout) ->
- return {
- restrict: "A"
- link: (scope, element, attr) ->
- scope.$watch attr.focusWhen, (value) ->
- if value
- $timeout ->
- element.focus()
- }
-
- App.directive 'focusOn', ($timeout) ->
- return {
- restrict: 'A'
- link: (scope, element, attrs) ->
- scope.$on attrs.focusOn, () ->
- element.focus()
- }
-
- App.directive "selectWhen", ($timeout) ->
- return {
- restrict: "A"
- link: (scope, element, attr) ->
- scope.$watch attr.selectWhen, (value) ->
- if value
- $timeout ->
- element.select()
- }
-
- App.directive 'selectOn', ($timeout) ->
- return {
- restrict: 'A'
- link: (scope, element, attrs) ->
- scope.$on attrs.selectOn, () ->
- element.select()
- }
-
- App.directive "selectNameWhen", ($timeout) ->
- return {
- restrict: 'A'
- link: (scope, element, attrs) ->
- scope.$watch attrs.selectNameWhen, (value) ->
- if value
- $timeout () ->
- selectName(element)
- }
-
- App.directive "selectNameOn", () ->
- return {
- restrict: 'A'
- link: (scope, element, attrs) ->
- scope.$on attrs.selectNameOn, () ->
- selectName(element)
- }
-
- selectName = (element) ->
- # Select up to last '.'. I.e. everything
- # except the file extension
- element.focus()
- name = element.val()
- if element[0].setSelectionRange?
- selectionEnd = name.lastIndexOf(".")
- if selectionEnd == -1
- selectionEnd = name.length
- element[0].setSelectionRange(0, selectionEnd)
diff --git a/services/web/public/coffee/app/directives/onEnter.coffee b/services/web/public/coffee/app/directives/onEnter.coffee
deleted file mode 100644
index 459d444b60..0000000000
--- a/services/web/public/coffee/app/directives/onEnter.coffee
+++ /dev/null
@@ -1,10 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive 'onEnter', () ->
- return (scope, element, attrs) ->
- element.bind "keydown keypress", (event) ->
- if event.which == 13
- scope.$apply () ->
- scope.$eval(attrs.onEnter, event: event)
- event.preventDefault()
\ No newline at end of file
diff --git a/services/web/public/coffee/app/directives/selectAll.coffee b/services/web/public/coffee/app/directives/selectAll.coffee
deleted file mode 100644
index 79b683c20a..0000000000
--- a/services/web/public/coffee/app/directives/selectAll.coffee
+++ /dev/null
@@ -1,67 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive "selectAllList", () ->
- return {
- controller: ["$scope", ($scope) ->
- # Selecting or deselecting all should apply to all projects
- selectAll = () ->
- $scope.$broadcast "select-all:select"
-
- deselectAll = () ->
- $scope.$broadcast "select-all:deselect"
-
- clearSelectAllState = () ->
- $scope.$broadcast "select-all:clear"
-
- return {
- clearSelectAllState: clearSelectAllState
- selectAll: selectAll
- deselectAll: deselectAll
- }
- ]
- link: (scope, element, attrs) ->
-
-
- }
-
- App.directive "selectAll", () ->
- return {
- require: "^selectAllList"
- link: (scope, element, attrs, selectAllListController) ->
- scope.$on "select-all:clear", () ->
- element.prop("checked", false)
-
- element.change () ->
- if element.is(":checked")
- selectAllListController.selectAll()
- else
- selectAllListController.deselectAll()
- return true
- }
-
- App.directive "selectIndividual", () ->
- return {
- require: "^selectAllList"
- scope: {
- ngModel: "="
- }
- link: (scope, element, attrs, selectAllListController) ->
- ignoreChanges = false
-
- scope.$watch "ngModel", (value) ->
- if value? and !ignoreChanges
- selectAllListController.clearSelectAllState()
-
- scope.$on "select-all:select", () ->
- ignoreChanges = true
- scope.$apply () ->
- scope.ngModel = true
- ignoreChanges = false
-
- scope.$on "select-all:deselect", () ->
- ignoreChanges = true
- scope.$apply () ->
- scope.ngModel = false
- ignoreChanges = false
- }
\ No newline at end of file
diff --git a/services/web/public/coffee/app/directives/stopPropagation.coffee b/services/web/public/coffee/app/directives/stopPropagation.coffee
deleted file mode 100644
index 8cd3b25c0a..0000000000
--- a/services/web/public/coffee/app/directives/stopPropagation.coffee
+++ /dev/null
@@ -1,18 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive "stopPropagation", ($http) ->
- return {
- restrict: "A",
- link: (scope, element, attrs) ->
- element.bind attrs.stopPropagation, (e) ->
- e.stopPropagation()
- }
-
- App.directive "preventDefault", ($http) ->
- return {
- restrict: "A",
- link: (scope, element, attrs) ->
- element.bind attrs.preventDefault, (e) ->
- e.preventDefault()
- }
diff --git a/services/web/public/coffee/app/filters/formatDate.coffee b/services/web/public/coffee/app/filters/formatDate.coffee
deleted file mode 100644
index c8cef08dbd..0000000000
--- a/services/web/public/coffee/app/filters/formatDate.coffee
+++ /dev/null
@@ -1,18 +0,0 @@
-define [
- "base"
-], (App) ->
- moment.lang "en", calendar:
- lastDay : '[Yesterday]'
- sameDay : '[Today]'
- nextDay : '[Tomorrow]'
- lastWeek : "ddd, Do MMM YY"
- nextWeek : "ddd, Do MMM YY"
- sameElse : 'ddd, Do MMM YY'
-
- App.filter "formatDate", () ->
- (date, format = "Do MMM YYYY, h:mm a") ->
- moment(date).format(format)
-
- App.filter "relativeDate", () ->
- (date) ->
- moment(date).calendar()
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide.coffee b/services/web/public/coffee/app/ide.coffee
deleted file mode 100644
index 393b77219c..0000000000
--- a/services/web/public/coffee/app/ide.coffee
+++ /dev/null
@@ -1,70 +0,0 @@
-define [
- "base"
- "ide/file-tree/FileTreeManager"
- "ide/connection/ConnectionManager"
- "ide/editor/EditorManager"
- "ide/online-users/OnlineUsersManager"
- "ide/track-changes/TrackChangesManager"
- "ide/permissions/PermissionsManager"
- "ide/pdf/PdfManager"
- "ide/binary-files/BinaryFilesManager"
- "ide/settings/index"
- "ide/share/index"
- "ide/chat/index"
- "ide/directives/layout"
- "ide/services/ide"
- "directives/focus"
- "directives/fineUpload"
- "directives/onEnter"
- "filters/formatDate"
-], (
- App
- FileTreeManager
- ConnectionManager
- EditorManager
- OnlineUsersManager
- TrackChangesManager
- PermissionsManager
- PdfManager
- BinaryFilesManager
-) ->
- App.controller "IdeController", ["$scope", "$timeout", "ide", ($scope, $timeout, ide) ->
- # Don't freak out if we're already in an apply callback
- $scope.$originalApply = $scope.$apply
- $scope.$apply = (fn = () ->) ->
- phase = @$root.$$phase
- if (phase == '$apply' || phase == '$digest')
- fn()
- else
- this.$originalApply(fn);
-
- $scope.state = {
- loading: true
- load_progress: 40
- }
- $scope.ui = {
- leftMenuShown: false
- view: "editor"
- chatOpen: false
- }
- $scope.user = window.user
- $scope.settings = window.userSettings
-
- $scope.chat = {}
-
- window._ide = ide
-
- ide.project_id = $scope.project_id = window.project_id
- ide.$scope = $scope
-
- ide.connectionManager = new ConnectionManager(ide, $scope)
- 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.pdfManager = new PdfManager(ide, $scope)
- ide.permissionsManager = new PermissionsManager(ide, $scope)
- ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
- ]
-
- angular.bootstrap(document.body, ["SharelatexApp"])
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/binary-files/BinaryFilesManager.coffee b/services/web/public/coffee/app/ide/binary-files/BinaryFilesManager.coffee
deleted file mode 100644
index 9b80c0e0dc..0000000000
--- a/services/web/public/coffee/app/ide/binary-files/BinaryFilesManager.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-define [
- "ide/binary-files/controllers/BinaryFileController"
-], () ->
- class BinaryFilesManager
- constructor: (@ide, @$scope) ->
- @$scope.$on "entity:selected", (event, entity) =>
- if (@$scope.ui.view != "track-changes" and entity.type == "file")
- @openFile(entity)
-
- openFile: (file) ->
- @$scope.ui.view = "file"
- @$scope.openFile = file
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/app/ide/binary-files/controllers/BinaryFileController.coffee
deleted file mode 100644
index 5dcabe8000..0000000000
--- a/services/web/public/coffee/app/ide/binary-files/controllers/BinaryFileController.coffee
+++ /dev/null
@@ -1,7 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "BinaryFileController", ["$scope", ($scope) ->
- $scope.extension = (file) ->
- return file.name.split(".").pop()?.toLowerCase()
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/chat/controllers/ChatButtonController.coffee b/services/web/public/coffee/app/ide/chat/controllers/ChatButtonController.coffee
deleted file mode 100644
index 74e6d31ee3..0000000000
--- a/services/web/public/coffee/app/ide/chat/controllers/ChatButtonController.coffee
+++ /dev/null
@@ -1,7 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "ChatButtonController", ["$scope", ($scope) ->
- $scope.toggleChat = () ->
- $scope.ui.chatOpen = !$scope.ui.chatOpen
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/chat/controllers/ChatController.coffee b/services/web/public/coffee/app/ide/chat/controllers/ChatController.coffee
deleted file mode 100644
index a5c42dc435..0000000000
--- a/services/web/public/coffee/app/ide/chat/controllers/ChatController.coffee
+++ /dev/null
@@ -1,64 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "ChatController", ["$scope", ($scope) ->
- james = {
- id: $scope.user.id
- first_name: "James"
- last_name: "Allen"
- email: "james@sharelatex.com"
- }
- james2 = {
- id: "james2-id"
- first_name: "James"
- last_name: "Allen"
- email: "jamesallen0108@gmail.com"
- }
- henry = {
- id: "henry-id"
- first_name: "Henry"
- last_name: "Oswald"
- email: "henry.oswald@sharelatex.com"
- }
- $scope.chat.messages = [
- { content: "Hello world", timestamp: Date.now() - 2000, user: james2 }
- { content: "Hello, this is the new chat", timestamp: Date.now() - 4000, user: james }
- { content: "Here are some longer messages to show what it looks like when I say a lot!", timestamp: Date.now() - 20000, user: henry }
- { content: "What about some maths? $x^2 = 1$?", timestamp: Date.now() - 22000, user: james2 }
- { content: "Nope, that doesn't work yet!", timestamp: Date.now() - 45000, user: henry }
- { content: "I'm running out of things to say.", timestamp: Date.now() - 56000, user: henry }
- { content: "Yep, me too", timestamp: Date.now() - 100000, user: james }
- { content: "Hmm, looks like we've had this conversation backwards", timestamp: Date.now() - 120000, user: james }
- { content: "Hello world", timestamp: Date.now() - 202000, user: james2 }
- { content: "Hello, this is the new chat", timestamp: Date.now() - 204000, user: james }
- { content: "Here are some longer messages to show what it looks like when I say a lot!", timestamp: Date.now() - 2020000, user: henry }
- { content: "What about some maths? $x^2 = 1$?", timestamp: Date.now() - 12022000, user: james2 }
- { content: "Nope, that doesn't work yet!", timestamp: Date.now() - 12045000, user: henry }
- { content: "I'm running out of things to say.", timestamp: Date.now() - 22056000, user: henry }
- { content: "Yep, me too", timestamp: Date.now() - 220100000, user: james }
- { content: "Hmm, looks like we've had this conversation backwards", timestamp: Date.now() - 520120000, user: james }
- ].reverse()
-
- $scope.$watch "chat.messages", (messages) ->
- if messages?
- $scope.chat.groupedMessages = groupMessages(messages)
-
- TIMESTAMP_GROUP_SIZE = 5 * 60 * 1000 # 5 minutes
- groupMessages = (messages) ->
- previousMessage = null
- groupedMessages = []
- for message in messages
- shouldGroup = previousMessage? and
- previousMessage.user == message.user and
- message.timestamp - previousMessage.timestamp < TIMESTAMP_GROUP_SIZE
- if shouldGroup
- previousMessage.timestamp = message.timestamp
- previousMessage.contents.push message.content
- else
- groupedMessages.push(previousMessage = {
- user: message.user
- timestamp: message.timestamp
- contents: [message.content]
- })
- return groupedMessages
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/chat/controllers/ChatMessageController.coffee b/services/web/public/coffee/app/ide/chat/controllers/ChatMessageController.coffee
deleted file mode 100644
index 9e337cbc10..0000000000
--- a/services/web/public/coffee/app/ide/chat/controllers/ChatMessageController.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "ChatMessageController", ["$scope", "ide", ($scope, ide) ->
- $scope.gravatarUrl = (user) ->
- email = user.email.trim().toLowerCase()
- hash = CryptoJS.MD5(email).toString()
- return "//www.gravatar.com/avatar/#{hash}?d=mm&s=50"
-
- $scope.hue = (user) ->
- ide.onlineUsersManager.getHueForUserId(user.id)
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/chat/index.coffee b/services/web/public/coffee/app/ide/chat/index.coffee
deleted file mode 100644
index f60b41d1fb..0000000000
--- a/services/web/public/coffee/app/ide/chat/index.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-define [
- "ide/chat/controllers/ChatButtonController"
- "ide/chat/controllers/ChatController"
- "ide/chat/controllers/ChatMessageController"
-], () ->
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/connection/ConnectionManager.coffee b/services/web/public/coffee/app/ide/connection/ConnectionManager.coffee
deleted file mode 100644
index 9d5f29983a..0000000000
--- a/services/web/public/coffee/app/ide/connection/ConnectionManager.coffee
+++ /dev/null
@@ -1,113 +0,0 @@
-define [], () ->
- class ConnectionManager
- constructor: (@ide, @$scope) ->
- @connected = false
-
- @$scope.connection =
- reconnecting: false
- # If we need to force everyone to reload the editor
- forced_disconnect: false
-
- @$scope.tryReconnectNow = () =>
- @tryReconnect()
-
- @ide.socket = io.connect null,
- reconnect: false
- "force new connection": true
-
- @ide.socket.on "connect", () =>
- @connected = true
- @ide.pushEvent("connected")
-
- @$scope.$apply () =>
- @$scope.connection.reconnecting = false
- if @$scope.state.loading
- @$scope.state.load_progress = 80
-
- setTimeout(() =>
- @joinProject()
- , 100)
-
- @ide.socket.on 'disconnect', () =>
- @connected = false
- @ide.pushEvent("disconnected")
-
- @$scope.$apply () =>
- @$scope.connection.reconnecting = false
-
- setTimeout(=>
- ga('send', 'event', 'editor-interaction', 'disconnect')
- , 2000)
-
- if !$scope.connection.forced_disconnect
- @startAutoReconnectCountdown()
-
- @ide.socket.on 'forceDisconnect', (message) =>
- @$scope.$apply () =>
- @$scope.connection.forced_disconnect = true
- @socket.disconnect()
-
- joinProject: () ->
- @ide.socket.emit 'joinProject', {
- project_id: @ide.project_id
- }, (err, project, permissionsLevel, protocolVersion) =>
- if @$scope.protocolVersion? and @$scope.protocolVersion != protocolVersion
- location.reload(true)
-
- @$scope.$apply () =>
- @$scope.protocolVersion = protocolVersion
- @$scope.project = project
- @$scope.permissionsLevel = permissionsLevel
- @$scope.state.load_progress = 100
- @$scope.state.loading = false
- @$scope.$emit "project:joined"
-
- reconnectImmediately: () ->
- @disconnect()
- @tryReconnect()
-
- disconnect: () ->
- @ide.socket.disconnect()
-
- startAutoReconnectCountdown: () ->
- lastUpdated = @ide.editorManager.lastUpdated()
-
- twoMinutes = 2 * 60 * 1000
- if lastUpdated? and new Date() - lastUpdated > twoMinutes
- # between 1 minute and 3 minutes
- countdown = 60 + Math.floor(Math.random() * 120)
- else
- countdown = 3 + Math.floor(Math.random() * 7)
-
- @$scope.$apply () =>
- @$scope.connection.reconnecting = false
- @$scope.connection.reconnection_countdown = countdown
-
- setTimeout(=>
- if !@connected
- @timeoutId = setTimeout (=> @decreaseCountdown()), 1000
- , 200)
-
- cancelReconnect: () ->
- clearTimeout @timeoutId if @timeoutId?
-
- decreaseCountdown: () ->
- console.log "Decreasing countdown"
- return if !@$scope.connection.reconnection_countdown?
- @$scope.$apply () =>
- @$scope.connection.reconnection_countdown--
-
- if @$scope.connection.reconnection_countdown <= 0
- @$scope.$apply () =>
- @tryReconnect()
- else
- @timeoutId = setTimeout (=> @decreaseCountdown()), 1000
-
- tryReconnect: () ->
- console.log "Trying reconnect"
- @cancelReconnect()
- @$scope.connection.reconnecting = true
- delete @$scope.connection.reconnection_countdown
- @ide.socket.socket.reconnect()
- setTimeout (=> @startAutoReconnectCountdown() if !@connected), 2000
-
diff --git a/services/web/public/coffee/app/ide/directives/layout.coffee b/services/web/public/coffee/app/ide/directives/layout.coffee
deleted file mode 100644
index af746f30a7..0000000000
--- a/services/web/public/coffee/app/ide/directives/layout.coffee
+++ /dev/null
@@ -1,101 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive "layout", ["$parse", ($parse) ->
- return {
- compile: () ->
- pre: (scope, element, attrs) ->
- name = attrs.layout
-
- if attrs.spacingOpen?
- spacingOpen = parseInt(attrs.spacingOpen, 10)
- else
- spacingOpen = 24
-
- if attrs.spacingClosed?
- spacingClosed = parseInt(attrs.spacingClosed, 10)
- else
- spacingClosed = 24
-
- options =
- spacing_open: spacingOpen
- spacing_closed: spacingClosed
- slidable: false
- onresize: () =>
- onInternalResize()
- maskIframesOnResize: scope.$eval(
- attrs.maskIframesOnResize or "false"
- )
- east:
- size: scope.$eval(attrs.initialSizeEast)
- initClosed: scope.$eval(attrs.initClosedEast)
- west:
- size: scope.$eval(attrs.initialSizeEast)
- initClosed: scope.$eval(attrs.initClosedWest)
-
- # Restore previously recorded state
- if (state = $.localStorage("layout.#{name}"))?
- options.west = state.west
- options.east = state.east
-
- # Someone moved the resizer
- onInternalResize = () ->
- state = element.layout().readState()
- scope.$broadcast "layout:#{name}:resize", state
- repositionControls()
- resetOpenStates()
-
- oldWidth = element.width()
- # Something resized our parent element
- onExternalResize = () ->
- console.log "EXTERNAL RESIOZE", name, attrs.resizeProportionally
- if attrs.resizeProportionally? and scope.$eval(attrs.resizeProportionally)
- eastState = element.layout().readState().east
- if eastState?
- newInternalWidth = eastState.size / oldWidth * element.width()
- oldWidth = element.width()
- element.layout().sizePane("east", newInternalWidth)
- return
-
- element.layout().resizeAll()
-
- element.layout options
- element.layout().resizeAll()
-
- if attrs.resizeOn?
- scope.$on attrs.resizeOn, () -> onExternalResize()
-
- # Save state when exiting
- $(window).unload () ->
- $.localStorage("layout.#{name}", element.layout().readState())
-
- repositionControls = () ->
- state = element.layout().readState()
- if state.east?
- element.find("> .ui-layout-resizer-controls").css({
- position: "absolute"
- right: state.east.size
- "z-index": 10
- })
-
- resetOpenStates = () ->
- state = element.layout().readState()
- if attrs.openEast?
- openEast = $parse(attrs.openEast)
- openEast.assign(scope, !state.east.initClosed)
-
- if attrs.openEast?
- scope.$watch attrs.openEast, (value, oldValue) ->
- console.log "Open East", value, oldValue
- if value? and value != oldValue
- if value
- element.layout().open("east")
- else
- element.layout().close("east")
- setTimeout () ->
- scope.$digest()
- , 0
-
- resetOpenStates()
- }
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/editor/Document.coffee b/services/web/public/coffee/app/ide/editor/Document.coffee
deleted file mode 100644
index 4b7347b520..0000000000
--- a/services/web/public/coffee/app/ide/editor/Document.coffee
+++ /dev/null
@@ -1,254 +0,0 @@
-define [
- "utils/EventEmitter"
- "ide/editor/ShareJsDoc"
- "underscore"
-], (EventEmitter, ShareJsDoc) ->
- class Document extends EventEmitter
- @getDocument: (ide, doc_id) ->
- @openDocs ||= {}
- if !@openDocs[doc_id]?
- @openDocs[doc_id] = new Document(ide, doc_id)
- return @openDocs[doc_id]
-
- @hasUnsavedChanges: () ->
- for doc_id, doc of (@openDocs or {})
- return true if doc.hasBufferedOps()
- return false
-
- constructor: (@ide, @doc_id) ->
- @connected = @ide.socket.socket.connected
- @joined = false
- @wantToBeJoined = false
- @_checkConsistency = _.bind(@_checkConsistency, @)
- @inconsistentCount = 0
- @_bindToEditorEvents()
- @_bindToSocketEvents()
-
- attachToAce: (@ace) ->
- @doc?.attachToAce(@ace)
- editorDoc = @ace.getSession().getDocument()
- editorDoc.on "change", @_checkConsistency
-
- detachFromAce: () ->
- @doc?.detachFromAce()
- editorDoc = @ace?.getSession().getDocument()
- editorDoc?.off "change", @_checkConsistency
-
- _checkConsistency: () ->
- # We've been seeing a lot of errors when I think there shouldn't be
- # any, which may be related to this check happening before the change is
- # applied. If we use a timeout, hopefully we can reduce this.
- setTimeout () =>
- editorValue = @ace?.getValue()
- sharejsValue = @doc?.getSnapshot()
- if editorValue != sharejsValue
- @inconsistentCount++
- else
- @inconsistentCount = 0
-
- if @inconsistentCount >= 3
- @_onError new Error("Editor text does not match server text")
- , 0
-
- getSnapshot: () ->
- @doc?.getSnapshot()
-
- getType: () ->
- @doc?.getType()
-
- getInflightOp: () ->
- @doc?.getInflightOp()
-
- getPendingOp: () ->
- @doc?.getPendingOp()
-
- hasBufferedOps: () ->
- @doc?.hasBufferedOps()
-
- _bindToSocketEvents: () ->
- @_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update)
- @ide.socket.on "otUpdateApplied", @_onUpdateAppliedHandler
- @_onErrorHandler = (error, update) => @_onError(error, update)
- @ide.socket.on "otUpdateError", @_onErrorHandler
- @_onDisconnectHandler = (error) => @_onDisconnect(error)
- @ide.socket.on "disconnect", @_onDisconnectHandler
-
- _bindToEditorEvents: () ->
- onReconnectHandler = (update) =>
- @_onReconnect(update)
- @_unsubscribeReconnectHandler = @ide.$scope.$on "project:joined", onReconnectHandler
-
- _unBindFromEditorEvents: () ->
- @_unsubscribeReconnectHandler()
-
- _unBindFromSocketEvents: () ->
- @ide.socket.removeListener "otUpdateApplied", @_onUpdateAppliedHandler
- @ide.socket.removeListener "otUpdateError", @_onUpdateErrorHandler
- @ide.socket.removeListener "disconnect", @_onDisconnectHandler
-
- leaveAndCleanUp: () ->
- @leave (error) =>
- @_cleanUp()
-
- join: (callback = (error) ->) ->
- @wantToBeJoined = true
- @_cancelLeave()
- if @connected
- return @_joinDoc callback
- else
- @_joinCallbacks ||= []
- @_joinCallbacks.push callback
-
- leave: (callback = (error) ->) ->
- @wantToBeJoined = false
- @_cancelJoin()
- if (@doc? and @doc.hasBufferedOps())
- @_leaveCallbacks ||= []
- @_leaveCallbacks.push callback
- else if !@connected
- callback()
- else
- @_leaveDoc(callback)
-
- pollSavedStatus: () ->
- # returns false if doc has ops waiting to be acknowledged or
- # sent that haven't changed since the last time we checked.
- # Otherwise returns true.
- inflightOp = @getInflightOp()
- pendingOp = @getPendingOp()
- if !inflightOp? and !pendingOp?
- # there's nothing going on
- saved = true
- else if inflightOp == @oldInflightOp
- saved = false
- else if pendingOp?
- saved = false
- else
- saved = true
-
- @oldInflightOp = inflightOp
- return saved
-
- _cancelLeave: () ->
- if @_leaveCallbacks?
- delete @_leaveCallbacks
-
- _cancelJoin: () ->
- if @_joinCallbacks?
- delete @_joinCallbacks
-
- _onUpdateApplied: (update) ->
- @ide.pushEvent "received-update",
- doc_id: @doc_id
- remote_doc_id: update?.doc
- wantToBeJoined: @wantToBeJoined
- update: update
-
- if Math.random() < (@ide.disconnectRate or 0)
- console.log "Simulating disconnect"
- @ide.connectionManager.disconnect()
- return
-
- if Math.random() < (@ide.ignoreRate or 0)
- console.log "Simulating lost update"
- return
-
- if update?.doc == @doc_id and @doc?
- @doc.processUpdateFromServer update
-
- if !@wantToBeJoined
- @leave()
-
- _onDisconnect: () ->
- @connected = false
- @joined = false
- @doc?.updateConnectionState "disconnected"
-
- _onReconnect: () ->
- @ide.pushEvent "reconnected:afterJoinProject"
-
- @connected = true
- if @wantToBeJoined or @doc?.hasBufferedOps()
- @_joinDoc (error) =>
- return @_onError(error) if error?
- @doc.updateConnectionState "ok"
- @doc.flushPendingOps()
- @_callJoinCallbacks()
-
- _callJoinCallbacks: () ->
- for callback in @_joinCallbacks or []
- callback()
- delete @_joinCallbacks
-
- _joinDoc: (callback = (error) ->) ->
- if @doc?
- @ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), (error, docLines, version, updates) =>
- return callback(error) if error?
- @joined = true
- @doc.catchUp( updates )
- callback()
- else
- @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version) =>
- return callback(error) if error?
- @joined = true
- @doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket
- @_bindToShareJsDocEvents()
- callback()
-
- _leaveDoc: (callback = (error) ->) ->
- @ide.socket.emit 'leaveDoc', @doc_id, (error) =>
- return callback(error) if error?
- @joined = false
- for callback in @_leaveCallbacks or []
- callback(error)
- delete @_leaveCallbacks
- callback(error)
-
- _cleanUp: () ->
- delete Document.openDocs[@doc_id]
- @_unBindFromEditorEvents()
- @_unBindFromSocketEvents()
-
- _bindToShareJsDocEvents: () ->
- @doc.on "error", (error, meta) => @_onError error, meta
- @doc.on "externalUpdate", () =>
- @ide.pushEvent "externalUpdate",
- doc_id: @doc_id
- @trigger "externalUpdate"
- @doc.on "remoteop", () =>
- @ide.pushEvent "remoteop",
- doc_id: @doc_id
- @trigger "remoteop"
- @doc.on "op:sent", (op) =>
- @ide.pushEvent "op:sent",
- doc_id: @doc_id
- op: op
- @trigger "op:sent"
- @doc.on "op:acknowledged", (op) =>
- @ide.pushEvent "op:acknowledged",
- doc_id: @doc_id
- op: op
- @trigger "op:acknowledged"
- @doc.on "op:timeout", (op) =>
- @ide.pushEvent "op:timeout",
- doc_id: @doc_id
- op: op
- @trigger "op:timeout"
- ga?('send', 'event', 'error', "op timeout", "Op was now acknowledged - #{@ide.socket.socket.transport.name}" )
- @ide.connectionManager.reconnectImmediately()
- @doc.on "flush", (inflightOp, pendingOp, version) =>
- @ide.pushEvent "flush",
- doc_id: @doc_id,
- inflightOp: inflightOp,
- pendingOp: pendingOp
- v: version
-
- _onError: (error, meta = {}) ->
- console.error "ShareJS error", error, meta
- ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{@ide.socket.socket.transport.name}" )
- @ide.socket.disconnect()
- meta.doc_id = @doc_id
- @ide.reportError(error, meta)
- @doc?.clearInflightAndPendingOps()
- @_cleanUp()
- @trigger "error", error
diff --git a/services/web/public/coffee/app/ide/editor/EditorManager.coffee b/services/web/public/coffee/app/ide/editor/EditorManager.coffee
deleted file mode 100644
index 4d0f965d93..0000000000
--- a/services/web/public/coffee/app/ide/editor/EditorManager.coffee
+++ /dev/null
@@ -1,103 +0,0 @@
-define [
- "ide/editor/Document"
- "ide/editor/directives/aceEditor"
- "ide/editor/controllers/SavingNotificationController"
-], (Document) ->
- class EditorManager
- constructor: (@ide, @$scope) ->
- @$scope.editor = {
- sharejs_doc: null
- last_updated: null
- open_doc_id: null
- opening: true
- cursorPosition: {}
- gotoLine: null
- }
-
- @$scope.$on "entity:selected", (event, entity) =>
- if (@$scope.ui.view != "track-changes" and entity.type == "doc")
- @openDoc(entity)
-
- initialized = false
- @$scope.$on "file-tree:initialized", () =>
- if !initialized
- initialized = true
- @autoOpenDoc()
-
- autoOpenDoc: () ->
- open_doc_id =
- $.localStorage("doc.open_id.#{@$scope.project_id}") or
- @$scope.project.rootDoc_id
- return if !open_doc_id?
- doc = @ide.fileTreeManager.findEntityById(open_doc_id)
- return if !doc?
- @openDoc(doc)
-
- openDoc: (doc, options = {}) ->
- @$scope.ui.view = "editor"
-
- done = () =>
- if options.gotoLine?
- @$scope.editor.gotoLine = options.gotoLine
-
- if doc.id == @$scope.editor.open_doc_id and !options.forceReopen
- @$scope.$apply () =>
- done()
- return
-
- @$scope.editor.open_doc_id = doc.id
-
- $.localStorage "doc.open_id.#{@$scope.project_id}", doc.id
- @ide.fileTreeManager.selectEntity(doc)
-
- @$scope.editor.opening = true
- @_openNewDocument doc, (error, sharejs_doc) =>
- if error?
- @ide.showGenericServerErrorMessage()
- return
-
- @$scope.$broadcast "doc:opened"
-
- @$scope.$apply () =>
- @$scope.editor.opening = false
- @$scope.editor.sharejs_doc = sharejs_doc
- done()
-
- _openNewDocument: (doc, callback = (error, sharejs_doc) ->) ->
- current_sharejs_doc = @$scope.editor.sharejs_doc
- if current_sharejs_doc?
- current_sharejs_doc.leaveAndCleanUp()
- @_unbindFromDocumentEvents(current_sharejs_doc)
-
- new_sharejs_doc = Document.getDocument @ide, doc.id
-
- new_sharejs_doc.join (error) =>
- return callback(error) if error?
- @_bindToDocumentEvents(doc, new_sharejs_doc)
- callback null, new_sharejs_doc
-
- _bindToDocumentEvents: (doc, sharejs_doc) ->
- sharejs_doc.on "error", (error) =>
- @openDoc(doc, forceReopen: true)
- @ide.showGenericMessageModal(
- "Out of sync"
- "Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
- )
-
- sharejs_doc.on "externalUpdate", () =>
- @ide.showGenericMessageModal(
- "Document Updated Externally"
- "This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history."
- )
-
- _unbindFromDocumentEvents: (document) ->
- document.off()
-
- lastUpdated: () ->
- @$scope.editor.last_updated
-
- getCurrentDocValue: () ->
- @$scope.editor.sharejs_doc?.getSnapshot()
-
- getCurrentDocId: () ->
- @$scope.editor.open_doc_id
diff --git a/services/web/public/coffee/app/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/app/ide/editor/ShareJsDoc.coffee
deleted file mode 100644
index fe61580041..0000000000
--- a/services/web/public/coffee/app/ide/editor/ShareJsDoc.coffee
+++ /dev/null
@@ -1,123 +0,0 @@
-define [
- "utils/EventEmitter"
- "../../../libs/sharejs"
-], (EventEmitter, ShareJs) ->
- class ShareJsDoc extends EventEmitter
- constructor: (@doc_id, docLines, version, @socket) ->
- # Dencode any binary bits of data
- # See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
- @type = "text"
- docLines = for line in docLines
- if line.text?
- @type = "json"
- line.text = decodeURIComponent(escape(line.text))
- else
- @type = "text"
- line = decodeURIComponent(escape(line))
- line
-
- if @type == "text"
- snapshot = docLines.join("\n")
- else if @type == "json"
- snapshot = { lines: docLines }
- else
- throw new Error("Unknown type: #{@type}")
-
- @connection = {
- send: (update) =>
- @_startInflightOpTimeout(update)
- @socket.emit "applyOtUpdate", @doc_id, update
- state: "ok"
- id: @socket.socket.sessionid
- }
-
- @_doc = new ShareJs.Doc @connection, @doc_id,
- type: @type
- @_doc.on "change", () =>
- @trigger "change"
- @_doc.on "acknowledge", () =>
- @trigger "acknowledge"
- @_doc.on "remoteop", () =>
- @trigger "remoteop"
-
- @_bindToDocChanges(@_doc)
-
- @processUpdateFromServer
- open: true
- v: version
- snapshot: snapshot
-
- submitOp: (args...) -> @_doc.submitOp(args...)
-
- processUpdateFromServer: (message) ->
- try
- @_doc._onMessage message
- catch error
- # Version mismatches are thrown as errors
- @_handleError(error)
-
- if message?.meta?.type == "external"
- @trigger "externalUpdate"
-
- catchUp: (updates) ->
- for update, i in updates
- update.v = @_doc.version
- update.doc = @doc_id
- @processUpdateFromServer(update)
-
- getSnapshot: () -> @_doc.snapshot
- getVersion: () -> @_doc.version
- getType: () -> @type
-
- clearInflightAndPendingOps: () ->
- @_doc.inflightOp = null
- @_doc.inflightCallbacks = []
- @_doc.pendingOp = null
- @_doc.pendingCallbacks = []
-
- flushPendingOps: () ->
- # This will flush any ops that are pending.
- # If there is an inflight op it will do nothing.
- @_doc.flush()
-
- updateConnectionState: (state) ->
- @connection.state = state
- @connection.id = @socket.socket.sessionid
- @_doc.autoOpen = false
- @_doc._connectionStateChanged(state)
-
- hasBufferedOps: () ->
- @_doc.inflightOp? or @_doc.pendingOp?
-
- getInflightOp: () -> @_doc.inflightOp
- getPendingOp: () -> @_doc.pendingOp
-
- attachToAce: (ace) -> @_doc.attach_ace(ace)
- detachFromAce: () -> @_doc.detach_ace?()
-
- INFLIGHT_OP_TIMEOUT: 10000
- _startInflightOpTimeout: (update) ->
- meta =
- v: update.v
- op_sent_at: new Date()
- timer = setTimeout () =>
- @trigger "op:timeout", update
- , @INFLIGHT_OP_TIMEOUT
- @_doc.inflightCallbacks.push () =>
- clearTimeout timer
-
- _handleError: (error, meta = {}) ->
- @trigger "error", error, meta
-
- _bindToDocChanges: (doc) ->
- submitOp = doc.submitOp
- doc.submitOp = (args...) =>
- @trigger "op:sent", args...
- doc.pendingCallbacks.push () =>
- @trigger "op:acknowledged", args...
- submitOp.apply(doc, args)
-
- flush = doc.flush
- doc.flush = (args...) =>
- @trigger "flush", doc.inflightOp, doc.pendingOp, doc.version
- flush.apply(doc, args)
diff --git a/services/web/public/coffee/app/ide/editor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/app/ide/editor/auto-complete/AutoCompleteManager.coffee
deleted file mode 100644
index 20f451e040..0000000000
--- a/services/web/public/coffee/app/ide/editor/auto-complete/AutoCompleteManager.coffee
+++ /dev/null
@@ -1,95 +0,0 @@
-define [
- "ide/editor/auto-complete/SuggestionManager"
- "ide/editor/auto-complete/Snippets"
- "ace/autocomplete/util"
- "ace/autocomplete"
- "ace/range"
- "ace/ext/language_tools"
-], (SuggestionManager, Snippets, Util, AutoComplete) ->
- Range = require("ace/range").Range
- Autocomplete = AutoComplete.Autocomplete
-
- Util.retrievePrecedingIdentifier = (text, pos, regex) ->
- currentLineOffset = 0
- for i in [(pos-1)..0]
- if text[i] == "\n"
- currentLineOffset = i + 1
- break
- currentLine = text.slice(currentLineOffset, pos)
- fragment = getLastCommandFragment(currentLine) or ""
- return fragment
-
- getLastCommandFragment = (lineUpToCursor) ->
- if m = lineUpToCursor.match(/(\\[^\\ ]+)$/)
- return m[1]
- else
- return null
-
- class AutoCompleteManager
- constructor: (@$scope, @editor) ->
- @suggestionManager = new SuggestionManager()
-
- if !Autocomplete::_insertMatch?
- # Only override this once since it's global but we may create multiple
- # autocomplete handlers
- Autocomplete::_insertMatch = Autocomplete::insertMatch
- Autocomplete::insertMatch = (data) ->
- pos = editor.getCursorPosition()
- range = new Range(pos.row, pos.column, pos.row, pos.column + 1)
- nextChar = editor.session.getTextRange(range)
-
- console.log "INSERT MATCH", this
-
- # If we are in \begin{it|}, then we need to remove the trailing }
- # since it will be adding in with the autocomplete of \begin{item}...
- if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}"
- editor.session.remove(range)
-
- Autocomplete::_insertMatch.call this, data
-
- @$scope.$watch "autoComplete", (autocomplete) =>
- console.log "autocomplete change", autocomplete
- if autocomplete
- @enable()
- else
- @disable()
-
-
- @editor.on "changeSession", (e) =>
- @bindToSession(e.session)
-
- enable: () ->
- @editor.setOptions({
- enableBasicAutocompletion: true,
- enableSnippets: true
- })
-
- SnippetCompleter =
- getCompletions: (editor, session, pos, prefix, callback) ->
- callback null, Snippets
- @editor.completers = [@suggestionManager, SnippetCompleter]
-
- disable: () ->
- @editor.setOptions({
- enableBasicAutocompletion: false,
- enableSnippets: false
- })
-
- bindToSession: (@aceSession) ->
- @aceSession.on "change", (change) => @onChange(change)
-
- onChange: (change) ->
- cursorPosition = @editor.getCursorPosition()
- end = change.data.range.end
- # Check that this change was made by us, not a collaborator
- # (Cursor is still one place behind)
- if end.row == cursorPosition.row and end.column == cursorPosition.column + 1
- if change.data.action == "insertText"
- range = new Range(end.row, 0, end.row, end.column)
- lineUpToCursor = @aceSession.getTextRange(range)
- commandFragment = getLastCommandFragment(lineUpToCursor)
-
- if commandFragment? and commandFragment.length > 2
- setTimeout () =>
- @editor.execCommand("startAutocomplete")
- , 0
diff --git a/services/web/public/coffee/app/ide/editor/auto-complete/Snippets.coffee b/services/web/public/coffee/app/ide/editor/auto-complete/Snippets.coffee
deleted file mode 100644
index 53e0f57fa1..0000000000
--- a/services/web/public/coffee/app/ide/editor/auto-complete/Snippets.coffee
+++ /dev/null
@@ -1,100 +0,0 @@
-define () ->
- environments = [
- "abstract",
- "align", "align*",
- "equation", "equation*",
- "gather", "gather*",
- "multline", "multline*",
- "split",
- "verbatim"
- ]
-
- snippets = for env in environments
- {
- caption: "\\begin{#{env}}..."
- snippet: """
- \\begin{#{env}}
- $1
- \\end{#{env}}
- """
- meta: "env"
- }
-
- snippets = snippets.concat [{
- caption: "\\begin{array}..."
- snippet: """
- \\begin{array}{${1:cc}}
- $2 & $3 \\\\\\\\
- $4 & $5
- \\end{array}
- """
- meta: "env"
- }, {
- caption: "\\begin{figure}..."
- snippet: """
- \\begin{figure}
- \\centering
- \\includegraphics{$1}
- \\caption{${2:Caption}}
- \\label{${3:fig:my_label}}
- \\end{figure}
- """
- meta: "env"
- }, {
- caption: "\\begin{tabular}..."
- snippet: """
- \\begin{tabular}{${1:c|c}}
- $2 & $3 \\\\\\\\
- $4 & $5
- \\end{tabular}
- """
- meta: "env"
- }, {
- caption: "\\begin{table}..."
- snippet: """
- \\begin{table}[$1]
- \\centering
- \\begin{tabular}{${2:c|c}}
- $3 & $4 \\\\\\\\
- $5 & $6
- \\end{tabular}
- \\caption{${7:Caption}}
- \\label{${8:tab:my_label}}
- \\end{table}
- """
- meta: "env"
- }, {
- caption: "\\begin{list}..."
- snippet: """
- \\begin{list}
- \\item $1
- \\end{list}
- """
- meta: "env"
- }, {
- caption: "\\begin{enumerate}..."
- snippet: """
- \\begin{enumerate}
- \\item $1
- \\end{enumerate}
- """
- meta: "env"
- }, {
- caption: "\\begin{itemize}..."
- snippet: """
- \\begin{itemize}
- \\item $1
- \\end{itemize}
- """
- meta: "env"
- }, {
- caption: "\\begin{frame}..."
- snippet: """
- \\begin{frame}{${1:Frame Title}}
- $2
- \\end{frame}
- """
- meta: "env"
- }]
-
- return snippets
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/editor/auto-complete/SuggestionManager.coffee b/services/web/public/coffee/app/ide/editor/auto-complete/SuggestionManager.coffee
deleted file mode 100644
index 559a2c5981..0000000000
--- a/services/web/public/coffee/app/ide/editor/auto-complete/SuggestionManager.coffee
+++ /dev/null
@@ -1,126 +0,0 @@
-define [], () ->
- class Parser
- constructor: (@doc) ->
-
- parse: () ->
- commands = []
- seen = {}
- while command = @nextCommand()
- docState = @doc
-
- optionalArgs = 0
- while @consumeArgument("[", "]")
- optionalArgs++
-
- args = 0
- while @consumeArgument("{", "}")
- args++
-
- commandHash = "#{command}\\#{optionalArgs}\\#{args}"
- if !seen[commandHash]?
- seen[commandHash] = true
- commands.push [command, optionalArgs, args]
-
- # Reset to before argument to handle nested commands
- @doc = docState
-
- return commands
-
- # Ignore single letter commands since auto complete is moot then.
- commandRegex: /\\([a-zA-Z][a-zA-Z]+)/
-
- nextCommand: () ->
- i = @doc.search(@commandRegex)
- if i == -1
- return false
- else
- match = @doc.match(@commandRegex)[1]
- @doc = @doc.substr(i + match.length + 1)
- return match
-
- consumeWhitespace: () ->
- match = @doc.match(/^[ \t\n]*/m)[0]
- @doc = @doc.substr(match.length)
-
- consumeArgument: (openingBracket, closingBracket) ->
- @consumeWhitespace()
-
- if @doc[0] == openingBracket
- i = 1
- bracketParity = 1
- while bracketParity > 0 and i < @doc.length
- if @doc[i] == openingBracket
- bracketParity++
- else if @doc[i] == closingBracket
- bracketParity--
- i++
-
- if bracketParity == 0
- @doc = @doc.substr(i)
- return true
- else
- return false
- else
- return false
-
- class SuggestionManager
- getCompletions: (editor, session, pos, prefix, callback) ->
- doc = session.getValue()
- parser = new Parser(doc)
- commands = parser.parse()
-
- completions = []
- for command in commands
- caption = "\\#{command[0]}"
- snippet = caption
- i = 1
- _.times command[1], () ->
- snippet += "[${#{i}}]"
- caption += "[]"
- i++
- _.times command[2], () ->
- snippet += "{${#{i}}}"
- caption += "{}"
- i++
- unless caption == prefix
- completions.push {
- caption: caption
- snippet: snippet
- meta: "cmd"
- }
-
- callback null, completions
-
- loadCommandsFromDoc: (doc) ->
- parser = new Parser(doc)
- @commands = parser.parse()
-
- getSuggestions: (commandFragment) ->
- matchingCommands = _.filter @commands, (command) ->
- command[0].slice(0, commandFragment.length) == commandFragment
-
- return _.map matchingCommands, (command) ->
- base = "\\" + commandFragment
-
- args = ""
- _.times command[1], () -> args = args + "[]"
- _.times command[2], () -> args = args + "{}"
- completionBase = command[0].slice(commandFragment.length)
-
- squareArgsNo = command[1]
- curlyArgsNo = command[2]
- totalArgs = squareArgsNo + curlyArgsNo
- if totalArgs == 0
- completionBeforeCursor = completionBase
- completionAfterCurspr = ""
- else
- completionBeforeCursor = completionBase + args[0]
- completionAfterCursor = args.slice(1)
-
- return {
- base: base,
- completion: completionBase + args,
- completionBeforeCursor: completionBeforeCursor
- completionAfterCursor: completionAfterCursor
- }
-
diff --git a/services/web/public/coffee/app/ide/editor/controllers/SavingNotificationController.coffee b/services/web/public/coffee/app/ide/editor/controllers/SavingNotificationController.coffee
deleted file mode 100644
index edefe07ea9..0000000000
--- a/services/web/public/coffee/app/ide/editor/controllers/SavingNotificationController.coffee
+++ /dev/null
@@ -1,33 +0,0 @@
-define [
- "base"
- "ide/editor/Document"
-], (App, Document) ->
- App.controller "SavingNotificationController", ["$scope", "$interval", "ide", ($scope, $interval, ide) ->
- $interval () ->
- pollSavedStatus()
- , 1000
-
- $(window).bind 'beforeunload', () =>
- warnAboutUnsavedChanges()
-
- $scope.docSavingStatus = {}
- pollSavedStatus = () ->
- oldStatus = $scope.docSavingStatus
- $scope.docSavingStatus = {}
-
- for doc_id, doc of Document.openDocs
- saving = doc.pollSavedStatus()
- if !saving
- if oldStatus[doc_id]?
- $scope.docSavingStatus[doc_id] = oldStatus[doc_id]
- $scope.docSavingStatus[doc_id].unsavedSeconds += 1
- else
- $scope.docSavingStatus[doc_id] = {
- unsavedSeconds: 0
- doc: ide.fileTreeManager.findEntityById(doc_id)
- }
-
- warnAboutUnsavedChanges = () ->
- if Document.hasUnsavedChanges()
- return "You have unsaved changes. If you leave now they will not be saved."
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/editor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/app/ide/editor/cursor-position/CursorPositionManager.coffee
deleted file mode 100644
index 27f7045d77..0000000000
--- a/services/web/public/coffee/app/ide/editor/cursor-position/CursorPositionManager.coffee
+++ /dev/null
@@ -1,52 +0,0 @@
-define [], () ->
- class CursorPositionManager
- constructor: (@$scope, @editor, @element) ->
-
- @editor.on "changeSession", (e) =>
- e.session.on "changeScrollTop", (e) =>
- @onScrollTopChange(e)
-
- e.session.selection.on 'changeCursor', (e) =>
- @onCursorChange(e)
-
- @gotoStoredPosition()
-
- @editor.on "changeSelection", () =>
- cursor = @editor.getCursorPosition()
- @$scope.$apply () =>
- if @$scope.cursorPosition?
- @$scope.cursorPosition = cursor
-
- @$scope.$watch "gotoLine", (value) =>
- console.log "Going to line", value
- if value?
- setTimeout () =>
- @gotoLine(value)
- @$scope.$apply () =>
- @$scope.gotoLine = null
- , 0
-
- onScrollTopChange: (event) ->
- if !@ignoreCursorPositionChanges and doc_id = @$scope.sharejsDoc?.doc_id
- docPosition = $.localStorage("doc.position.#{doc_id}") || {}
- docPosition.scrollTop = @editor.getSession().getScrollTop()
- $.localStorage("doc.position.#{doc_id}", docPosition)
-
- onCursorChange: (event) ->
- if !@ignoreCursorPositionChanges and doc_id = @$scope.sharejsDoc?.doc_id
- docPosition = $.localStorage("doc.position.#{doc_id}") || {}
- docPosition.cursorPosition = @editor.getCursorPosition()
- $.localStorage("doc.position.#{doc_id}", docPosition)
-
- gotoStoredPosition: () ->
- doc_id = @$scope.sharejsDoc?.doc_id
- return if !doc_id?
- pos = $.localStorage("doc.position.#{doc_id}") || {}
- @ignoreCursorPositionChanges = true
- @editor.moveCursorToPosition(pos.cursorPosition or {row: 0, column: 0})
- @editor.getSession().setScrollTop(pos.scrollTop or 0)
- delete @ignoreCursorPositionChanges
-
- gotoLine: (line) ->
- @editor.gotoLine(line)
- @editor.focus()
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee
deleted file mode 100644
index 423f9a1307..0000000000
--- a/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee
+++ /dev/null
@@ -1,187 +0,0 @@
-define [
- "base"
- "ace/ace"
- "ide/editor/undo/UndoManager"
- "ide/editor/auto-complete/AutoCompleteManager"
- "ide/editor/spell-check/SpellCheckManager"
- "ide/editor/highlights/HighlightsManager"
- "ide/editor/cursor-position/CursorPositionManager"
- "ace/keyboard/vim"
- "ace/keyboard/emacs"
- "ace/mode/latex"
- "ace/edit_session"
-], (App, Ace, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager) ->
- LatexMode = require("ace/mode/latex").Mode
- EditSession = require('ace/edit_session').EditSession
-
- App.directive "aceEditor", ["$timeout", ($timeout) ->
- return {
- scope: {
- theme: "="
- showPrintMargin: "="
- keybindings: "="
- fontSize: "="
- autoComplete: "="
- sharejsDoc: "="
- lastUpdated: "="
- spellCheckLanguage: "="
- cursorPosition: "="
- highlights: "="
- text: "="
- readOnly: "="
- gotoLine: "="
- annotations: "="
- }
- link: (scope, element, attrs) ->
- # Don't freak out if we're already in an apply callback
- scope.$originalApply = scope.$apply
- scope.$apply = (fn = () ->) ->
- phase = @$root.$$phase
- if (phase == '$apply' || phase == '$digest')
- fn()
- else
- @$originalApply(fn);
-
- editor = Ace.edit(element.find(".ace-editor-body")[0])
- window.editors ||= []
- window.editors.push editor
-
- autoCompleteManager = new AutoCompleteManager(scope, editor, element)
- spellCheckManager = new SpellCheckManager(scope, editor, element)
- undoManager = new UndoManager(scope, editor, element)
- highlightsManager = new HighlightsManager(scope, editor, element)
- cursorPositionManager = new CursorPositionManager(scope, editor, element)
-
- # Prevert Ctrl|Cmd-S from triggering save dialog
- editor.commands.addCommand
- name: "save",
- bindKey: win: "Ctrl-S", mac: "Command-S"
- exec: () ->
- readOnly: true
- editor.commands.removeCommand "transposeletters"
- editor.commands.removeCommand "showSettingsMenu"
- editor.commands.removeCommand "foldall"
-
- if attrs.resizeOn?
- for event in attrs.resizeOn.split(",")
- scope.$on event, () ->
- editor.resize()
-
- scope.$watch "theme", (value) ->
- editor.setTheme("ace/theme/#{value}")
-
- scope.$watch "showPrintMargin", (value) ->
- editor.setShowPrintMargin(value)
-
- scope.$watch "keybindings", (value) ->
- Vim = require("ace/keyboard/vim").handler
- Emacs = require("ace/keyboard/emacs").handler
- keybindings = vim: Vim, emacs: Emacs
- editor.setKeyboardHandler(keybindings[value])
-
- scope.$watch "fontSize", (value) ->
- element.find(".ace_editor, .ace_content").css({
- "font-size": value + "px"
- })
-
- scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) ->
- if old_sharejs_doc?
- detachFromAce(old_sharejs_doc)
-
- if sharejs_doc?
- attachToAce(sharejs_doc)
-
- scope.$watch "text", (text) ->
- if text?
- editor.setValue(text, -1)
- session = editor.getSession()
- session.setUseWrapMode(true)
- session.setMode(new LatexMode())
-
- scope.$watch "annotations", (annotations) ->
- console.log "SETTING ANNOTATIONS", annotations
- if annotations?
- session = editor.getSession()
- session.setAnnotations annotations
-
- scope.$watch "readOnly", (value) ->
- editor.setReadOnly !!value
-
- resetSession = () ->
- session = editor.getSession()
- session.setUseWrapMode(true)
- session.setMode(new LatexMode())
- session.setAnnotations scope.annotations
-
- attachToAce = (sharejs_doc) ->
- lines = sharejs_doc.getSnapshot().split("\n")
- editor.setSession(new EditSession(lines))
- resetSession()
- session = editor.getSession()
-
- doc = session.getDocument()
- doc.on "change", () ->
- scope.$apply () ->
- scope.lastUpdated = new Date()
-
- sharejs_doc.on "remoteop.recordForUndo", () =>
- undoManager.nextUpdateIsRemote = true
-
- sharejs_doc.attachToAce(editor)
-
- editor.focus()
-
- detachFromAce = (sharejs_doc) ->
- sharejs_doc.detachFromAce()
- sharejs_doc.off "remoteop.recordForUndo"
-
- template: """
-
-
-
Watch out!
- We had to undo some of your collaborators changes before we could undo yours.
-
Dismiss
-
-
-
-
- {{ annotationLabel.text }}
-
-
- """
- }
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/editor/highlights/HighlightsManager.coffee b/services/web/public/coffee/app/ide/editor/highlights/HighlightsManager.coffee
deleted file mode 100644
index a8b7699407..0000000000
--- a/services/web/public/coffee/app/ide/editor/highlights/HighlightsManager.coffee
+++ /dev/null
@@ -1,228 +0,0 @@
-define [
- "ace/range"
-], () ->
- Range = require("ace/range").Range
-
- class HighlightsManager
- constructor: (@$scope, @editor, @element) ->
- @markerIds = []
- @labels = []
-
- @$scope.annotationLabel = {
- show: false
- right: "auto"
- left: "auto"
- top: "auto"
- bottom: "auto"
- backgroundColor: "black"
- text: ""
- }
-
- @$scope.$watch "highlights", (value) =>
- @redrawAnnotations()
-
- @$scope.$watch "theme", (value) =>
- @redrawAnnotations()
-
- @editor.on "mousemove", (e) =>
- position = @editor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
- e.position = position
- @showAnnotationLabels(position)
-
- @editor.on "changeSession", () =>
- @redrawAnnotations()
-
- redrawAnnotations: () ->
- @_clearMarkers()
- @_clearLabels()
-
- for annotation in @$scope.highlights or []
- do (annotation) =>
- colorScheme = @_getColorScheme(annotation.hue)
- if annotation.cursor?
- console.log "DRAWING CURSOR", annotation
- @labels.push {
- text: annotation.label
- range: new Range(
- annotation.cursor.row, annotation.cursor.column,
- annotation.cursor.row, annotation.cursor.column + 1
- )
- colorScheme: colorScheme
- snapToStartOfRange: true
- }
- @_drawCursor(annotation, colorScheme)
- else if annotation.highlight?
- @labels.push {
- text: annotation.label
- range: new Range(
- annotation.highlight.start.row, annotation.highlight.start.column,
- annotation.highlight.end.row, annotation.highlight.end.column
- )
- colorScheme: colorScheme
- }
- @_drawHighlight(annotation, colorScheme)
- else if annotation.strikeThrough?
- @labels.push {
- text: annotation.label
- range: new Range(
- annotation.strikeThrough.start.row, annotation.strikeThrough.start.column,
- annotation.strikeThrough.end.row, annotation.strikeThrough.end.column
- )
- colorScheme: colorScheme
- }
- @_drawStrikeThrough(annotation, colorScheme)
-
- showAnnotationLabels: (position) ->
- labelToShow = null
- for label in @labels or []
- if label.range.contains(position.row, position.column)
- labelToShow = label
-
- if !labelToShow?
- @$scope.$apply () =>
- @$scope.annotationLabel.show = false
- else
- $ace = $(@editor.renderer.container).find(".ace_scroller")
- # Move the label into the Ace content area so that offsets and positions are easy to calculate.
- $ace.append(@element.find(".annotation-label"))
-
- if labelToShow.snapToStartOfRange
- coords = @editor.renderer.textToScreenCoordinates(labelToShow.range.start.row, labelToShow.range.start.column)
- else
- coords = @editor.renderer.textToScreenCoordinates(position.row, position.column)
-
- offset = $ace.offset()
- height = $ace.height()
- coords.pageX = coords.pageX - offset.left
- coords.pageY = coords.pageY - offset.top
-
- if coords.pageY > @editor.renderer.lineHeight * 2
- top = "auto"
- bottom = height - coords.pageY
- else
- top = coords.pageY + @editor.renderer.lineHeight
- bottom = "auto"
-
- # Apply this first that the label has the correct width when calculating below
- @$scope.$apply () =>
- @$scope.annotationLabel.text = labelToShow.text
- @$scope.annotationLabel.show = true
-
- $label = @element.find(".annotation-label")
- console.log "pageX", coords.pageX, "label", $label.outerWidth(), "ace", $ace.width()
-
- if coords.pageX + $label.outerWidth() < $ace.width()
- left = coords.pageX
- right = "auto"
- else
- right = 0
- left = "auto"
-
- @$scope.$apply () =>
- @$scope.annotationLabel = {
- show: true
- left: left
- right: right
- bottom: bottom
- top: top
- backgroundColor: labelToShow.colorScheme.labelBackgroundColor
- text: labelToShow.text
- }
-
- _clearMarkers: () ->
- for marker_id in @markerIds
- @editor.getSession().removeMarker(marker_id)
- @markerIds = []
-
- _clearLabels: () ->
- @labels = []
-
- _drawCursor: (annotation, colorScheme) ->
- @markerIds.push @editor.getSession().addMarker new Range(
- annotation.cursor.row, annotation.cursor.column,
- annotation.cursor.row, annotation.cursor.column + 1
- ), "annotation remote-cursor", (html, range, left, top, config) ->
- div = """
-
- """
- html.push div
- , true
-
- _drawHighlight: (annotation, colorScheme) ->
- @_addMarkerWithCustomStyle(
- new Range(
- annotation.highlight.start.row, annotation.highlight.start.column,
- annotation.highlight.end.row, annotation.highlight.end.column + 1
- ),
- "annotation highlight",
- false,
- "background-color: #{colorScheme.highlightBackgroundColor}"
- )
-
- _drawStrikeThrough: (annotation, colorScheme) ->
- lineHeight = @editor.renderer.lineHeight
- @_addMarkerWithCustomStyle(
- new Range(
- annotation.strikeThrough.start.row, annotation.strikeThrough.start.column,
- annotation.strikeThrough.end.row, annotation.strikeThrough.end.column + 1
- ),
- "annotation strike-through-background",
- false,
- "background-color: #{colorScheme.strikeThroughBackgroundColor}"
- )
- @_addMarkerWithCustomStyle(
- new Range(
- annotation.strikeThrough.start.row, annotation.strikeThrough.start.column,
- annotation.strikeThrough.end.row, annotation.strikeThrough.end.column + 1
- ),
- "annotation strike-through-foreground",
- true,
- """
- height: #{Math.round(lineHeight/2) + 2}px;
- border-bottom: 2px solid #{colorScheme.strikeThroughForegroundColor};
- """
- )
-
- _addMarkerWithCustomStyle: (range, klass, foreground, style) ->
- if foreground?
- markerLayer = @editor.renderer.$markerBack
- else
- markerLayer = @editor.renderer.$markerFront
-
- @markerIds.push @editor.getSession().addMarker range, klass, (html, range, left, top, config) ->
- if range.isMultiLine()
- markerLayer.drawTextMarker(html, range, klass, config, style)
- else
- markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, style)
- , foreground
-
- _getColorScheme: (hue) ->
- if @_isDarkTheme()
- return {
- cursor: "hsl(#{hue}, 70%, 50%)"
- labelBackgroundColor: "hsl(#{hue}, 70%, 50%)"
- highlightBackgroundColor: "hsl(#{hue}, 100%, 28%);"
- strikeThroughBackgroundColor: "hsl(#{hue}, 100%, 20%);"
- strikeThroughForegroundColor: "hsl(#{hue}, 100%, 60%);"
- }
- else
- return {
- cursor: "hsl(#{hue}, 70%, 50%)"
- labelBackgroundColor: "hsl(#{hue}, 70%, 50%)"
- highlightBackgroundColor: "hsl(#{hue}, 70%, 85%);"
- strikeThroughBackgroundColor: "hsl(#{hue}, 70%, 95%);"
- strikeThroughForegroundColor: "hsl(#{hue}, 70%, 40%);"
- }
-
- _isDarkTheme: () ->
- rgb = @element.find(".ace_editor").css("background-color");
- [m, r, g, b] = rgb.match(/rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)/)
- r = parseInt(r, 10)
- g = parseInt(g, 10)
- b = parseInt(b, 10)
- return r + g + b < 3 * 128
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/editor/spell-check/HighlightedWordManager.coffee b/services/web/public/coffee/app/ide/editor/spell-check/HighlightedWordManager.coffee
deleted file mode 100644
index acd4c512e9..0000000000
--- a/services/web/public/coffee/app/ide/editor/spell-check/HighlightedWordManager.coffee
+++ /dev/null
@@ -1,150 +0,0 @@
-define [
- "ace/range"
-], () ->
- Range = require("ace/range").Range
-
- class Highlight
- constructor: (options) ->
- @row = options.row
- @column = options.column
- @word = options.word
- @suggestions = options.suggestions
-
- class HighlightedWordManager
- constructor: (@editor) ->
- @highlights = rows: []
-
- addHighlight: (highlight) ->
- unless highlight instanceof Highlight
- highlight = new Highlight(highlight)
- range = new Range(
- highlight.row, highlight.column,
- highlight.row, highlight.column + highlight.word.length
- )
- highlight.markerId = @editor.getSession().addMarker range, "spelling-highlight"
- @highlights.rows[highlight.row] ||= []
- @highlights.rows[highlight.row].push highlight
-
- removeHighlight: (highlight) ->
- @editor.getSession().removeMarker(highlight.markerId)
- for h, i in @highlights.rows[highlight.row]
- if h == highlight
- @highlights.rows[highlight.row].splice(i, 1)
-
- removeWord: (word) ->
- toRemove = []
- for row in @highlights.rows
- for highlight in (row || [])
- if highlight.word == word
- toRemove.push(highlight)
- for highlight in toRemove
- @removeHighlight highlight
-
- moveHighlight: (highlight, position) ->
- @removeHighlight highlight
- highlight.row = position.row
- highlight.column = position.column
- @addHighlight highlight
-
- clearRows: (from, to) ->
- from ||= 0
- to ||= @highlights.rows.length - 1
- for row in @highlights.rows.slice(from, to + 1)
- for highlight in (row || []).slice(0)
- @removeHighlight highlight
-
- insertRows: (offset, number) ->
- # rows are inserted after offset. i.e. offset row is not modified
- affectedHighlights = []
- for row in @highlights.rows.slice(offset)
- affectedHighlights.push(highlight) for highlight in (row || [])
- for highlight in affectedHighlights
- @moveHighlight highlight,
- row: highlight.row + number
- column: highlight.column
-
- removeRows: (offset, number) ->
- # offset is the first row to delete
- affectedHighlights = []
- for row in @highlights.rows.slice(offset)
- affectedHighlights.push(highlight) for highlight in (row || [])
- for highlight in affectedHighlights
- if highlight.row >= offset + number
- @moveHighlight highlight,
- row: highlight.row - number
- column: highlight.column
- else
- @removeHighlight highlight
-
- findHighlightWithinRange: (range) ->
- rows = @highlights.rows.slice(range.start.row, range.end.row + 1)
- for row in rows
- for highlight in (row || [])
- if @_doesHighlightOverlapRange(highlight, range.start, range.end)
- return highlight
- return null
-
- applyChange: (change) ->
- start = change.range.start
- end = change.range.end
- if change.action == "insertText"
- if start.row != end.row
- rowsAdded = end.row - start.row
- @insertRows start.row + 1, rowsAdded
- # make a copy since we're going to modify in place
- oldHighlights = (@highlights.rows[start.row] || []).slice(0)
- for highlight in oldHighlights
- if highlight.column > start.column
- # insertion was fully before this highlight
- @moveHighlight highlight,
- row: end.row
- column: highlight.column + (end.column - start.column)
- else if highlight.column + highlight.word.length >= start.column
- # insertion was inside this highlight
- @removeHighlight highlight
-
- else if change.action == "insertLines"
- @insertRows start.row, change.lines.length
-
- else if change.action == "removeText"
- if start.row == end.row
- oldHighlights = (@highlights.rows[start.row] || []).slice(0)
- else
- rowsRemoved = end.row - start.row
- oldHighlights =
- (@highlights.rows[start.row] || []).concat(
- (@highlights.rows[end.row] || [])
- )
- @removeRows start.row + 1, rowsRemoved
-
- for highlight in oldHighlights
- if @_doesHighlightOverlapRange highlight, start, end
- @removeHighlight highlight
- else if @_isHighlightAfterRange highlight, start, end
- @moveHighlight highlight,
- row: start.row
- column: highlight.column - (end.column - start.column)
-
- else if change.action == "removeLines"
- @removeRows start.row, change.lines.length
-
- _doesHighlightOverlapRange: (highlight, start, end) ->
- highlightIsAllBeforeRange =
- highlight.row < start.row or
- (highlight.row == start.row and highlight.column + highlight.word.length <= start.column)
- highlightIsAllAfterRange =
- highlight.row > end.row or
- (highlight.row == end.row and highlight.column >= end.column)
- !(highlightIsAllBeforeRange or highlightIsAllAfterRange)
-
- _isHighlightAfterRange: (highlight, start, end) ->
- return true if highlight.row > end.row
- return false if highlight.row < end.row
- highlight.column >= end.column
-
-
-
-
-
-
-
diff --git a/services/web/public/coffee/app/ide/editor/spell-check/SpellCheckManager.coffee b/services/web/public/coffee/app/ide/editor/spell-check/SpellCheckManager.coffee
deleted file mode 100644
index 2f9838a9ab..0000000000
--- a/services/web/public/coffee/app/ide/editor/spell-check/SpellCheckManager.coffee
+++ /dev/null
@@ -1,191 +0,0 @@
-define [
- "ide/editor/spell-check/HighlightedWordManager"
- "ace/range"
-], (HighlightedWordManager) ->
- Range = require("ace/range").Range
-
- class SpellCheckManager
- constructor: (@$scope, @editor, @element) ->
- @updatedLines = []
- @highlightedWordManager = new HighlightedWordManager(@editor)
-
- @$scope.$watch "spellCheckLanguage", (language, oldLanguage) =>
- if language != oldLanguage and oldLanguage?
- @runFullCheck()
-
- @editor.on "changeSession", (e) =>
- @runFullCheck()
-
- doc = e.session.getDocument()
- doc.on "change", (e) =>
- @runCheckOnChange(e)
-
- @$scope.spellingMenu = {left: '0px', top: '0px'}
-
- @editor.on "nativecontextmenu", (e) =>
- @closeContextMenu(e.domEvent)
- @openContextMenu(e.domEvent)
-
- $(document).on "click", (e) =>
- @closeContextMenu(e)
- return true
- # $(document).on "contextmenu", (e) =>
- # @closeContextMenu(e)
-
- @$scope.replaceWord = (highlight, suggestion) =>
- @replaceWord(highlight, suggestion)
-
- @$scope.learnWord = (highlight) =>
- @learnWord(highlight)
-
- runFullCheck: () ->
- console.log "Running full check"
- @highlightedWordManager.clearRows()
- if @$scope.spellCheckLanguage and @$scope.spellCheckLanguage != ""
- @runSpellCheck()
-
- runCheckOnChange: (e) ->
- console.log "Checking change", e.data
- if @$scope.spellCheckLanguage and @$scope.spellCheckLanguage != ""
- @highlightedWordManager.applyChange(e.data)
- @markLinesAsUpdated(e.data)
- @runSpellCheckSoon()
-
- openContextMenu: (e) ->
- position = @editor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
- highlight = @highlightedWordManager.findHighlightWithinRange
- start: position
- end: position
-
- @$scope.$apply () =>
- @$scope.spellingMenu.highlight = highlight
-
- console.log "highlight", @$scope.highlight_under_mouse
-
- if highlight
- e.stopPropagation()
- e.preventDefault()
-
- @editor.getSession().getSelection().setSelectionRange(
- new Range(
- highlight.row, highlight.column
- highlight.row, highlight.column + highlight.word.length
- )
- )
-
- console.log "Height", @element.find(".context-menu").height()
-
- @$scope.$apply () =>
- @$scope.spellingMenu.open = true
- @$scope.spellingMenu.left = e.clientX + 'px'
- @$scope.spellingMenu.top = e.clientY + 'px'
-
- closeContextMenu: (e) ->
- @$scope.$apply () =>
- @$scope.spellingMenu.open = false
-
- replaceWord: (highlight, text) ->
- @editor.getSession().replace(new Range(
- highlight.row, highlight.column,
- highlight.row, highlight.column + highlight.word.length
- ), text)
-
- learnWord: (highlight) ->
- @apiRequest "/learn", word: highlight.word
- @highlightedWordManager.removeWord highlight.word
-
- getHighlightedWordAtCursor: () ->
- cursor = @editor.getCursorPosition()
- highlight = @highlightedWordManager.findHighlightWithinRange
- start: cursor
- end: cursor
- return highlight
-
- runSpellCheckSoon: () ->
- run = () =>
- delete @timeoutId
- @runSpellCheck(@updatedLines)
- @updatedLines = []
- if @timeoutId?
- clearTimeout @timeoutId
- @timeoutId = setTimeout run, 1000
-
- markLinesAsUpdated: (change) ->
- start = change.range.start
- end = change.range.end
-
- insertLines = () =>
- lines = end.row - start.row
- while lines--
- @updatedLines.splice(start.row, 0, true)
-
- removeLines = () =>
- lines = end.row - start.row
- while lines--
- @updatedLines.splice(start.row + 1, 1)
-
- if change.action == "insertText"
- @updatedLines[start.row] = true
- insertLines()
- else if change.action == "removeText"
- @updatedLines[start.row] = true
- removeLines()
- else if change.action == "insertLines"
- insertLines()
- else if change.action == "removeLines"
- removeLines()
-
- runSpellCheck: (linesToProcess) ->
- {words, positions} = @getWords(linesToProcess)
- language = @$scope.spellCheckLanguage
- @apiRequest "/check", {language: language, words: words}, (error, result) =>
- if error? or !result? or !result.misspellings?
- return null
-
- if linesToProcess?
- for shouldProcess, row in linesToProcess
- @highlightedWordManager.clearRows(row, row) if shouldProcess
- else
- @highlightedWordManager.clearRows()
-
- for misspelling in result.misspellings
- word = words[misspelling.index]
- position = positions[misspelling.index]
- @highlightedWordManager.addHighlight
- column: position.column
- row: position.row
- word: word
- suggestions: misspelling.suggestions
-
- getWords: (linesToProcess) ->
- lines = @editor.getValue().split("\n")
- words = []
- positions = []
- for line, row in lines
- if !linesToProcess? or linesToProcess[row]
- wordRegex = /\\?['a-zA-Z\u00C0-\u00FF]+/g
- while (result = wordRegex.exec(line))
- word = result[0]
- if word[0] == "'"
- word = word.slice(1)
- if word[word.length - 1] == "'"
- word = word.slice(0,-1)
- positions.push row: row, column: result.index
- words.push(word)
- return words: words, positions: positions
-
- apiRequest: (endpoint, data, callback = (error, result) ->)->
- data.token = window.user.id
- data._csrf = window.csrfToken
- options =
- url: "/spelling" + endpoint
- type: "POST"
- dataType: "json"
- headers:
- "Content-Type": "application/json"
- data: JSON.stringify data
- success: (data, status, xhr) ->
- callback null, data
- error: (xhr, status, error) ->
- callback error
- $.ajax options
diff --git a/services/web/public/coffee/app/ide/editor/spell-check/SpellingMenuView.coffee b/services/web/public/coffee/app/ide/editor/spell-check/SpellingMenuView.coffee
deleted file mode 100644
index 3ba9fd1d96..0000000000
--- a/services/web/public/coffee/app/ide/editor/spell-check/SpellingMenuView.coffee
+++ /dev/null
@@ -1,82 +0,0 @@
-define [
- "libs/backbone"
- "libs/mustache"
-], () ->
- SUGGESTIONS_TO_SHOW = 5
-
- SpellingMenuView = Backbone.View.extend
- templates:
- menu: $("#spellingMenuTemplate").html()
- entry: $("#spellingMenuEntryTemplate").html()
-
- events:
- "click a#learnWord": ->
- @trigger "click:learn", @_currentHighlight
- @hide()
-
- initialize: (options) ->
- @ide = options.ide
- @ide.editor.getContainerElement().append @render().el
- @ide.editor.on "click", () => @hide()
- @ide.editor.on "scroll", () => @hide()
- @ide.editor.on "update:doc", () => @hide()
- @ide.editor.on "change:doc", () => @hide()
-
- render: () ->
- @setElement($(@templates.menu))
- @$el.css "z-index" : 10000
- @$(".dropdown-toggle").dropdown()
- @hide()
- return @
-
- showForHighlight: (highlight) ->
- if @_currentHighlight? and highlight != @_currentHighlight
- @_close()
-
- if !@_currentHighlight?
- @_currentHighlight = highlight
- @_setSuggestions(highlight)
- position = @ide.editor.textToEditorCoordinates(
- highlight.row
- highlight.column + highlight.word.length
- )
- @_position(position.x, position.y)
- @_show()
-
- hideIfAppropriate: (cursorPosition) ->
- if @_currentHighlight?
- if !@_cursorCloseToHighlight(cursorPosition, @_currentHighlight) and !@_isOpen()
- @hide()
-
- hide: () ->
- delete @_currentHighlight
- @_close()
- @$el.hide()
-
- _setSuggestions: (highlight) ->
- @$(".spelling-suggestion").remove()
- divider = @$(".divider")
- for suggestion in highlight.suggestions.slice(0, SUGGESTIONS_TO_SHOW)
- do (suggestion) =>
- entry = $(Mustache.to_html(@templates.entry, word: suggestion))
- divider.before(entry)
- entry.on "click", () =>
- @trigger "click:suggestion", suggestion, highlight
-
- _show: () -> @$el.show()
-
- _isOpen: () ->
- @$(".dropdown-menu").is(":visible")
-
- _close: () ->
- if @_isOpen()
- @$el.dropdown("toggle")
-
- _cursorCloseToHighlight: (position, highlight) ->
- position.row == highlight.row and
- position.column >= highlight.column and
- position.column <= highlight.column + highlight.word.length + 1
-
- _position: (x,y) -> @$el.css left: x, top: y
-
-
diff --git a/services/web/public/coffee/app/ide/editor/undo/UndoManager.coffee b/services/web/public/coffee/app/ide/editor/undo/UndoManager.coffee
deleted file mode 100644
index a6caf52372..0000000000
--- a/services/web/public/coffee/app/ide/editor/undo/UndoManager.coffee
+++ /dev/null
@@ -1,374 +0,0 @@
-define [
- "ace/range"
- "ace/edit_session"
- "ace/document"
-], () ->
- Range = require("ace/range").Range
- EditSession = require("ace/edit_session").EditSession
- Doc = require("ace/document").Document
-
- class UndoManager
- constructor: (@$scope, @editor) ->
- @$scope.undo =
- show_remote_warning: false
-
- @reset()
- @nextUpdateIsRemote = false
-
- @editor.on "changeSession", (e) =>
- console.log "setting undo manager", e.session
- e.session.setUndoManager(@)
-
- showUndoConflictWarning: () ->
- @$scope.$apply () =>
- @$scope.undo.show_remote_warning = true
-
- setTimeout () =>
- @$scope.$apply () =>
- @$scope.undo.show_remote_warning = false
- , 4000
-
- reset: () ->
- @undoStack = []
- @redoStack = []
-
- execute: (options) ->
- aceDeltaSets = options.args[0]
- @session = options.args[1]
- return if !aceDeltaSets?
-
- lines = @session.getDocument().getAllLines()
- linesBeforeChange = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, lines)
- simpleDeltaSets = @_aceDeltaSetsToSimpleDeltaSets(aceDeltaSets, linesBeforeChange)
- @undoStack.push(
- deltaSets: simpleDeltaSets
- remote: @nextUpdateIsRemote
- )
- @redoStack = []
- @nextUpdateIsRemote = false
-
- undo: (dontSelect) ->
- localUpdatesMade = @_shiftLocalChangeToTopOfUndoStack()
- return if !localUpdatesMade
-
- update = @undoStack.pop()
- return if !update?
-
- if update.remote
- @showUndoConflictWarning()
-
- lines = @session.getDocument().getAllLines()
- linesBeforeDelta = @_revertSimpleDeltaSetsOnDocLines(update.deltaSets, lines)
- deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, linesBeforeDelta)
- selectionRange = @session.undoChanges(deltaSets, dontSelect)
- @redoStack.push(update)
- return selectionRange
-
- redo: (dontSelect) ->
- update = @redoStack.pop()
- return if !update?
- lines = @session.getDocument().getAllLines()
- deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, lines)
- selectionRange = @session.redoChanges(deltaSets, dontSelect)
- @undoStack.push(update)
- return selectionRange
-
- _shiftLocalChangeToTopOfUndoStack: () ->
- head = []
- localChangeExists = false
- while @undoStack.length > 0
- update = @undoStack.pop()
- head.unshift update
- if !update.remote
- localChangeExists = true
- break
-
- if !localChangeExists
- @undoStack = @undoStack.concat head
- return false
- else
- # Undo stack looks like undoStack ++ reorderedhead ++ head
- # Reordered head starts of empty and consumes entries from head
- # while keeping the localChange at the top for as long as it can
- localChange = head.shift()
- reorderedHead = [localChange]
- while head.length > 0
- remoteChange = head.shift()
- localChange = reorderedHead.pop()
- result = @_swapSimpleDeltaSetsOrder(localChange.deltaSets, remoteChange.deltaSets)
- if result?
- remoteChange.deltaSets = result[0]
- localChange.deltaSets = result[1]
- reorderedHead.push remoteChange
- reorderedHead.push localChange
- else
- reorderedHead.push localChange
- reorderedHead.push remoteChange
- break
- @undoStack = @undoStack.concat(reorderedHead).concat(head)
- return true
-
-
- _swapSimpleDeltaSetsOrder: (firstDeltaSets, secondDeltaSets) ->
- newFirstDeltaSets = @_copyDeltaSets(firstDeltaSets)
- newSecondDeltaSets = @_copyDeltaSets(secondDeltaSets)
- for firstDeltaSet in newFirstDeltaSets.slice(0).reverse()
- for firstDelta in firstDeltaSet.deltas.slice(0).reverse()
- for secondDeltaSet in newSecondDeltaSets
- for secondDelta in secondDeltaSet.deltas
- success = @_swapSimpleDeltaOrderInPlace(firstDelta, secondDelta)
- return null if !success
- return [newSecondDeltaSets, newFirstDeltaSets]
-
- _copyDeltaSets: (deltaSets) ->
- newDeltaSets = []
- for deltaSet in deltaSets
- newDeltaSet =
- deltas: []
- group: deltaSet.group
- newDeltaSets.push newDeltaSet
- for delta in deltaSet.deltas
- newDelta =
- position: delta.position
- newDelta.insert = delta.insert if delta.insert?
- newDelta.remove = delta.remove if delta.remove?
- newDeltaSet.deltas.push newDelta
- return newDeltaSets
-
- _swapSimpleDeltaOrderInPlace: (firstDelta, secondDelta) ->
- result = @_swapSimpleDeltaOrder(firstDelta, secondDelta)
- return false if !result?
- firstDelta.position = result[1].position
- secondDelta.position = result[0].position
- return true
-
- _swapSimpleDeltaOrder: (firstDelta, secondDelta) ->
- if firstDelta.insert? and secondDelta.insert?
- if secondDelta.position >= firstDelta.position + firstDelta.insert.length
- secondDelta.position -= firstDelta.insert.length
- return [secondDelta, firstDelta]
- else if secondDelta.position > firstDelta.position
- return null
- else
- firstDelta.position += secondDelta.insert.length
- return [secondDelta, firstDelta]
- else if firstDelta.remove? and secondDelta.remove?
- if secondDelta.position >= firstDelta.position
- secondDelta.position += firstDelta.remove.length
- return [secondDelta, firstDelta]
- else if secondDelta.position + secondDelta.remove.length > firstDelta.position
- return null
- else
- firstDelta.position -= secondDelta.remove.length
- return [secondDelta, firstDelta]
- else if firstDelta.insert? and secondDelta.remove?
- if secondDelta.position >= firstDelta.position + firstDelta.insert.length
- secondDelta.position -= firstDelta.insert.length
- return [secondDelta, firstDelta]
- else if secondDelta.position + secondDelta.remove.length > firstDelta.position
- return null
- else
- firstDelta.position -= secondDelta.remove.length
- return [secondDelta, firstDelta]
- else if firstDelta.remove? and secondDelta.insert?
- if secondDelta.position >= firstDelta.position
- secondDelta.position += firstDelta.remove.length
- return [secondDelta, firstDelta]
- else
- firstDelta.position += secondDelta.insert.length
- return [secondDelta, firstDelta]
- else
- throw "Unknown delta types"
-
- _applyAceDeltasToDocLines: (deltas, docLines) ->
- doc = new Doc(docLines.join("\n"))
- doc.applyDeltas(deltas)
- return doc.getAllLines()
-
- _revertAceDeltaSetsOnDocLines: (deltaSets, docLines) ->
- session = new EditSession(docLines.join("\n"))
- session.undoChanges(deltaSets)
- return session.getDocument().getAllLines()
-
- _revertSimpleDeltaSetsOnDocLines: (deltaSets, docLines) ->
- doc = docLines.join("\n")
- for deltaSet in deltaSets.slice(0).reverse()
- for delta in deltaSet.deltas.slice(0).reverse()
- if delta.remove?
- doc = doc.slice(0, delta.position) + delta.remove + doc.slice(delta.position)
- else if delta.insert?
- doc = doc.slice(0, delta.position) + doc.slice(delta.position + delta.insert.length)
- else
- throw "Unknown delta type"
- return doc.split("\n")
-
- _aceDeltaSetsToSimpleDeltaSets: (aceDeltaSets, docLines) ->
- for deltaSet in aceDeltaSets
- simpleDeltas = []
- for delta in deltaSet.deltas
- simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
- docLines = @_applyAceDeltasToDocLines([delta], docLines)
- {
- deltas: simpleDeltas
- group: deltaSet.group
- }
-
- _simpleDeltaSetsToAceDeltaSets: (simpleDeltaSets, docLines) ->
- for deltaSet in simpleDeltaSets
- aceDeltas = []
- for delta in deltaSet.deltas
- newAceDeltas = @_simpleDeltaToAceDeltas(delta, docLines)
- docLines = @_applyAceDeltasToDocLines(newAceDeltas, docLines)
- aceDeltas = aceDeltas.concat newAceDeltas
- {
- deltas: aceDeltas
- group: deltaSet.group
- }
-
- _aceDeltaToSimpleDelta: (aceDelta, docLines) ->
- start = aceDelta.range.start
- linesBefore = docLines.slice(0, start.row)
- position =
- linesBefore.join("").length + # full lines
- linesBefore.length + # new line characters
- start.column # partial line
- switch aceDelta.action
- when "insertText"
- return {
- position: position
- insert: aceDelta.text
- }
- when "insertLines"
- return {
- position: position
- insert: aceDelta.lines.join("\n") + "\n"
- }
- when "removeText"
- return {
- position: position
- remove: aceDelta.text
- }
- when "removeLines"
- return {
- position: position
- remove: aceDelta.lines.join("\n") + "\n"
- }
- else
- throw "Unknown Ace action: #{aceDelta.action}"
-
- _simplePositionToAcePosition: (position, docLines) ->
- column = 0
- row = 0
- for line in docLines
- if position > line.length
- row++
- position -= (line + "\n").length
- else
- column = position
- break
- return {row: row, column: column}
-
- _textToAceActions: (simpleText, row, column, type) ->
- aceDeltas = []
- lines = simpleText.split("\n")
-
- range = (options) -> new Range(options.start.row, options.start.column, options.end.row, options.end.column)
-
- do stripFirstLine = () ->
- firstLine = lines.shift()
- if firstLine.length > 0
- aceDeltas.push {
- text: firstLine
- range: range(
- start: column: column, row: row
- end: column: column + firstLine.length, row: row
- )
- action: "#{type}Text"
- }
- column += firstLine.length
-
- do stripFirstNewLine = () ->
- if lines.length > 0
- aceDeltas.push {
- text: "\n"
- range: range(
- start: column: column, row: row
- end: column: 0, row: row + 1
- )
- action: "#{type}Text"
- }
- row += 1
-
- do stripMiddleFullLines = () ->
- middleLines = lines.slice(0, -1)
- if middleLines.length > 0
- aceDeltas.push {
- lines: middleLines
- range: range(
- start: column: 0, row: row
- end: column: 0, row: row + middleLines.length
- )
- action: "#{type}Lines"
- }
- row += middleLines.length
-
- do stripLastLine = () ->
- if lines.length > 0
- lastLine = lines.pop()
- aceDeltas.push {
- text: lastLine
- range: range(
- start: column: 0, row: row
- end: column: lastLine.length , row: row
- )
- action: "#{type}Text"
- }
-
- return aceDeltas
-
-
- _simpleDeltaToAceDeltas: (simpleDelta, docLines) ->
- {row, column} = @_simplePositionToAcePosition(simpleDelta.position, docLines)
-
- if simpleDelta.insert?
- return @_textToAceActions(simpleDelta.insert, row, column, "insert")
- if simpleDelta.remove?
- return @_textToAceActions(simpleDelta.remove, row, column, "remove").reverse()
- else
- throw "Unknown simple delta: #{simpleDelta}"
-
- _concatSimpleDeltas: (deltas) ->
- return [] if deltas.length == 0
-
- concattedDeltas = []
- previousDelta = deltas.shift()
- for delta in deltas
- if delta.insert? and previousDelta.insert?
- if previousDelta.position + previousDelta.insert.length == delta.position
- previousDelta =
- insert: previousDelta.insert + delta.insert
- position: previousDelta.position
- else
- concattedDeltas.push previousDelta
- previousDelta = delta
-
- else if delta.remove? and previousDelta.remove?
- if previousDelta.position == delta.position
- previousDelta =
- remove: previousDelta.remove + delta.remove
- position: delta.position
- else
- concattedDeltas.push previousDelta
- previousDelta = delta
- else
- concattedDeltas.push previousDelta
- previousDelta = delta
- concattedDeltas.push previousDelta
-
-
- return concattedDeltas
-
-
- hasUndo: () -> @undoStack.length > 0
- hasRedo: () -> @redoStack.length > 0
-
diff --git a/services/web/public/coffee/app/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/app/ide/file-tree/FileTreeManager.coffee
deleted file mode 100644
index 812b5d3fb6..0000000000
--- a/services/web/public/coffee/app/ide/file-tree/FileTreeManager.coffee
+++ /dev/null
@@ -1,281 +0,0 @@
-define [
- "ide/file-tree/directives/fileEntity"
- "ide/file-tree/directives/draggable"
- "ide/file-tree/directives/droppable"
- "ide/file-tree/controllers/FileTreeController"
- "ide/file-tree/controllers/FileTreeEntityController"
- "ide/file-tree/controllers/FileTreeFolderController"
- "ide/file-tree/controllers/FileTreeRootFolderController"
-], () ->
- class FileTreeManager
- constructor: (@ide, @$scope) ->
- @$scope.$on "project:joined", =>
- @loadRootFolder()
- @loadDeletedDocs()
- @$scope.$emit "file-tree:initialized"
-
- @_bindToSocketEvents()
-
- _bindToSocketEvents: () ->
- @ide.socket.on "reciveNewDoc", (parent_folder_id, doc) =>
- parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder
- @$scope.$apply () ->
- parent_folder.children.push {
- name: doc.name
- id: doc._id
- type: "doc"
- }
-
- @ide.socket.on "reciveNewFile", (parent_folder_id, file) =>
- parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder
- @$scope.$apply () ->
- parent_folder.children.push {
- name: file.name
- id: file._id
- type: "file"
- }
-
- @ide.socket.on "reciveNewFolder", (parent_folder_id, folder) =>
- parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder
- @$scope.$apply () ->
- parent_folder.children.push {
- name: folder.name
- id: folder._id
- type: "folder"
- children: []
- }
-
- @ide.socket.on "reciveEntityRename", (entity_id, name) =>
- entity = @findEntityById(entity_id)
- return if !entity?
- @$scope.$apply () ->
- entity.name = name
-
- @ide.socket.on "removeEntity", (entity_id) =>
- entity = @findEntityById(entity_id)
- return if !entity?
- @$scope.$apply () =>
- @_deleteEntityFromScope entity
-
- @ide.socket.on "reciveEntityMove", (entity_id, folder_id) =>
- entity = @findEntityById(entity_id)
- folder = @findEntityById(folder_id)
- console.log "Got recive ENTITY", entity_id, folder_id, entity, folder
- @$scope.$apply () =>
- @_moveEntityInScope(entity, folder)
-
- selectEntity: (entity) ->
- @selected_entity_id = entity.id # For reselecting after a reconnect
- @ide.fileTreeManager.forEachEntity (entity) ->
- entity.selected = false
- entity.selected = true
-
- findSelectedEntity: () ->
- selected = null
- @forEachEntity (entity) ->
- selected = entity if entity.selected
- return selected
-
- findEntityById: (id, options = {}) ->
- return @$scope.rootFolder if @$scope.rootFolder.id == id
-
- entity = @_findEntityByIdInFolder @$scope.rootFolder, id
- return entity if entity?
-
- if options.includeDeleted
- for entity in @$scope.deletedDocs
- return entity if entity.id == id
-
- return null
-
- _findEntityByIdInFolder: (folder, id) ->
- for entity in folder.children or []
- if entity.id == id
- return entity
- else if entity.children?
- result = @_findEntityByIdInFolder(entity, id)
- return result if result?
-
- return null
-
- findEntityByPath: (path) ->
- @_findEntityByPathInFolder @$scope.rootFolder, path
-
- _findEntityByPathInFolder: (folder, path) ->
- parts = path.split("/")
- name = parts.shift()
- rest = parts.join("/")
-
- if name == "."
- return @_findEntityByPathInFolder(folder, rest)
-
- for entity in folder.children
- if entity.name == name
- if rest == ""
- return entity
- else if entity.type == "folder"
- return @_findEntityByPathInFolder(entity, rest)
- return null
-
- forEachEntity: (callback = (entity, parent_folder) ->) ->
- @_forEachEntityInFolder(@$scope.rootFolder, callback)
-
- for entity in @$scope.deletedDocs or []
- callback(entity)
-
- _forEachEntityInFolder: (folder, callback) ->
- for entity in folder.children or []
- callback(entity, folder)
- if entity.children?
- @_forEachEntityInFolder(entity, callback)
-
- getEntityPath: (entity) ->
- @_getEntityPathInFolder @$scope.rootFolder, entity
-
- _getEntityPathInFolder: (folder, entity) ->
- for child in folder.children or []
- if child == entity
- return entity.name
- else if child.type == "folder"
- path = @_getEntityPathInFolder(child, entity)
- if path?
- return child.name + "/" + path
- return null
-
- getRootDocDirname: () ->
- rootDoc = @findEntityById @$scope.project.rootDoc_id
- return if !rootDoc?
- path = @getEntityPath(rootDoc)
- return if !path?
- return path.split("/").slice(0, -1).join("/")
-
- # forEachFolder: (callback) ->
- # @forEachEntity (entity) ->
- # if entity.type == "folder"
- # callback(entity)
-
- loadRootFolder: () ->
- @$scope.rootFolder = @_parseFolder(@$scope.project.rootFolder[0])
-
- _parseFolder: (rawFolder) ->
- folder = {
- name: rawFolder.name
- id: rawFolder._id
- type: "folder"
- children: []
- selected: (rawFolder._id == @selected_entity_id)
- }
-
- for doc in rawFolder.docs or []
- folder.children.push {
- name: doc.name
- id: doc._id
- type: "doc"
- selected: (doc._id == @selected_entity_id)
- }
-
- for file in rawFolder.fileRefs or []
- folder.children.push {
- name: file.name
- id: file._id
- type: "file"
- selected: (file._id == @selected_entity_id)
- }
-
- for childFolder in rawFolder.folders or []
- folder.children.push @_parseFolder(childFolder)
-
- return folder
-
- loadDeletedDocs: () ->
- @$scope.deletedDocs = []
- for doc in @$scope.project.deletedDocs
- @$scope.deletedDocs.push {
- name: doc.name
- id: doc._id
- type: "doc"
- deleted: true
- }
-
- getCurrentFolder: () ->
- # Return the root folder if nothing is selected
- @_getCurrentFolder(@$scope.rootFolder) or @$scope.rootFolder
-
- _getCurrentFolder: (startFolder = @$scope.rootFolder) ->
- for entity in startFolder.children or []
- # The 'current' folder is either the one selected, or
- # the one containing the selected doc/file
- if entity.selected
- if entity.type == "folder"
- return entity
- else
- return startFolder
-
- if entity.type == "folder"
- result = @_getCurrentFolder(entity)
- return result if result?
-
- return null
-
- createDoc: (name, parent_folder = @getCurrentFolder()) ->
- # We'll wait for the socket.io notification to actually
- # add the doc for us.
- @ide.$http.post "/project/#{@ide.project_id}/doc", {
- name: name,
- parent_folder_id: parent_folder?.id
- _csrf: window.csrfToken
- }
-
- createFolder: (name, parent_folder = @getCurrentFolder()) ->
- # We'll wait for the socket.io notification to actually
- # add the folder for us.
- return @ide.$http.post "/project/#{@ide.project_id}/folder", {
- name: name,
- parent_folder_id: parent_folder?.id
- _csrf: window.csrfToken
- }
-
- renameEntity: (entity, name, callback = (error) ->) ->
- return if entity.name == name
- entity.name = name
- return @ide.$http.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/rename", {
- name: name,
- _csrf: window.csrfToken
- }
-
- deleteEntity: (entity, callback = (error) ->) ->
- # We'll wait for the socket.io notification to
- # delete from scope.
- return @ide.$http {
- method: "DELETE"
- url: "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}"
- headers:
- "X-Csrf-Token": window.csrfToken
- }
-
- moveEntity: (entity, parent_folder, callback = (error) ->) ->
- @_moveEntityInScope(entity, parent_folder)
- return @ide.$http.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/move", {
- folder_id: parent_folder.id
- _csrf: window.csrfToken
- }
-
- _deleteEntityFromScope: (entity, options = { moveToDeleted: true }) ->
- parent_folder = null
- @forEachEntity (possible_entity, folder) ->
- if possible_entity == entity
- parent_folder = folder
-
- if parent_folder?
- index = parent_folder.children.indexOf(entity)
- if index > -1
- parent_folder.children.splice(index, 1)
-
- if entity.type == "doc" and options.moveToDeleted
- entity.deleted = true
- @$scope.deletedDocs.push entity
-
- _moveEntityInScope: (entity, parent_folder) ->
- return if entity in parent_folder.children
- @_deleteEntityFromScope(entity, moveToDeleted: false)
- parent_folder.children.push(entity)
diff --git a/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeController.coffee b/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeController.coffee
deleted file mode 100644
index 31015229ac..0000000000
--- a/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeController.coffee
+++ /dev/null
@@ -1,113 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "FileTreeController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
- $scope.openNewDocModal = () ->
- $modal.open(
- templateUrl: "newDocModalTemplate"
- controller: "NewDocModalController"
- resolve: {
- parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
- }
- )
-
- $scope.openNewFolderModal = () ->
- $modal.open(
- templateUrl: "newFolderModalTemplate"
- controller: "NewFolderModalController"
- resolve: {
- parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
- }
- )
-
- $scope.openUploadFileModal = () ->
- $modal.open(
- templateUrl: "uploadFileModalTemplate"
- controller: "UploadFileModalController"
- scope: $scope
- resolve: {
- parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
- }
- )
-
- $scope.orderByFoldersFirst = (entity) ->
- return '0' if entity.type == "folder"
- return '1'
-
- $scope.startRenamingSelected = () ->
- $scope.$broadcast "rename:selected"
-
- $scope.openDeleteModalForSelected = () ->
- $scope.$broadcast "delete:selected"
- ]
-
- App.controller "NewDocModalController", [
- "$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
- ($scope, ide, $modalInstance, $timeout, parent_folder) ->
- $scope.inputs =
- name: "name.tex"
- $scope.state =
- inflight: false
-
- $modalInstance.opened.then () ->
- $timeout () ->
- $scope.$broadcast "open"
- , 200
-
- $scope.create = () ->
- $scope.state.inflight = true
- ide.fileTreeManager
- .createDoc($scope.inputs.name, parent_folder)
- .success () ->
- $scope.state.inflight = false
- $modalInstance.close()
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
- ]
-
- App.controller "NewFolderModalController", [
- "$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
- ($scope, ide, $modalInstance, $timeout, parent_folder) ->
- $scope.inputs =
- name: "name"
- $scope.state =
- inflight: false
-
- $modalInstance.opened.then () ->
- $timeout () ->
- $scope.$broadcast "open"
- , 200
-
- $scope.create = () ->
- $scope.state.inflight = true
- ide.fileTreeManager
- .createFolder($scope.inputs.name, parent_folder)
- .success () ->
- $scope.state.inflight = false
- $modalInstance.close()
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
- ]
-
- App.controller "UploadFileModalController", [
- "$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
- ($scope, ide, $modalInstance, $timeout, parent_folder) ->
- console.log "PArent folder", parent_folder
- $scope.parent_folder_id = parent_folder?.id
-
- uploadCount = 0
- $scope.onUpload = () ->
- uploadCount++
-
- $scope.onComplete = (error, name, response) ->
- $timeout (() ->
- uploadCount--
- if uploadCount == 0 and response? and response.success
- $modalInstance.close("done")
- ), 250
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeEntityController.coffee b/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeEntityController.coffee
deleted file mode 100644
index 7c56d1cf24..0000000000
--- a/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeEntityController.coffee
+++ /dev/null
@@ -1,49 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) ->
- $scope.select = () ->
- ide.fileTreeManager.selectEntity($scope.entity)
- $scope.$emit "entity:selected", $scope.entity
-
- $scope.inputs =
- name: $scope.entity.name
-
- $scope.startRenaming = () ->
- $scope.entity.renaming = true
-
- $scope.finishRenaming = () ->
- delete $scope.entity.renaming
- ide.fileTreeManager.renameEntity($scope.entity, $scope.inputs.name)
-
- $scope.$on "rename:selected", () ->
- $scope.startRenaming() if $scope.entity.selected
-
- $scope.openDeleteModal = () ->
- $modal.open(
- templateUrl: "deleteEntityModalTemplate"
- controller: "DeleteEntityModalController"
- scope: $scope
- )
-
- $scope.$on "delete:selected", () ->
- $scope.openDeleteModal() if $scope.entity.selected
- ]
-
- App.controller "DeleteEntityModalController", [
- "$scope", "ide", "$modalInstance",
- ($scope, ide, $modalInstance) ->
- $scope.state =
- inflight: false
-
- $scope.delete = () ->
- $scope.state.inflight = true
- ide.fileTreeManager
- .deleteEntity($scope.entity)
- .success () ->
- $scope.state.inflight = false
- $modalInstance.close()
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
- ]
diff --git a/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeFolderController.coffee b/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeFolderController.coffee
deleted file mode 100644
index 93daaae1f5..0000000000
--- a/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeFolderController.coffee
+++ /dev/null
@@ -1,51 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "FileTreeFolderController", ["$scope", "ide", "$modal", ($scope, ide, $modal) ->
- $scope.expanded = $.localStorage("folder.#{$scope.entity.id}.expanded") or false
-
- $scope.toggleExpanded = () ->
- $scope.expanded = !$scope.expanded
- $.localStorage("folder.#{$scope.entity.id}.expanded", $scope.expanded)
-
- $scope.onDrop = (events, ui) ->
- source = $(ui.draggable).scope().entity
- return if !source?
- ide.fileTreeManager.moveEntity(source, $scope.entity)
-
- $scope.orderByFoldersFirst = (entity) ->
- # We need this here as well as in FileTreeController
- # since the file-entity diretive creates a new scope
- # that doesn't inherit from previous scopes.
- return '0' if entity.type == "folder"
- return '1'
-
- $scope.openNewDocModal = () ->
- $modal.open(
- templateUrl: "newDocModalTemplate"
- controller: "NewDocModalController"
- resolve: {
- parent_folder: () -> $scope.entity
- }
- )
-
- $scope.openNewFolderModal = () ->
- $modal.open(
- templateUrl: "newFolderModalTemplate"
- controller: "NewFolderModalController"
- resolve: {
- parent_folder: () -> $scope.entity
- }
- )
-
- $scope.openUploadFileModal = () ->
- $scope.project_id = ide.project_id
- $modal.open(
- templateUrl: "uploadFileModalTemplate"
- controller: "UploadFileModalController"
- scope: $scope
- resolve: {
- parent_folder: () -> $scope.entity
- }
- )
- ]
diff --git a/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeRootFolderController.coffee b/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeRootFolderController.coffee
deleted file mode 100644
index 8e3fe5e8bb..0000000000
--- a/services/web/public/coffee/app/ide/file-tree/controllers/FileTreeRootFolderController.coffee
+++ /dev/null
@@ -1,13 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "FileTreeRootFolderController", ["$scope", "ide", ($scope, ide) ->
- console.log "CREATING FileTreeRootFolderController"
- rootFolder = $scope.rootFolder
- console.log "ROOT FOLDER", rootFolder
- $scope.onDrop = (events, ui) ->
- source = $(ui.draggable).scope().entity
- console.log "DROPPED INTO ROOT", source, rootFolder
- return if !source?
- ide.fileTreeManager.moveEntity(source, rootFolder)
- ]
diff --git a/services/web/public/coffee/app/ide/file-tree/directives/draggable.coffee b/services/web/public/coffee/app/ide/file-tree/directives/draggable.coffee
deleted file mode 100644
index 4b08a0daf6..0000000000
--- a/services/web/public/coffee/app/ide/file-tree/directives/draggable.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive "draggable", () ->
- return {
- link: (scope, element, attrs) ->
- scope.$watch attrs.draggable, (draggable) ->
- if draggable
- element.draggable
- delay: 250
- opacity: 0.7
- helper: "clone"
- scroll: true
- }
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/file-tree/directives/droppable.coffee b/services/web/public/coffee/app/ide/file-tree/directives/droppable.coffee
deleted file mode 100644
index ea68630a21..0000000000
--- a/services/web/public/coffee/app/ide/file-tree/directives/droppable.coffee
+++ /dev/null
@@ -1,17 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive "droppable", () ->
- return {
- scope: {
- onDropCallback: "="
- }
- link: (scope, element, attrs) ->
- scope.$watch attrs.droppable, (droppable) ->
- if droppable
- element.droppable
- greedy: true
- hoverClass: "droppable-hover"
- accept: attrs.accept
- drop: scope.onDropCallback
- }
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/file-tree/directives/fileEntity.coffee b/services/web/public/coffee/app/ide/file-tree/directives/fileEntity.coffee
deleted file mode 100644
index 9f3fcd2407..0000000000
--- a/services/web/public/coffee/app/ide/file-tree/directives/fileEntity.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive "fileEntity", ["RecursionHelper", (RecursionHelper) ->
- return {
- restrict: "E"
- scope: {
- entity: "="
- permissions: "="
- }
- templateUrl: "entityListItemTemplate"
- compile: (element) ->
- RecursionHelper.compile element, (scope, element, attrs, ctrl) ->
- # Don't freak out if we're already in an apply callback
- scope.$originalApply = scope.$apply
- scope.$apply = (fn = () ->) ->
- phase = @$root.$$phase
- if (phase == '$apply' || phase == '$digest')
- fn()
- else
- @$originalApply(fn);
- }
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee b/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee
deleted file mode 100644
index 5e73a6446b..0000000000
--- a/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee
+++ /dev/null
@@ -1,71 +0,0 @@
-define [
- "../../../libs/md5"
-], () ->
- class OnlineUsersManager
- constructor: (@ide, @$scope) ->
- @$scope.onlineUsers = {}
- @$scope.onlineUserCursorHighlights = {}
-
- @$scope.$watch "editor.cursorPosition", (position) =>
- if position?
- @sendCursorPositionUpdate()
-
- @ide.socket.on "clientTracking.clientUpdated", (client) =>
- if client.id != @ide.socket.socket.sessionid # Check it's not me!
- @$scope.$apply () =>
- @$scope.onlineUsers[client.id] = client
- @updateCursorHighlights()
-
- @ide.socket.on "clientTracking.clientDisconnected", (client_id) =>
- @$scope.$apply () =>
- delete @$scope.onlineUsers[client_id]
- @updateCursorHighlights()
-
- updateCursorHighlights: () ->
- console.log "UPDATING CURSOR HIGHLIGHTS"
- @$scope.onlineUserCursorHighlights = {}
- for client_id, client of @$scope.onlineUsers
- doc_id = client.doc_id
- continue if !doc_id?
- @$scope.onlineUserCursorHighlights[doc_id] ||= []
- @$scope.onlineUserCursorHighlights[doc_id].push {
- label: client.name
- cursor:
- row: client.row
- column: client.column
- hue: @getHueForUserId(client.user_id)
- }
-
- UPDATE_INTERVAL: 500
- sendCursorPositionUpdate: () ->
- console.log "SENDING CURSOR POSITION UPDATE", @$scope.editor.cursorPosition
- if !@cursorUpdateTimeout?
- @cursorUpdateTimeout = setTimeout ()=>
- position = @$scope.editor.cursorPosition
- doc_id = @$scope.editor.open_doc_id
-
- @ide.socket.emit "clientTracking.updatePosition", {
- row: position.row
- column: position.column
- doc_id: doc_id
- }
-
- delete @cursorUpdateTimeout
- , @UPDATE_INTERVAL
-
- OWN_HUE: 200 # We will always appear as this color to ourselves
- ANONYMOUS_HUE: 100
- getHueForUserId: (user_id) ->
- if !user_id? or user_id == "anonymous-user"
- return @ANONYMOUS_HUE
-
- if window.user.id == user_id
- return @OWN_HUE
-
- hash = CryptoJS.MD5(user_id)
- hue = parseInt(hash.toString().slice(0,8), 16) % 320
- # Avoid 20 degrees either side of the personal hue
- if hue > @OWNER_HUE - 20
- hue = hue + 40
- return hue
-
diff --git a/services/web/public/coffee/app/ide/pdf/PdfManager.coffee b/services/web/public/coffee/app/ide/pdf/PdfManager.coffee
deleted file mode 100644
index 47ac42a1b6..0000000000
--- a/services/web/public/coffee/app/ide/pdf/PdfManager.coffee
+++ /dev/null
@@ -1,20 +0,0 @@
-define [
- "ide/pdf/controllers/PdfController"
- "ide/pdf/directives/pdfJs"
-], () ->
- class PdfManager
- constructor: (@ide, @$scope) ->
- @$scope.pdf =
- url: null # Pdf Url
- error: false # Server error
- timeout: false # Server timed out
- failure: false # PDF failed to compile
- compiling: false
- uncompiled: true
- logEntries: []
- logEntryAnnotations: {}
- rawLog: ""
- view: null # 'pdf' 'logs'
- showRawLog: false
- highlights: []
- position: null
diff --git a/services/web/public/coffee/app/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/app/ide/pdf/controllers/PdfController.coffee
deleted file mode 100644
index 4fa3829f0d..0000000000
--- a/services/web/public/coffee/app/ide/pdf/controllers/PdfController.coffee
+++ /dev/null
@@ -1,277 +0,0 @@
-define [
- "base"
- "libs/latex-log-parser"
-], (App, LogParser) ->
- App.controller "PdfController", ["$scope", "$http", "ide", "$modal", "synctex", ($scope, $http, ide, $modal, synctex) ->
- autoCompile = true
- $scope.$on "doc:opened", () ->
- return if !autoCompile
- autoCompile = false
- $scope.recompile(isAutoCompile: true)
-
- sendCompileRequest = (options = {}) ->
- url = "/project/#{$scope.project_id}/compile"
- if options.isAutoCompile
- url += "?auto_compile=true"
- return $http.post url, {
- settingsOverride:
- rootDoc_id: options.rootDocOverride_id or null
- _csrf: window.csrfToken
- }
-
- parseCompileResponse = (response) ->
- # Reset everything
- $scope.pdf.error = false
- $scope.pdf.timedout = false
- $scope.pdf.failure = false
- $scope.pdf.uncompiled = false
- $scope.pdf.url = null
-
- if response.status == "timedout"
- $scope.pdf.timedout = true
- else if response.status == "autocompile-backoff"
- $scope.pdf.uncompiled = true
- else if response.status == "failure"
- $scope.pdf.failure = true
- fetchLogs()
- else if response.status == "success"
- $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}"
- fetchLogs()
-
- IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
- $scope.pdf.outputFiles = []
- for file in response.outputFiles
- if IGNORE_FILES.indexOf(file.path) == -1
- # Turn 'output.blg' into 'blg file'.
- if file.path.match(/^output\./)
- file.name = "#{file.path.replace(/^output\./, "")} file"
- else
- file.name = file.path
- $scope.pdf.outputFiles.push file
-
- fetchLogs = () ->
- $http.get "/project/#{$scope.project_id}/output/output.log"
- .success (log) ->
- $scope.pdf.rawLog = log
- logEntries = LogParser.parse(log, ignoreDuplicates: true)
- $scope.pdf.logEntries = logEntries
- $scope.pdf.logEntries.all = logEntries.errors.concat(logEntries.warnings).concat(logEntries.typesetting)
-
- $scope.pdf.logEntryAnnotations = {}
- for entry in logEntries.all
- entry.file = normalizeFilePath(entry.file)
-
- entity = ide.fileTreeManager.findEntityByPath(entry.file)
- if entity?
- $scope.pdf.logEntryAnnotations[entity.id] ||= []
- $scope.pdf.logEntryAnnotations[entity.id].push {
- row: entry.line - 1
- type: if entry.level == "error" then "error" else "warning"
- text: entry.message
- }
-
- .error () ->
- $scope.pdf.logEntries = []
- $scope.pdf.rawLog = ""
-
- getRootDocOverride_id = () ->
- doc = ide.editorManager.getCurrentDocValue()
- return null if !doc?
- for line in doc.split("\n")
- match = line.match /(.*)\\documentclass/
- if match and !match[1].match /%/
- return ide.editorManager.getCurrentDocId()
- return null
-
- normalizeFilePath = (path) ->
- path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
- path = path.replace(/^\/compile\//, "")
-
- rootDocDirname = ide.fileTreeManager.getRootDocDirname()
- if rootDocDirname?
- path = path.replace(/^\.\//, rootDocDirname + "/")
-
- return path
-
- $scope.recompile = (options = {}) ->
- console.log "Recompiling", options
- return if $scope.pdf.compiling
- $scope.pdf.compiling = true
-
- options.rootDocOverride_id = getRootDocOverride_id()
-
- sendCompileRequest(options)
- .success (data) ->
- $scope.pdf.view = "pdf"
- $scope.pdf.compiling = false
- parseCompileResponse(data)
- .error () ->
- $scope.pdf.compiling = false
- $scope.pdf.error = true
-
- $scope.clearCache = () ->
- $http {
- url: "/project/#{$scope.project_id}/output"
- method: "DELETE"
- headers:
- "X-Csrf-Token": window.csrfToken
- }
-
- $scope.toggleLogs = () ->
- if !$scope.pdf.view? or $scope.pdf.view == "pdf"
- $scope.pdf.view = "logs"
- else
- $scope.pdf.view = "pdf"
-
- $scope.showPdf = () ->
- $scope.pdf.view = "pdf"
-
- $scope.toggleRawLog = () ->
- $scope.pdf.showRawLog = !$scope.pdf.showRawLog
-
- $scope.openOutputFile = (file) ->
- window.open("/project/#{$scope.project_id}/output/#{file.path}")
-
- $scope.openClearCacheModal = () ->
- modalInstance = $modal.open(
- templateUrl: "clearCacheModalTemplate"
- controller: "ClearCacheModalController"
- scope: $scope
- )
-
- $scope.syncToCode = (position) ->
- console.log "SYNCING VIA DBL CLICK", position
- synctex
- .syncToCode(position)
- .then (data) ->
- {doc, line} = data
- ide.editorManager.openDoc(doc, gotoLine: line)
-
- ]
-
- App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
- synctex =
- syncToPdf: (cursorPosition) ->
- deferred = $q.defer()
-
- doc_id = ide.editorManager.getCurrentDocId()
- if !doc_id?
- deferred.reject()
- return deferred.promise
- doc = ide.fileTreeManager.findEntityById(doc_id)
- if !doc?
- deferred.reject()
- return deferred.promise
- path = ide.fileTreeManager.getEntityPath(doc)
- if !path?
- deferred.reject()
- return deferred.promise
-
- # If the root file is folder/main.tex, then synctex sees the
- # path as folder/./main.tex
- rootDocDirname = ide.fileTreeManager.getRootDocDirname()
- if rootDocDirname? and rootDocDirname != ""
- path = path.replace(RegExp("^#{rootDocDirname}"), "#{rootDocDirname}/.")
-
- {row, column} = cursorPosition
-
- $http({
- url: "/project/#{ide.project_id}/sync/code",
- method: "GET",
- params: {
- file: path
- line: row + 1
- column: column
- }
- })
- .success (data) ->
- deferred.resolve(data.pdf or [])
- .error (error) ->
- deferred.reject(error)
-
- return deferred.promise
-
- syncToCode: (position, options = {}) ->
- deferred = $q.defer()
- if !position?
- deferred.reject()
- return deferred.promise
-
- # It's not clear exactly where we should sync to if it wasn't directly
- # clicked on, but a little bit down from the very top seems best.
- if options.includeVisualOffset
- position.offset.top = position.offset.top + 80
-
- $http({
- url: "/project/#{ide.project_id}/sync/pdf",
- method: "GET",
- params: {
- page: position.page + 1
- h: position.offset.left.toFixed(2)
- v: position.offset.top.toFixed(2)
- }
- })
- .success (data) ->
- if data.code? and data.code.length > 0
- doc = ide.fileTreeManager.findEntityByPath(data.code[0].file)
- return if !doc?
- deferred.resolve({doc: doc, line: data.code[0].line})
- .error (error) ->
- deferred.reject(error)
-
- return deferred.promise
-
- return synctex
- ]
-
- App.controller "PdfSynctexController", ["$scope", "synctex", "ide", ($scope, synctex, ide) ->
- $scope.showControls = true
- $scope.$on "layout:pdf:resize", (event, data) ->
- if data.east.initClosed
- $scope.showControls = false
- else
- $scope.showControls = true
- setTimeout () ->
- $scope.$digest()
- , 0
-
- $scope.syncToPdf = () ->
- synctex
- .syncToPdf($scope.editor.cursorPosition)
- .then (highlights) ->
- $scope.pdf.highlights = highlights
-
- $scope.syncToCode = () ->
- synctex
- .syncToCode($scope.pdf.position, includeVisualOffset: true)
- .then (data) ->
- {doc, line} = data
- console.log "OPENING DOC", doc, line
- ide.editorManager.openDoc(doc, gotoLine: line)
- ]
-
- App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) ->
- $scope.openInEditor = (entry) ->
- console.log "OPENING", entry.file, entry.line
- entity = ide.fileTreeManager.findEntityByPath(entry.file)
- return if !entity? or entity.type != "doc"
- if entry.line?
- line = entry.line
- ide.editorManager.openDoc(entity, gotoLine: line)
- ]
-
- App.controller 'ClearCacheModalController', ["$scope", "$modalInstance", ($scope, $modalInstance) ->
- $scope.state =
- inflight: false
-
- $scope.clear = () ->
- $scope.state.inflight = true
- $scope
- .clearCache()
- .then () ->
- $scope.state.inflight = false
- $modalInstance.close()
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/pdf/directives/pdfJs.coffee b/services/web/public/coffee/app/ide/pdf/directives/pdfJs.coffee
deleted file mode 100644
index b5ef69c4e3..0000000000
--- a/services/web/public/coffee/app/ide/pdf/directives/pdfJs.coffee
+++ /dev/null
@@ -1,196 +0,0 @@
-define [
- "base"
- "libs/pdfListView/PdfListView"
- "libs/pdfListView/TextLayerBuilder"
- "libs/pdfListView/AnnotationsLayerBuilder"
- "libs/pdfListView/HighlightsLayerBuilder"
- "text!libs/pdfListView/TextLayer.css"
- "text!libs/pdfListView/AnnotationsLayer.css"
- "text!libs/pdfListView/HighlightsLayer.css"
-], (
- App
- PDFListView
- TextLayerBuilder
- AnnotationsLayerBuilder
- HighlightsLayerBuilder
- textLayerCss
- annotationsLayerCss
- highlightsLayerCss
-) ->
- if PDFJS?
- PDFJS.workerSrc = "#{window.sharelatex.pdfJsWorkerPath}"
-
- style = $("")
- style.text(textLayerCss + "\n" + annotationsLayerCss + "\n" + highlightsLayerCss)
- $("body").append(style)
-
- App.directive "pdfjs", ["$timeout", ($timeout) ->
- return {
- scope: {
- "pdfSrc": "="
- "highlights": "="
- "position": "="
- "dblClickCallback": "="
- }
- link: (scope, element, attrs) ->
- pdfListView = new PDFListView element.find(".pdfjs-viewer")[0],
- textLayerBuilder: TextLayerBuilder
- annotationsLayerBuilder: AnnotationsLayerBuilder
- highlightsLayerBuilder: HighlightsLayerBuilder
- ondblclick: (e) -> onDoubleClick(e)
- logLevel: PDFListView.Logger.DEBUG
- pdfListView.listView.pageWidthOffset = 20
- pdfListView.listView.pageHeightOffset = 20
-
- scope.loading = false
-
- onProgress = (progress) ->
- scope.$apply () ->
- scope.progress = Math.floor(progress.loaded/progress.total*100)
- console.log "PROGRESS", scope.progress, progress.loaded, progress.total
-
- initializedPosition = false
- initializePosition = () ->
- return if initializedPosition
- initializedPosition = true
-
- if (scale = $.localStorage("pdf.scale"))?
- pdfListView.setScaleMode(scale.scaleMode, scale.scale)
- else
- pdfListView.setToFitWidth()
-
- if (position = $.localStorage("pdf.position.#{attrs.key}"))
- pdfListView.setPdfPosition(position)
-
- scope.position = pdfListView.getPdfPosition(true)
-
- $(window).unload () =>
- $.localStorage "pdf.scale", {
- scaleMode: pdfListView.getScaleMode()
- scale: pdfListView.getScale()
- }
- $.localStorage "pdf.position.#{attrs.key}", pdfListView.getPdfPosition()
-
- flashControls = () ->
- scope.flashControls = true
- $timeout () ->
- scope.flashControls = false
- , 1000
-
- element.find(".pdfjs-viewer").scroll () ->
- console.log "UPDATING POSITION", pdfListView.getPdfPosition(true)
- scope.position = pdfListView.getPdfPosition(true)
-
- onDoubleClick = (e) ->
- scope.dblClickCallback?(page: e.page, offset: { top: e.y, left: e.x })
-
- scope.$watch "pdfSrc", (url) ->
- if url
- scope.loading = true
- scope.progress = 0
-
- pdfListView
- .loadPdf(url, onProgress)
- .then () ->
- scope.$apply () ->
- scope.loading = false
- delete scope.progress
- initializePosition()
- flashControls()
-
- scope.$watch "highlights", (areas) ->
- console.log "UPDATING HIGHLIGHTS", areas
- return if !areas?
- highlights = for area in areas or []
- {
- page: area.page - 1
- highlight:
- left: area.h
- top: area.v
- height: area.height
- width: area.width
- }
-
- if highlights.length > 0
- first = highlights[0]
- pdfListView.setPdfPosition({
- page: first.page
- offset:
- left: first.highlight.left
- top: first.highlight.top - 80
- }, true)
-
- pdfListView.clearHighlights()
- pdfListView.setHighlights(highlights, true)
-
- setTimeout () =>
- pdfListView.clearHighlights()
- , 1000
-
- scope.fitToHeight = () ->
- pdfListView.setToFitHeight()
-
- scope.fitToWidth = () ->
- pdfListView.setToFitWidth()
-
- scope.zoomIn = () ->
- scale = pdfListView.getScale()
- pdfListView.setScale(scale * 1.2)
-
- scope.zoomOut = () ->
- scale = pdfListView.getScale()
- pdfListView.setScale(scale / 1.2)
-
- if attrs.resizeOn?
- for event in attrs.resizeOn.split(",")
- scope.$on event, () ->
- pdfListView.onResize()
-
- template: """
-
-
-
- """
- }
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/permissions/PermissionsManager.coffee b/services/web/public/coffee/app/ide/permissions/PermissionsManager.coffee
deleted file mode 100644
index b52ab933e4..0000000000
--- a/services/web/public/coffee/app/ide/permissions/PermissionsManager.coffee
+++ /dev/null
@@ -1,19 +0,0 @@
-define [], () ->
- class PermissionsManager
- constructor: (@ide, @$scope) ->
- @$scope.$watch "permissionsLevel", (permissionsLevel) =>
- @$scope.permissions =
- read: false
- write: false
- admin: false
- if permissionsLevel?
- if permissionsLevel == "readOnly"
- @$scope.permissions.read = true
- else if permissionsLevel == "readAndWrite"
- @$scope.permissions.read = true
- @$scope.permissions.write = true
- else if permissionsLevel == "owner"
- @$scope.permissions.read = true
- @$scope.permissions.write = true
- @$scope.permissions.admin = true
-
diff --git a/services/web/public/coffee/app/ide/services/ide.coffee b/services/web/public/coffee/app/ide/services/ide.coffee
deleted file mode 100644
index a0d724dbfd..0000000000
--- a/services/web/public/coffee/app/ide/services/ide.coffee
+++ /dev/null
@@ -1,37 +0,0 @@
-define [
- "base"
-], (App) ->
- # We create and provide this as service so that we can access the global ide
- # from within other parts of the angular app.
- App.factory "ide", ["$http", "$modal", ($http, $modal) ->
- ide = {}
- ide.$http = $http
-
- ide.pushEvent = () ->
- #console.log "PUSHING EVENT STUB", arguments
-
- ide.reportError = () ->
- console.log "REPORTING ERROR STUB", arguments
-
- ide.showGenericServerErrorMessage = () ->
- console.error "GENERIC SERVER ERROR MESSAGE STUB"
-
- ide.showGenericMessageModal = (title, message) ->
- $modal.open {
- templateUrl: "genericMessageModalTemplate"
- controller: "GenericMessageModalController"
- resolve:
- title: -> title
- message: -> message
- }
-
- return ide
- ]
-
- App.controller "GenericMessageModalController", ["$scope", "$modalInstance", "title", "message", ($scope, $modalInstance, title, message) ->
- $scope.title = title
- $scope.message = message
-
- $scope.done = () ->
- $modalInstance.close()
- ]
diff --git a/services/web/public/coffee/app/ide/settings/controllers/ProjectNameController.coffee b/services/web/public/coffee/app/ide/settings/controllers/ProjectNameController.coffee
deleted file mode 100644
index 2285f8f029..0000000000
--- a/services/web/public/coffee/app/ide/settings/controllers/ProjectNameController.coffee
+++ /dev/null
@@ -1,26 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "ProjectNameController", ["$scope", "settings", "ide", ($scope, settings, ide) ->
- $scope.state =
- renaming: false
- $scope.inputs = {}
-
- $scope.startRenaming = () ->
- $scope.inputs.name = $scope.project.name
- $scope.state.renaming = true
- $scope.$emit "project:rename:start"
-
- $scope.finishRenaming = () ->
- $scope.project.name = $scope.inputs.name
- settings.saveProjectSettings({name: $scope.inputs.name})
- $scope.state.renaming = false
-
- ide.socket.on "projectNameUpdated", (name) ->
- $scope.$apply () ->
- $scope.project.name = name
-
- $scope.$watch "project.name", (name) ->
- if name?
- window.document.title = name + " - Online LaTeX Editor ShareLaTeX"
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/settings/controllers/SettingsController.coffee b/services/web/public/coffee/app/ide/settings/controllers/SettingsController.coffee
deleted file mode 100644
index 2cf5cc4690..0000000000
--- a/services/web/public/coffee/app/ide/settings/controllers/SettingsController.coffee
+++ /dev/null
@@ -1,51 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "SettingsController", ["$scope", "settings", "ide", ($scope, settings, ide) ->
- if $scope.settings.mode not in ["default", "vim", "emacs"]
- $scope.settings.mode = "default"
-
- $scope.$watch "settings.theme", (theme, oldTheme) =>
- if theme != oldTheme
- settings.saveSettings({theme: theme})
-
- $scope.$watch "settings.fontSize", (fontSize, oldFontSize) =>
- if fontSize != oldFontSize
- settings.saveSettings({fontSize: parseInt(fontSize, 10)})
-
- $scope.$watch "settings.mode", (mode, oldMode) =>
- if mode != oldMode
- settings.saveSettings({mode: mode})
-
- $scope.$watch "settings.autoComplete", (autoComplete, oldAutoComplete) =>
- if autoComplete != oldAutoComplete
- settings.saveSettings({autoComplete: autoComplete})
-
- $scope.$watch "settings.pdfViewer", (pdfViewer, oldPdfViewer) =>
- if pdfViewer != oldPdfViewer
- settings.saveSettings({pdfViewer: pdfViewer})
-
- $scope.$watch "project.spellCheckLanguage", (language, oldLanguage) =>
- return if @ignoreUpdates
- if oldLanguage? and language != oldLanguage
- settings.saveProjectSettings({spellCheckLanguage: language})
- # Also set it as the default for the user
- settings.saveSettings({spellCheckLanguage: language})
-
- $scope.$watch "project.compiler", (compiler, oldCompiler) =>
- return if @ignoreUpdates
- if oldCompiler? and compiler != oldCompiler
- settings.saveProjectSettings({compiler: compiler})
-
- ide.socket.on "compilerUpdated", (compiler) =>
- @ignoreUpdates = true
- $scope.$apply () =>
- $scope.project.compiler = compiler
- delete @ignoreUpdates
-
- ide.socket.on "spellCheckLanguageUpdated", (languageCode) =>
- @ignoreUpdates = true
- $scope.$apply () =>
- $scope.project.spellCheckLanguage = languageCode
- delete @ignoreUpdates
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/settings/index.coffee b/services/web/public/coffee/app/ide/settings/index.coffee
deleted file mode 100644
index 8ed64432d1..0000000000
--- a/services/web/public/coffee/app/ide/settings/index.coffee
+++ /dev/null
@@ -1,7 +0,0 @@
-define [
- "ide/settings/services/settings"
- "ide/settings/controllers/SettingsController"
- "ide/settings/controllers/ProjectNameController"
-
-], () ->
-
diff --git a/services/web/public/coffee/app/ide/settings/services/settings.coffee b/services/web/public/coffee/app/ide/settings/services/settings.coffee
deleted file mode 100644
index 78dda105ad..0000000000
--- a/services/web/public/coffee/app/ide/settings/services/settings.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-define [
- "base"
-], (App) ->
- App.factory "settings", ["ide", (ide) ->
- return {
- saveSettings: (data) ->
- data._csrf = window.csrfToken
- ide.$http.post "/user/settings", data
-
- saveProjectSettings: (data) ->
- data._csrf = window.csrfToken
- ide.$http.post "/project/#{ide.project_id}/settings", data
- }
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/app/ide/share/controllers/ShareController.coffee
deleted file mode 100644
index d76bd07965..0000000000
--- a/services/web/public/coffee/app/ide/share/controllers/ShareController.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "ShareController", ["$scope", "$modal", ($scope, $modal) ->
- $scope.openShareProjectModal = () ->
- $modal.open(
- templateUrl: "shareProjectModalTemplate"
- controller: "ShareProjectModalController"
- scope: $scope
- )
- ]
diff --git a/services/web/public/coffee/app/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/app/ide/share/controllers/ShareProjectModalController.coffee
deleted file mode 100644
index 6de4dae037..0000000000
--- a/services/web/public/coffee/app/ide/share/controllers/ShareProjectModalController.coffee
+++ /dev/null
@@ -1,104 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "ShareProjectModalController", ["$scope", "$modalInstance", "$timeout", "projectMembers", "$modal", ($scope, $modalInstance, $timeout, projectMembers, $modal) ->
- $scope.inputs = {
- privileges: "readAndWrite"
- email: ""
- }
- $scope.state = {
- error: null
- inflight: false
- startedFreeTrial: false
- }
-
- $modalInstance.opened.then () ->
- $timeout () ->
- $scope.$broadcast "open"
- , 200
-
- INFINITE_COLLABORATORS = -1
- $scope.$watch "project.members.length", (noOfMembers) ->
- allowedNoOfMembers = $scope.project.features.collaborators
- $scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS
-
- $scope.addMember = () ->
- console.log "EMAIL", $scope.inputs.email
- return if !$scope.inputs.email? or $scope.inputs.email == ""
- $scope.state.error = null
- $scope.state.inflight = true
- projectMembers
- .addMember($scope.inputs.email, $scope.inputs.privileges)
- .then (user) ->
- $scope.state.inflight = false
- $scope.inputs.email = ""
- console.log "GOT USER", user
- $scope.project.members.push user
- .catch () ->
- $scope.state.inflight = false
- $scope.state.error = "Sorry, something went wrong :("
-
-
- $scope.removeMember = (member) ->
- $scope.state.error = null
- $scope.state.inflight = true
- projectMembers
- .removeMember(member)
- .then () ->
- $scope.state.inflight = false
- index = $scope.project.members.indexOf(member)
- return if index == -1
- $scope.project.members.splice(index, 1)
- .catch () ->
- $scope.state.inflight = false
- $scope.state.error = "Sorry, something went wrong :("
-
- $scope.startFreeTrial = () ->
- ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "projectMembers")
- window.open("/user/subscription/plans")
- $scope.state.startedFreeTrial = true
-
- $scope.openMakePublicModal = () ->
- $modal.open {
- templateUrl: "makePublicModalTemplate"
- controller: "MakePublicModalController"
- scope: $scope
- }
-
- $scope.openMakePrivateModal = () ->
- $modal.open {
- templateUrl: "makePrivateModalTemplate"
- controller: "MakePrivateModalController"
- scope: $scope
- }
-
- $scope.done = () ->
- $modalInstance.close()
-
- $scope.cancel = () ->
- $modalInstance.dismiss()
- ]
-
- App.controller "MakePublicModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) ->
- $scope.inputs = {
- privileges: "readAndWrite"
- }
-
- $scope.makePublic = () ->
- $scope.project.publicAccesLevel = $scope.inputs.privileges
- settings.saveProjectSettings({publicAccessLevel: $scope.inputs.privileges})
- $modalInstance.close()
-
- $scope.cancel = () ->
- $modalInstance.dismiss()
- ]
-
- App.controller "MakePrivateModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) ->
- $scope.makePrivate = () ->
- $scope.project.publicAccesLevel = "private"
- settings.saveProjectSettings({publicAccessLevel: "private"})
- $modalInstance.close()
-
- $scope.cancel = () ->
- $modalInstance.dismiss()
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/share/index.coffee b/services/web/public/coffee/app/ide/share/index.coffee
deleted file mode 100644
index 545a145be3..0000000000
--- a/services/web/public/coffee/app/ide/share/index.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-define [
- "ide/share/controllers/ShareController"
- "ide/share/controllers/ShareProjectModalController"
- "ide/share/services/projectMembers"
-], () ->
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/share/services/projectMembers.coffee b/services/web/public/coffee/app/ide/share/services/projectMembers.coffee
deleted file mode 100644
index 144a6c2684..0000000000
--- a/services/web/public/coffee/app/ide/share/services/projectMembers.coffee
+++ /dev/null
@@ -1,30 +0,0 @@
-define [
- "base"
-], (App) ->
- App.factory "projectMembers", ["ide", "$q", (ide, $q) ->
- return {
- removeMember: (member) ->
- deferred = $q.defer()
-
- ide.socket.emit "removeUserFromProject", member._id, (error) =>
- if error?
- return deferred.reject(error)
- deferred.resolve()
-
- return deferred.promise
-
- addMember: (email, privileges) ->
- deferred = $q.defer()
-
- ide.socket.emit "addUserToProject", email, privileges, (error, user) =>
- if error?
- return deferred.reject(error)
-
- if !user
- deferred.reject()
- else
- deferred.resolve(user)
-
- return deferred.promise
- }
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/app/ide/track-changes/TrackChangesManager.coffee
deleted file mode 100644
index cdc425c356..0000000000
--- a/services/web/public/coffee/app/ide/track-changes/TrackChangesManager.coffee
+++ /dev/null
@@ -1,256 +0,0 @@
-define [
- "ide/track-changes/controllers/TrackChangesListController"
- "ide/track-changes/controllers/TrackChangesDiffController"
- "ide/track-changes/directives/infiniteScroll"
-], () ->
- class TrackChangesManager
- constructor: (@ide, @$scope) ->
- @reset()
-
- @$scope.toggleTrackChanges = () =>
- if @$scope.ui.view == "track-changes"
- @hide()
- else
- @show()
-
- @$scope.$watch "trackChanges.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
- @reloadDiff()
-
- show: () ->
- @$scope.ui.view = "track-changes"
- @reset()
-
- hide: () ->
- @$scope.ui.view = "editor"
- # Make sure we run the 'open' logic for whatever is currently selected
- @$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity()
-
- reset: () ->
- @$scope.trackChanges = {
- updates: []
- nextBeforeTimestamp: null
- atEnd: false
- selection: {
- updates: []
- doc: null
- range: {
- fromV: null
- toV: null
- start_ts: null
- end_ts: null
- }
- }
- diff: null
- }
-
- autoSelectRecentUpdates: () ->
- console.log "AUTO SELECTING UPDATES", @$scope.trackChanges.updates.length
- return if @$scope.trackChanges.updates.length == 0
-
- @$scope.trackChanges.updates[0].selectedTo = true
-
- indexOfLastUpdateNotByMe = 0
- for update, i in @$scope.trackChanges.updates
- if @_updateContainsUserId(update, @$scope.user.id)
- break
- indexOfLastUpdateNotByMe = i
-
- @$scope.trackChanges.updates[indexOfLastUpdateNotByMe].selectedFrom = true
-
- BATCH_SIZE: 4
- fetchNextBatchOfUpdates: () ->
- url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
- if @$scope.trackChanges.nextBeforeTimestamp?
- url += "&before=#{@$scope.trackChanges.nextBeforeTimestamp}"
- @$scope.trackChanges.loading = true
- @ide.$http
- .get(url)
- .success (data) =>
- @_loadUpdates(data.updates)
- @$scope.trackChanges.nextBeforeTimestamp = data.nextBeforeTimestamp
- if !data.nextBeforeTimestamp?
- @$scope.trackChanges.atEnd = true
- @$scope.trackChanges.loading = false
-
- reloadDiff: () ->
- diff = @$scope.trackChanges.diff
- {updates, doc} = @$scope.trackChanges.selection
- {fromV, toV} = @_calculateRangeFromSelection()
-
- console.log "Checking if diff has changed", doc?.id, fromV, toV, updates
-
- return if !doc?
-
- return if diff? and
- diff.doc == doc and
- diff.fromV == fromV and
- diff.toV == toV
-
- console.log "Loading diff", fromV, toV, doc?.id
-
- @$scope.trackChanges.diff = diff = {
- fromV: fromV
- toV: toV
- doc: doc
- error: false
- }
-
- if !doc.deleted
- diff.loading = true
- url = "/project/#{@$scope.project_id}/doc/#{diff.doc.id}/diff"
- if diff.fromV? and diff.toV?
- url += "?from=#{diff.fromV}&to=#{diff.toV}"
-
- @ide.$http
- .get(url)
- .success (data) =>
- diff.loading = false
- {text, highlights} = @_parseDiff(data)
- diff.text = text
- diff.highlights = highlights
- .error () ->
- diff.loading = false
- diff.error = true
- else
- diff.deleted = true
- console.log "DOC IS DELETED - NO DIFF FOR YOU!"
-
- restoreDeletedDoc: (doc) ->
- @ide.$http.post "/project/#{@$scope.project_id}/doc/#{doc.id}/restore", {
- name: doc.name
- _csrf: window.csrfToken
- }
-
- _parseDiff: (diff) ->
- row = 0
- column = 0
- highlights = []
- text = ""
- for entry, i in diff.diff or []
- content = entry.u or entry.i or entry.d
- content ||= ""
- text += content
- lines = content.split("\n")
- startRow = row
- startColumn = column
- if lines.length > 1
- endRow = startRow + lines.length - 1
- endColumn = lines[lines.length - 1].length
- else
- endRow = startRow
- endColumn = startColumn + lines[0].length
- row = endRow
- column = endColumn
-
- range = {
- start:
- row: startRow
- column: startColumn
- end:
- row: endRow
- column: endColumn
- }
-
- if entry.i? or entry.d?
- if entry.meta.user?
- name = "#{entry.meta.user.first_name} #{entry.meta.user.last_name}"
- else
- name = "Anonymous"
- if entry.meta.user?.id == @$scope.user.id
- name = "you"
- date = moment(entry.meta.end_ts).format("Do MMM YYYY, h:mm a")
- if entry.i?
- highlights.push {
- label: "Added by #{name} on #{date}"
- highlight: range
- hue: @ide.onlineUsersManager.getHueForUserId(entry.meta.user?.id)
- }
- else if entry.d?
- highlights.push {
- label: "Deleted by #{name} on #{date}"
- strikeThrough: range
- hue: @ide.onlineUsersManager.getHueForUserId(entry.meta.user?.id)
- }
-
- return {text, highlights}
-
- _loadUpdates: (updates = []) ->
- previousUpdate = @$scope.trackChanges.updates[@$scope.trackChanges.updates.length - 1]
-
- for update in updates
- for doc_id, doc of update.docs or {}
- doc.entity = @ide.fileTreeManager.findEntityById(doc_id, includeDeleted: true)
-
- for user in update.meta.users or []
- user.hue = @ide.onlineUsersManager.getHueForUserId(user.id)
-
- if !previousUpdate? or !moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, "day")
- update.meta.first_in_day = true
-
- update.selectedFrom = false
- update.selectedTo = false
- update.inSelection = false
-
- previousUpdate = update
-
- firstLoad = @$scope.trackChanges.updates.length == 0
-
- @$scope.trackChanges.updates =
- @$scope.trackChanges.updates.concat(updates)
-
- @autoSelectRecentUpdates() if firstLoad
-
- _calculateRangeFromSelection: () ->
- fromV = toV = start_ts = end_ts = null
-
- selected_doc_id = @$scope.trackChanges.selection.doc?.id
-
- for update in @$scope.trackChanges.selection.updates or []
- for doc_id, doc of update.docs
- if doc_id == selected_doc_id
- if fromV? and toV?
- fromV = Math.min(fromV, doc.fromV)
- toV = Math.max(toV, doc.toV)
- start_ts = Math.min(start_ts, update.meta.start_ts)
- end_ts = Math.max(end_ts, update.meta.end_ts)
- else
- fromV = doc.fromV
- toV = doc.toV
- start_ts = update.meta.start_ts
- end_ts = update.meta.end_ts
- break
-
- return {fromV, toV, start_ts, end_ts}
-
- # Set the track changes selected doc to one of the docs in the range
- # of currently selected updates. If we already have a selected doc
- # then prefer this one if present.
- _selectDocFromUpdates: () ->
- affected_docs = {}
- for update in @$scope.trackChanges.selection.updates
- for doc_id, doc of update.docs
- affected_docs[doc_id] = doc.entity
-
- selected_doc = @$scope.trackChanges.selection.doc
- if selected_doc? and affected_docs[selected_doc.id]?
- console.log "An affected doc is already open, bravo!"
- else
- console.log "selected doc is not open, selecting first one"
- for doc_id, doc of affected_docs
- selected_doc = doc
- break
-
- @$scope.trackChanges.selection.doc = selected_doc
- @ide.fileTreeManager.selectEntity(selected_doc)
-
- _updateContainsUserId: (update, user_id) ->
- for user in update.meta.users
- return true if user.id == user_id
- return false
diff --git a/services/web/public/coffee/app/ide/track-changes/controllers/TrackChangesDiffController.coffee b/services/web/public/coffee/app/ide/track-changes/controllers/TrackChangesDiffController.coffee
deleted file mode 100644
index 926c0d7aae..0000000000
--- a/services/web/public/coffee/app/ide/track-changes/controllers/TrackChangesDiffController.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "TrackChangesDiffController", ["$scope", "ide", ($scope, ide) ->
- $scope.restoreDeletedDoc = () ->
- ide.trackChangesManager.restoreDeletedDoc(
- $scope.trackChanges.diff.doc
- )
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/track-changes/controllers/TrackChangesListController.coffee b/services/web/public/coffee/app/ide/track-changes/controllers/TrackChangesListController.coffee
deleted file mode 100644
index 71b8497824..0000000000
--- a/services/web/public/coffee/app/ide/track-changes/controllers/TrackChangesListController.coffee
+++ /dev/null
@@ -1,109 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "TrackChangesListController", ["$scope", "ide", ($scope, ide) ->
- $scope.hoveringOverListSelectors = false
-
- $scope.loadMore = () =>
- ide.trackChangesManager.fetchNextBatchOfUpdates()
-
- $scope.recalculateSelectedUpdates = () ->
- console.log "RECALCULATING UPDATES"
- beforeSelection = true
- afterSelection = false
- $scope.trackChanges.selection.updates = []
- for update in $scope.trackChanges.updates
- if update.selectedTo
- inSelection = true
- beforeSelection = false
-
- update.beforeSelection = beforeSelection
- update.inSelection = inSelection
- update.afterSelection = afterSelection
-
- if inSelection
- $scope.trackChanges.selection.updates.push update
-
- if update.selectedFrom
- inSelection = false
- afterSelection = true
-
- $scope.recalculateHoveredUpdates = () ->
- hoverSelectedFrom = false
- hoverSelectedTo = false
- for update in $scope.trackChanges.updates
- # Figure out whether the to or from selector is hovered over
- if update.hoverSelectedFrom
- hoverSelectedFrom = true
- if update.hoverSelectedTo
- hoverSelectedTo = true
-
- if hoverSelectedFrom
- # We want to 'hover select' everything between hoverSelectedFrom and selectedTo
- inHoverSelection = false
- for update in $scope.trackChanges.updates
- if update.selectedTo
- update.hoverSelectedTo = true
- inHoverSelection = true
- update.inHoverSelection = inHoverSelection
- if update.hoverSelectedFrom
- inHoverSelection = false
- if hoverSelectedTo
- # We want to 'hover select' everything between hoverSelectedTo and selectedFrom
- inHoverSelection = false
- for update in $scope.trackChanges.updates
- if update.hoverSelectedTo
- inHoverSelection = true
- update.inHoverSelection = inHoverSelection
- if update.selectedFrom
- update.hoverSelectedFrom = true
- inHoverSelection = false
-
- $scope.resetHoverState = () ->
- for update in $scope.trackChanges.updates
- delete update.hoverSelectedFrom
- delete update.hoverSelectedTo
- delete update.inHoverSelection
-
- $scope.$watch "trackChanges.updates.length", () ->
- $scope.recalculateSelectedUpdates()
- ]
-
- App.controller "TrackChangesListItemController", ["$scope", ($scope) ->
- $scope.$watch "update.selectedFrom", (selectedFrom, oldSelectedFrom) ->
- if selectedFrom
- console.log "SELECTED FROM CHANGED", $scope.update, selectedFrom, oldSelectedFrom
- for update in $scope.trackChanges.updates
- update.selectedFrom = false unless update == $scope.update
- $scope.recalculateSelectedUpdates()
-
- $scope.$watch "update.selectedTo", (selectedTo, oldSelectedTo) ->
- if selectedTo
- console.log "SELECTED TO CHANGED", $scope.update, selectedTo, oldSelectedTo
- for update in $scope.trackChanges.updates
- update.selectedTo = false unless update == $scope.update
- $scope.recalculateSelectedUpdates()
-
- $scope.select = () ->
- $scope.update.selectedTo = true
- $scope.update.selectedFrom = true
-
- $scope.mouseOverSelectedFrom = () ->
- $scope.trackChanges.hoveringOverListSelectors = true
- $scope.update.hoverSelectedFrom = true
- $scope.recalculateHoveredUpdates()
-
- $scope.mouseOutSelectedFrom = () ->
- $scope.trackChanges.hoveringOverListSelectors = false
- $scope.resetHoverState()
-
- $scope.mouseOverSelectedTo = () ->
- $scope.trackChanges.hoveringOverListSelectors = true
- $scope.update.hoverSelectedTo = true
- $scope.recalculateHoveredUpdates()
-
- $scope.mouseOutSelectedTo = () ->
- $scope.trackChanges.hoveringOverListSelectors = false
- $scope.resetHoverState()
-
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/track-changes/directives/infiniteScroll.coffee b/services/web/public/coffee/app/ide/track-changes/directives/infiniteScroll.coffee
deleted file mode 100644
index 550373e77c..0000000000
--- a/services/web/public/coffee/app/ide/track-changes/directives/infiniteScroll.coffee
+++ /dev/null
@@ -1,45 +0,0 @@
-define [
- "base"
-], (App) ->
- App.directive "infiniteScroll", () ->
- return {
- link: (scope, element, attrs, ctrl) ->
- innerElement = element.find(".infinite-scroll-inner")
- element.css 'overflow-y': 'auto'
-
- atEndOfListView = () ->
- element.scrollTop() + element.height() >= innerElement.height() - 30
-
- listShorterThanContainer = () ->
- element.innerHeight() > @$(".change-list").outerHeight()
-
- loadUntilFull = () ->
- if (listShorterThanContainer() or atEndOfListView()) and not scope.$eval(attrs.infiniteScrollDisabled)
- console.log "Loading more"
- promise = scope.$eval(attrs.infiniteScroll)
- console.log promise
- promise.then () ->
- loadUntilFull()
- # @collection.fetchNextBatch
- # error: (error) =>
- # @hideLoading()
- # @showEmptyMessageIfCollectionEmpty()
- # callback(error)
- # success: (collection, response) =>
- # @hideLoading()
- # if @collection.isAtEnd()
- # @atEndOfCollection = true
- # @showEmptyMessageIfCollectionEmpty()
- # callback()
- # else
- # @loadUntilFull(callback)
-
- element.on "scroll", (event) ->
- loadUntilFull()
-
- scope.$watch attrs.infiniteScrollInitialize, (value) ->
- console.log "INITIALIZE", value
- if value
- loadUntilFull()
-
- }
\ No newline at end of file
diff --git a/services/web/public/coffee/app/main.coffee b/services/web/public/coffee/app/main.coffee
deleted file mode 100644
index c2a70fdb78..0000000000
--- a/services/web/public/coffee/app/main.coffee
+++ /dev/null
@@ -1,16 +0,0 @@
-define [
- "main/project-list"
- "main/user-details"
- "main/account-settings"
- "main/plans"
- "main/group-members"
- "directives/asyncForm"
- "directives/stopPropagation"
- "directives/focus"
- "directives/equals"
- "directives/fineUpload"
- "directives/onEnter"
- "directives/selectAll"
- "filters/formatDate"
-], () ->
- angular.bootstrap(document.body, ["SharelatexApp"])
\ No newline at end of file
diff --git a/services/web/public/coffee/app/main/account-settings.coffee b/services/web/public/coffee/app/main/account-settings.coffee
deleted file mode 100644
index 29cd18e3c5..0000000000
--- a/services/web/public/coffee/app/main/account-settings.coffee
+++ /dev/null
@@ -1,54 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "AccountSettingsController", ["$scope", "$http", "$modal", ($scope, $http, $modal) ->
- $scope.subscribed = true
-
- $scope.unsubscribe = () ->
- $scope.unsubscribing = true
- $http({
- method: "DELETE"
- url: "/user/newsletter/unsubscribe"
- headers:
- "X-CSRF-Token": window.csrfToken
- })
- .success () ->
- $scope.unsubscribing = false
- $scope.subscribed = false
- .error () ->
- $scope.unsubscribing = true
-
- $scope.deleteAccount = () ->
- modalInstance = $modal.open(
- templateUrl: "deleteAccountModalTemplate"
- controller: "DeleteAccountModalController"
- )
- ]
-
- App.controller "DeleteAccountModalController", [
- "$scope", "$modalInstance", "$timeout", "$http",
- ($scope, $modalInstance, $timeout, $http) ->
- $scope.state =
- inflight: false
-
- $modalInstance.opened.then () ->
- $timeout () ->
- $scope.$broadcast "open"
- , 700
-
- $scope.delete = () ->
- $scope.state.inflight = true
-
- $http({
- method: "DELETE"
- url: "/user"
- headers:
- "X-CSRF-Token": window.csrfToken
- })
- .success () ->
- $modalInstance.close()
- window.location = "/"
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
- ]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/main/group-members.coffee b/services/web/public/coffee/app/main/group-members.coffee
deleted file mode 100644
index f1f61623ee..0000000000
--- a/services/web/public/coffee/app/main/group-members.coffee
+++ /dev/null
@@ -1,54 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "GroupMembersController", ($scope, queuedHttp) ->
- $scope.users = window.users
- $scope.groupSize = window.groupSize
- $scope.selectedUsers = []
-
- $scope.inputs =
- emails: ""
-
- parseEmails = (emailsString)->
- regexBySpaceOrComma = /[\s,]+/
- emails = emailsString.split(regexBySpaceOrComma)
- emails = _.map emails, (email)->
- email = email.trim()
- emails = _.select emails, (email)->
- email.indexOf("@") != -1
- return emails
-
- $scope.addMembers = () ->
- emails = parseEmails($scope.inputs.emails)
- for email in emails
- queuedHttp
- .post("/subscription/group/user", {
- email: email,
- _csrf: window.csrfToken
- })
- .success (data) ->
- $scope.users.push data.user if data.user?
- $scope.inputs.emails = ""
-
- $scope.removeMembers = () ->
- for user in $scope.selectedUsers
- do (user) ->
- queuedHttp({
- method: "DELETE",
- url: "/subscription/group/user/#{user._id}"
- headers:
- "X-Csrf-Token": window.csrfToken
- })
- .success () ->
- index = $scope.users.indexOf(user)
- return if index == -1
- $scope.users.splice(index, 1)
- $scope.selectedUsers = []
-
- $scope.updateSelectedUsers = () ->
- $scope.selectedUsers = $scope.users.filter (user) -> user.selected
-
- App.controller "GroupMemberListItemController", ($scope) ->
- $scope.$watch "user.selected", (value) ->
- if value?
- $scope.updateSelectedUsers()
\ No newline at end of file
diff --git a/services/web/public/coffee/app/main/plans.coffee b/services/web/public/coffee/app/main/plans.coffee
deleted file mode 100644
index f5a3eb8945..0000000000
--- a/services/web/public/coffee/app/main/plans.coffee
+++ /dev/null
@@ -1,6 +0,0 @@
-define [
- "base"
-], (App) ->
- App.controller "PlansController", ($scope) ->
- $scope.ui =
- view: "monthly"
\ No newline at end of file
diff --git a/services/web/public/coffee/app/main/project-list.coffee b/services/web/public/coffee/app/main/project-list.coffee
deleted file mode 100644
index 0ad0c94660..0000000000
--- a/services/web/public/coffee/app/main/project-list.coffee
+++ /dev/null
@@ -1,583 +0,0 @@
-define [
- "base"
-], (App) ->
- App.factory "queuedHttp", ["$http", "$q", ($http, $q) ->
- pendingRequests = []
- inflight = false
-
- processPendingRequests = () ->
- return if inflight
- doRequest = pendingRequests.shift()
- if doRequest?
- inflight = true
- doRequest()
- .success () ->
- inflight = false
- processPendingRequests()
- .error () ->
- inflight = false
- processPendingRequests()
-
- queuedHttp = (args...) ->
- deferred = $q.defer()
- promise = deferred.promise
-
- # Adhere to the $http promise conventions
- promise.success = (callback) ->
- promise.then(callback)
- return promise
-
- promise.error = (callback) ->
- promise.catch(callback)
- return promise
-
- doRequest = () ->
- $http(args...)
- .success (successArgs...) ->
- deferred.resolve(successArgs...)
- .error (errorArgs...) ->
- deferred.reject(errorArgs...)
-
- pendingRequests.push doRequest
- processPendingRequests()
-
- return promise
-
- queuedHttp.post = (url, data) ->
- queuedHttp({method: "POST", url: url, data: data})
-
- return queuedHttp
-
- ]
-
- App.controller "ProjectPageController", ($scope, $modal, $q, queuedHttp) ->
- $scope.projects = window.data.projects
- $scope.visibleProjects = $scope.projects
- $scope.tags = window.data.tags
- $scope.allSelected = false
- $scope.selectedProjects = []
- $scope.filter = "all"
-
- # Allow tags to be accessed on projects as well
- projectsById = {}
- for project in $scope.projects
- projectsById[project.id] = project
-
- for tag in $scope.tags
- for project_id in tag.project_ids or []
- project = projectsById[project_id]
- if project?
- project.tags ||= []
- project.tags.push tag
-
- $scope.$watch "searchText", (value) ->
- $scope.updateVisibleProjects()
-
- $scope.clearSearchText = () ->
- $scope.searchText = ""
- $scope.$emit "search:clear"
-
- $scope.setFilter = (filter) ->
- $scope.filter = filter
- $scope.updateVisibleProjects()
-
- $scope.updateSelectedProjects = () ->
- $scope.selectedProjects = $scope.projects.filter (project) -> project.selected
-
- $scope.getSelectedProjects = () ->
- $scope.selectedProjects
-
- $scope.getSelectedProjectIds = () ->
- $scope.selectedProjects.map (project) -> project.id
-
- $scope.getFirstSelectedProject = () ->
- $scope.selectedProjects[0]
-
- $scope.updateVisibleProjects = () ->
- $scope.visibleProjects = []
- selectedTag = $scope.getSelectedTag()
- for project in $scope.projects
- visible = true
- # Only show if it matches any search text
- if $scope.searchText? and $scope.searchText != ""
- if !project.name.toLowerCase().match($scope.searchText.toLowerCase())
- visible = false
- # Only show if it matches the selected tag
- if $scope.filter == "tag" and selectedTag? and project.id not in selectedTag.project_ids
- visible = false
-
- # Hide projects we own if we only want to see shared projects
- if $scope.filter == "shared" and project.accessLevel == "owner"
- visible = false
-
- # Hide projects we don't own if we only want to see owned projects
- if $scope.filter == "owned" and project.accessLevel != "owner"
- visible = false
-
- if $scope.filter == "archived"
- # Only show archived projects
- if !project.archived
- visible = false
- else
- # Only show non-archived projects
- if project.archived
- visible = false
-
- if visible
- $scope.visibleProjects.push project
- else
- # We don't want hidden selections
- project.selected = false
- $scope.updateSelectedProjects()
-
- $scope.getSelectedTag = () ->
- for tag in $scope.tags
- return tag if tag.selected
- return null
-
- $scope._removeProjectIdsFromTagArray = (tag, remove_project_ids) ->
- # Remove project_id from tag.project_ids
- remaining_project_ids = []
- removed_project_ids = []
- for project_id in tag.project_ids
- if project_id not in remove_project_ids
- remaining_project_ids.push project_id
- else
- removed_project_ids.push project_id
- tag.project_ids = remaining_project_ids
- return removed_project_ids
-
- $scope._removeProjectFromList = (project) ->
- index = $scope.projects.indexOf(project)
- if index > -1
- $scope.projects.splice(index, 1)
-
- $scope.removeSelectedProjectsFromTag = (tag) ->
- tag.showWhenEmpty = true
-
- selected_project_ids = $scope.getSelectedProjectIds()
- selected_projects = $scope.getSelectedProjects()
-
- removed_project_ids = $scope._removeProjectIdsFromTagArray(tag, selected_project_ids)
-
- # Remove tag from project.tags
- remaining_tags = []
- for project in selected_projects
- project.tags ||= []
- index = project.tags.indexOf tag
- if index > -1
- project.tags.splice(index, 1)
-
- for project_id in removed_project_ids
- queuedHttp.post "/project/#{project_id}/tag", {
- deletedTag: tag.name
- _csrf: window.csrfToken
- }
-
- # If we're filtering by this tag then we need to remove
- # the projects from view
- $scope.updateVisibleProjects()
-
- $scope.addSelectedProjectsToTag = (tag) ->
- selected_projects = $scope.getSelectedProjects()
-
- # Add project_ids into tag.project_ids
- added_project_ids = []
- for project_id in $scope.getSelectedProjectIds()
- unless project_id in tag.project_ids
- tag.project_ids.push project_id
- added_project_ids.push project_id
-
- # Add tag into each project.tags
- for project in selected_projects
- project.tags ||= []
- unless tag in project.tags
- project.tags.push tag
-
- for project_id in added_project_ids
- queuedHttp.post "/project/#{project_id}/tag", {
- tag: tag.name
- _csrf: window.csrfToken
- }
-
- $scope.createTag = (name) ->
- $scope.tags.push {
- name: name
- project_ids: []
- showWhenEmpty: true
- }
-
- $scope.openNewTagModal = (e) ->
- modalInstance = $modal.open(
- templateUrl: "newTagModalTemplate"
- controller: "NewTagModalController"
- )
-
- modalInstance.result.then(
- (newTagName) ->
- $scope.createTag(newTagName)
- )
-
- $scope.createProject = (name, template = "none") ->
- deferred = $q.defer()
-
- queuedHttp
- .post("/project/new", {
- _csrf: window.csrfToken
- projectName: name
- template: template
- })
- .success((data, status, headers, config) ->
- $scope.projects.push {
- name: name
- _id: data.project_id
- accessLevel: "owner"
- # TODO: Check access level if correct after adding it in
- # to the rest of the app
- }
- $scope.updateVisibleProjects()
- deferred.resolve(data.project_id)
- )
- .error((data, status, headers, config) ->
- deferred.reject()
- )
-
- return deferred.promise
-
- $scope.openCreateProjectModal = (template = "none") ->
- modalInstance = $modal.open(
- templateUrl: "newProjectModalTemplate"
- controller: "NewProjectModalController"
- resolve:
- template: () -> template
- scope: $scope
- )
-
- modalInstance.result.then (project_id) ->
- window.location = "/project/#{project_id}"
-
- $scope.renameProject = (project, newName) ->
- project.name = newName
- queuedHttp.post "/project/#{project.id}/rename", {
- newProjectName: newName
- _csrf: window.csrfToken
- }
-
- $scope.openRenameProjectModal = () ->
- project = $scope.getFirstSelectedProject()
- return if !project? or project.accessLevel != "owner"
-
- modalInstance = $modal.open(
- templateUrl: "renameProjectModalTemplate"
- controller: "RenameProjectModalController"
- resolve:
- projectName: () -> project.name
- )
-
- modalInstance.result.then(
- (newName) ->
- $scope.renameProject(project, newName)
- )
-
- $scope.cloneProject = (project, cloneName) ->
- deferred = $q.defer()
-
- queuedHttp
- .post("/project/#{project.id}/clone", {
- _csrf: window.csrfToken
- projectName: cloneName
- })
- .success((data, status, headers, config) ->
- $scope.projects.push {
- name: cloneName
- id: data.project_id
- accessLevel: "owner"
- # TODO: Check access level if correct after adding it in
- # to the rest of the app
- }
- $scope.updateVisibleProjects()
- deferred.resolve(data.project_id)
- )
- .error((data, status, headers, config) ->
- deferred.reject()
- )
-
- return deferred.promise
-
- $scope.openCloneProjectModal = () ->
- project = $scope.getFirstSelectedProject()
- return if !project?
-
- modalInstance = $modal.open(
- templateUrl: "cloneProjectModalTemplate"
- controller: "CloneProjectModalController"
- resolve:
- project: () -> project
- scope: $scope
- )
-
- $scope.openArchiveProjectsModal = () ->
- modalInstance = $modal.open(
- templateUrl: "deleteProjectsModalTemplate"
- controller: "DeleteProjectsModalController"
- resolve:
- projects: () -> $scope.getSelectedProjects()
- )
-
- modalInstance.result.then () ->
- $scope.archiveOrLeaveSelectedProjects()
-
- $scope.archiveOrLeaveSelectedProjects = () ->
- selected_projects = $scope.getSelectedProjects()
- selected_project_ids = $scope.getSelectedProjectIds()
-
- # Remove project from any tags
- for tag in $scope.tags
- $scope._removeProjectIdsFromTagArray(tag, selected_project_ids)
-
- for project in selected_projects
- if project.accessLevel == "owner"
- project.archived = true
- queuedHttp {
- method: "DELETE"
- url: "/project/#{project.id}"
- headers:
- "X-CSRF-Token": window.csrfToken
- }
- else
- $scope._removeProjectFromList project
-
- queuedHttp {
- method: "POST"
- url: "/project/#{project.id}/leave"
- headers:
- "X-CSRF-Token": window.csrfToken
- }
-
- $scope.updateVisibleProjects()
-
-
- $scope.openDeleteProjectsModal = () ->
- modalInstance = $modal.open(
- templateUrl: "deleteProjectsModalTemplate"
- controller: "DeleteProjectsModalController"
- resolve:
- projects: () -> $scope.getSelectedProjects()
- )
-
- modalInstance.result.then () ->
- $scope.deleteSelectedProjects()
-
- $scope.deleteSelectedProjects = () ->
- selected_projects = $scope.getSelectedProjects()
- selected_project_ids = $scope.getSelectedProjectIds()
-
- # Remove projects from array
- for project in selected_projects
- $scope._removeProjectFromList project
-
- # Remove project from any tags
- for tag in $scope.tags
- $scope._removeProjectIdsFromTagArray(tag, selected_project_ids)
-
- for project_id in selected_project_ids
- queuedHttp {
- method: "DELETE"
- url: "/project/#{project_id}?forever=true"
- headers:
- "X-CSRF-Token": window.csrfToken
- }
-
- $scope.updateVisibleProjects()
-
- $scope.restoreSelectedProjects = () ->
- selected_projects = $scope.getSelectedProjects()
- selected_project_ids = $scope.getSelectedProjectIds()
-
- for project in selected_projects
- project.archived = false
-
- for project_id in selected_project_ids
- queuedHttp {
- method: "POST"
- url: "/project/#{project_id}/restore"
- headers:
- "X-CSRF-Token": window.csrfToken
- }
-
- $scope.updateVisibleProjects()
-
- $scope.openUploadProjectModal = () ->
- modalInstance = $modal.open(
- templateUrl: "uploadProjectModalTemplate"
- controller: "UploadProjectModalController"
- )
-
- $scope.downloadSelectedProjects = () ->
- selected_project_ids = $scope.getSelectedProjectIds()
-
- if selected_project_ids.length > 1
- path = "/project/download/zip?project_ids=#{selected_project_ids.join(',')}"
- else
- path = "/project/#{selected_project_ids[0]}/download/zip"
-
- window.location = path
-
-
- App.controller "ProjectListItemController", ($scope) ->
- $scope.ownerName = () ->
- if $scope.project.accessLevel == "owner"
- return "You"
- else if $scope.project.owner?
- return "#{$scope.project.owner.first_name} #{$scope.project.owner.last_name}"
- else
- return "?"
-
- $scope.$watch "project.selected", (value) ->
- if value?
- $scope.updateSelectedProjects()
-
- App.controller "TagListController", ($scope) ->
- $scope.filterProjects = (filter = "all") ->
- $scope._clearTags()
- $scope.setFilter(filter)
-
- $scope._clearTags = () ->
- for tag in $scope.tags
- tag.selected = false
-
- $scope.nonEmpty = (tag) ->
- # The showWhenEmpty property will be set on any tag which we have
- # modified during this session. Otherwise, tags which are empty
- # when loading the page are not shown.
- tag.project_ids.length > 0 or !!tag.showWhenEmpty
-
- App.controller "TagListItemController", ($scope) ->
- $scope.selectTag = () ->
- $scope._clearTags()
- $scope.tag.selected = true
- $scope.setFilter("tag")
-
- App.controller "TagDropdownItemController", ($scope) ->
- $scope.recalculateProjectsInTag = () ->
- $scope.areSelectedProjectsInTag = false
- for project_id in $scope.getSelectedProjectIds()
- if project_id in $scope.tag.project_ids
- $scope.areSelectedProjectsInTag = true
- else
- partialSelection = true
-
- if $scope.areSelectedProjectsInTag and partialSelection
- $scope.areSelectedProjectsInTag = "partial"
-
- $scope.addOrRemoveProjectsFromTag = () ->
- if $scope.areSelectedProjectsInTag == true
- $scope.removeSelectedProjectsFromTag($scope.tag)
- $scope.areSelectedProjectsInTag = false
- else if $scope.areSelectedProjectsInTag == false or $scope.areSelectedProjectsInTag == "partial"
- $scope.addSelectedProjectsToTag($scope.tag)
- $scope.areSelectedProjectsInTag = true
-
- $scope.$watch "selectedProjects", () ->
- $scope.recalculateProjectsInTag()
- $scope.recalculateProjectsInTag()
-
- App.controller 'NewTagModalController', ($scope, $modalInstance, $timeout) ->
- $scope.inputs =
- newTagName: ""
-
- $modalInstance.opened.then () ->
- $timeout () ->
- $scope.$broadcast "open"
- , 700
-
- $scope.create = () ->
- $modalInstance.close($scope.inputs.newTagName)
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
-
- App.controller 'RenameProjectModalController', ($scope, $modalInstance, $timeout, projectName) ->
- $scope.inputs =
- projectName: projectName
-
- $modalInstance.opened.then () ->
- $timeout () ->
- $scope.$broadcast "open"
- , 700
-
- $scope.rename = () ->
- $modalInstance.close($scope.inputs.projectName)
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
-
- App.controller 'CloneProjectModalController', ($scope, $modalInstance, $timeout, project) ->
- $scope.inputs =
- projectName: project.name + " (Copy)"
- $scope.state =
- inflight: false
-
- $modalInstance.opened.then () ->
- $timeout () ->
- $scope.$broadcast "open"
- , 700
-
- $scope.clone = () ->
- $scope.state.inflight = true
- $scope
- .cloneProject(project, $scope.inputs.projectName)
- .then (project_id) ->
- $scope.state.inflight = false
- $modalInstance.close(project_id)
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
-
- App.controller 'NewProjectModalController', ($scope, $modalInstance, $timeout, template) ->
- $scope.inputs =
- projectName: ""
- $scope.state =
- inflight: false
-
- $modalInstance.opened.then () ->
- $timeout () ->
- $scope.$broadcast "open"
- , 700
-
- $scope.create = () ->
- $scope.state.inflight = true
- $scope
- .createProject($scope.inputs.projectName, template)
- .then (project_id) ->
- $scope.state.inflight = false
- $modalInstance.close(project_id)
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
-
- App.controller 'DeleteProjectsModalController', ($scope, $modalInstance, $timeout, projects) ->
- $scope.projectsToDelete = projects.filter (project) -> project.accessLevel == "owner"
- $scope.projectsToLeave = projects.filter (project) -> project.accessLevel != "owner"
-
- if $scope.projectsToLeave.length > 0 and $scope.projectsToDelete.length > 0
- $scope.action = "Delete & Leave"
- else if $scope.projectsToLeave.length == 0 and $scope.projectsToDelete.length > 0
- $scope.action = "Delete"
- else
- $scope.action = "Leave"
-
- $scope.delete = () ->
- $modalInstance.close()
-
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
-
-
- App.controller 'UploadProjectModalController', ($scope, $modalInstance, $timeout) ->
- $scope.cancel = () ->
- $modalInstance.dismiss('cancel')
-
- $scope.onComplete = (error, name, response) ->
- if response.project_id?
- window.location = '/project/' + response.project_id
diff --git a/services/web/public/coffee/app/main/user-details.coffee b/services/web/public/coffee/app/main/user-details.coffee
deleted file mode 100644
index 0d8413a835..0000000000
--- a/services/web/public/coffee/app/main/user-details.coffee
+++ /dev/null
@@ -1,58 +0,0 @@
-define [
- "base"
- "../../libs/algolia"
-], (App, algolia)->
- app.factory "Institutions", ->
- new AlgoliaSearch(window.algolia.institutions.app_id, window.algolia.institutions.api_key).initIndex("institutions")
-
- App.controller "UserProfileController", ($scope, $modal, $http)->
- $scope.institutions = []
- $http.get("/user/personal_info").success (data)->
- $scope.userInfoForm =
- first_name: data.first_name || ""
- last_name: data.last_name || ""
- role: data.role || ""
- institution: data.institution || ""
- _csrf : window.csrfToken
-
- $scope.showForm = ->
- $scope.formVisable = true
-
- $scope.getPercentComplete = ->
- results = _.filter $scope.userInfoForm, (value)-> !value? or value?.length != 0
- results.length * 20
-
- $scope.$watch "userInfoForm", (value) ->
- if value?
- $scope.percentComplete = $scope.getPercentComplete()
- , true
-
- $scope.openUserProfileModal = () ->
- $modal.open {
- templateUrl: "userProfileModalTemplate"
- controller: "UserProfileModalController"
- scope: $scope
- }
-
- App.controller "UserProfileModalController", ($scope, $modalInstance, $http, Institutions) ->
- $scope.roles = ["Student", "Post-graduate student", "Post-doctoral researcher", "Lecturer", "Professor"]
-
- $scope.sendUpdate = ->
- request = $http.post "/user/settings", $scope.userInfoForm
- request.success (data, status)->
- request.error (data, status)->
- console.log "the request failed"
-
- $scope.updateInstitutionsList = (inputVal)->
-
- # this is a little hack to use until we change auto compelete lib with redesign and can
- # listen for blur events on institution field to send the post
- if inputVal?.indexOf("(") != -1 and inputVal?.indexOf(")") != -1
- $scope.sendUpdate()
-
- Institutions.search $scope.userInfoForm.institution, (err, response)->
- $scope.institutions = _.map response.hits, (institution)->
- "#{institution.name} (#{institution.domain})"
-
- $scope.done = () ->
- $modalInstance.close()
diff --git a/services/web/public/coffee/app/modules/recursionHelper.coffee b/services/web/public/coffee/app/modules/recursionHelper.coffee
deleted file mode 100644
index 621b6c11be..0000000000
--- a/services/web/public/coffee/app/modules/recursionHelper.coffee
+++ /dev/null
@@ -1,44 +0,0 @@
-#
-# * An Angular service which helps with creating recursive directives.
-# * @author Mark Lagendijk
-# * @license MIT
-#
-# From: https://github.com/marklagendijk/angular-recursion
-angular.module("RecursionHelper", []).factory "RecursionHelper", [
- "$compile"
- ($compile) ->
-
- ###
- Manually compiles the element, fixing the recursion loop.
- @param element
- @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
- @returns An object containing the linking functions.
- ###
- return compile: (element, link) ->
-
- # Normalize the link parameter
- link = post: link if angular.isFunction(link)
-
- # Break the recursion loop by removing the contents
- contents = element.contents().remove()
- compiledContents = undefined
- pre: (if (link and link.pre) then link.pre else null)
-
- ###
- Compiles and re-adds the contents
- ###
- post: (scope, element) ->
-
- # Compile the contents
- compiledContents = $compile(contents) unless compiledContents
-
- # Re-add the compiled contents to the element
- compiledContents scope, (clone) ->
- element.append clone
- return
-
-
- # Call the post-linking function, if any
- link.post.apply null, arguments if link and link.post
- return
-]
\ No newline at end of file
diff --git a/services/web/public/coffee/app/utils/EventEmitter.coffee b/services/web/public/coffee/app/utils/EventEmitter.coffee
deleted file mode 100644
index 12c34e73f0..0000000000
--- a/services/web/public/coffee/app/utils/EventEmitter.coffee
+++ /dev/null
@@ -1,33 +0,0 @@
-define [], () ->
- class EventEmitter
- on: (event, callback) ->
- @events ||= {}
- [event, namespace] = event.split(".")
- @events[event] ||= []
- @events[event].push {
- callback: callback
- namespace: namespace
- }
-
- off: (event) ->
- @events ||= {}
- if event?
- [event, namespace] = event.split(".")
- if !namespace?
- # Clear all listeners for event
- delete @events[event]
- else
- # Clear only namespaced listeners
- remaining_events = []
- for callback in @events[event] or []
- if callback.namespace != namespace
- remaining_events.push callback
- @events[event] = remaining_events
- else
- # Remove all listeners
- @events = {}
-
- trigger: (event, args...) ->
- @events ||= {}
- for callback in @events[event] or []
- callback.callback(args...)
\ No newline at end of file
diff --git a/services/web/public/coffee/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/auto-complete/AutoCompleteManager.coffee
deleted file mode 100644
index fbc90f0324..0000000000
--- a/services/web/public/coffee/auto-complete/AutoCompleteManager.coffee
+++ /dev/null
@@ -1,79 +0,0 @@
-define [
- "auto-complete/SuggestionManager"
- "auto-complete/Snippets"
- "ace/autocomplete/util"
- "ace/autocomplete"
- "ace/range"
- "ace/ext/language_tools"
-], (SuggestionManager, Snippets, Util, AutoComplete) ->
- Range = require("ace/range").Range
- Autocomplete = AutoComplete.Autocomplete
-
- Util.retrievePrecedingIdentifier = (text, pos, regex) ->
- currentLineOffset = 0
- for i in [(pos-1)..0]
- if text[i] == "\n"
- currentLineOffset = i + 1
- break
- currentLine = text.slice(currentLineOffset, pos)
- fragment = getLastCommandFragment(currentLine) or ""
- return fragment
-
- getLastCommandFragment = (lineUpToCursor) ->
- if m = lineUpToCursor.match(/(\\[^\\ ]+)$/)
- return m[1]
- else
- return null
-
- class AutoCompleteManager
- constructor: (@ide) ->
- @aceEditor = @ide.editor.aceEditor
- @aceEditor.setOptions({
- enableBasicAutocompletion: true,
- enableSnippets: true
- })
-
- SnippetCompleter =
- getCompletions: (editor, session, pos, prefix, callback) ->
- callback null, Snippets
- @suggestionManager = new SuggestionManager()
-
- @aceEditor.completers = [@suggestionManager, SnippetCompleter]
-
- insertMatch = Autocomplete::insertMatch
- editor = @aceEditor
- Autocomplete::insertMatch = (data) ->
- pos = editor.getCursorPosition()
- range = new Range(pos.row, pos.column, pos.row, pos.column + 1)
- nextChar = editor.session.getTextRange(range)
-
- console.log "CALLING OLD"
-
- # If we are in \begin{it|}, then we need to remove the trailing }
- # since it will be adding in with the autocomplete of \begin{item}...
- if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}"
- editor.session.remove(range)
-
- insertMatch.call editor.completer, data
-
- @bindToEditorEvents()
-
- bindToEditorEvents: () ->
- @ide.editor.on "change:doc", (@aceSession) =>
- @aceSession.on "change", (change) => @onChange(change)
-
- onChange: (change) ->
- cursorPosition = @aceEditor.getCursorPosition()
- end = change.data.range.end
- # Check that this change was made by us, not a collaborator
- # (Cursor is still one place behind)
- if end.row == cursorPosition.row and end.column == cursorPosition.column + 1
- if change.data.action == "insertText"
- range = new Range(end.row, 0, end.row, end.column)
- lineUpToCursor = @aceSession.getTextRange(range)
- commandFragment = getLastCommandFragment(lineUpToCursor)
-
- if commandFragment? and commandFragment.length > 2
- setTimeout () =>
- @aceEditor.execCommand("startAutocomplete")
- , 0
diff --git a/services/web/public/coffee/auto-complete/Snippets.coffee b/services/web/public/coffee/auto-complete/Snippets.coffee
deleted file mode 100644
index 53e0f57fa1..0000000000
--- a/services/web/public/coffee/auto-complete/Snippets.coffee
+++ /dev/null
@@ -1,100 +0,0 @@
-define () ->
- environments = [
- "abstract",
- "align", "align*",
- "equation", "equation*",
- "gather", "gather*",
- "multline", "multline*",
- "split",
- "verbatim"
- ]
-
- snippets = for env in environments
- {
- caption: "\\begin{#{env}}..."
- snippet: """
- \\begin{#{env}}
- $1
- \\end{#{env}}
- """
- meta: "env"
- }
-
- snippets = snippets.concat [{
- caption: "\\begin{array}..."
- snippet: """
- \\begin{array}{${1:cc}}
- $2 & $3 \\\\\\\\
- $4 & $5
- \\end{array}
- """
- meta: "env"
- }, {
- caption: "\\begin{figure}..."
- snippet: """
- \\begin{figure}
- \\centering
- \\includegraphics{$1}
- \\caption{${2:Caption}}
- \\label{${3:fig:my_label}}
- \\end{figure}
- """
- meta: "env"
- }, {
- caption: "\\begin{tabular}..."
- snippet: """
- \\begin{tabular}{${1:c|c}}
- $2 & $3 \\\\\\\\
- $4 & $5
- \\end{tabular}
- """
- meta: "env"
- }, {
- caption: "\\begin{table}..."
- snippet: """
- \\begin{table}[$1]
- \\centering
- \\begin{tabular}{${2:c|c}}
- $3 & $4 \\\\\\\\
- $5 & $6
- \\end{tabular}
- \\caption{${7:Caption}}
- \\label{${8:tab:my_label}}
- \\end{table}
- """
- meta: "env"
- }, {
- caption: "\\begin{list}..."
- snippet: """
- \\begin{list}
- \\item $1
- \\end{list}
- """
- meta: "env"
- }, {
- caption: "\\begin{enumerate}..."
- snippet: """
- \\begin{enumerate}
- \\item $1
- \\end{enumerate}
- """
- meta: "env"
- }, {
- caption: "\\begin{itemize}..."
- snippet: """
- \\begin{itemize}
- \\item $1
- \\end{itemize}
- """
- meta: "env"
- }, {
- caption: "\\begin{frame}..."
- snippet: """
- \\begin{frame}{${1:Frame Title}}
- $2
- \\end{frame}
- """
- meta: "env"
- }]
-
- return snippets
\ No newline at end of file
diff --git a/services/web/public/coffee/auto-complete/SuggestionManager.coffee b/services/web/public/coffee/auto-complete/SuggestionManager.coffee
deleted file mode 100644
index 559a2c5981..0000000000
--- a/services/web/public/coffee/auto-complete/SuggestionManager.coffee
+++ /dev/null
@@ -1,126 +0,0 @@
-define [], () ->
- class Parser
- constructor: (@doc) ->
-
- parse: () ->
- commands = []
- seen = {}
- while command = @nextCommand()
- docState = @doc
-
- optionalArgs = 0
- while @consumeArgument("[", "]")
- optionalArgs++
-
- args = 0
- while @consumeArgument("{", "}")
- args++
-
- commandHash = "#{command}\\#{optionalArgs}\\#{args}"
- if !seen[commandHash]?
- seen[commandHash] = true
- commands.push [command, optionalArgs, args]
-
- # Reset to before argument to handle nested commands
- @doc = docState
-
- return commands
-
- # Ignore single letter commands since auto complete is moot then.
- commandRegex: /\\([a-zA-Z][a-zA-Z]+)/
-
- nextCommand: () ->
- i = @doc.search(@commandRegex)
- if i == -1
- return false
- else
- match = @doc.match(@commandRegex)[1]
- @doc = @doc.substr(i + match.length + 1)
- return match
-
- consumeWhitespace: () ->
- match = @doc.match(/^[ \t\n]*/m)[0]
- @doc = @doc.substr(match.length)
-
- consumeArgument: (openingBracket, closingBracket) ->
- @consumeWhitespace()
-
- if @doc[0] == openingBracket
- i = 1
- bracketParity = 1
- while bracketParity > 0 and i < @doc.length
- if @doc[i] == openingBracket
- bracketParity++
- else if @doc[i] == closingBracket
- bracketParity--
- i++
-
- if bracketParity == 0
- @doc = @doc.substr(i)
- return true
- else
- return false
- else
- return false
-
- class SuggestionManager
- getCompletions: (editor, session, pos, prefix, callback) ->
- doc = session.getValue()
- parser = new Parser(doc)
- commands = parser.parse()
-
- completions = []
- for command in commands
- caption = "\\#{command[0]}"
- snippet = caption
- i = 1
- _.times command[1], () ->
- snippet += "[${#{i}}]"
- caption += "[]"
- i++
- _.times command[2], () ->
- snippet += "{${#{i}}}"
- caption += "{}"
- i++
- unless caption == prefix
- completions.push {
- caption: caption
- snippet: snippet
- meta: "cmd"
- }
-
- callback null, completions
-
- loadCommandsFromDoc: (doc) ->
- parser = new Parser(doc)
- @commands = parser.parse()
-
- getSuggestions: (commandFragment) ->
- matchingCommands = _.filter @commands, (command) ->
- command[0].slice(0, commandFragment.length) == commandFragment
-
- return _.map matchingCommands, (command) ->
- base = "\\" + commandFragment
-
- args = ""
- _.times command[1], () -> args = args + "[]"
- _.times command[2], () -> args = args + "{}"
- completionBase = command[0].slice(commandFragment.length)
-
- squareArgsNo = command[1]
- curlyArgsNo = command[2]
- totalArgs = squareArgsNo + curlyArgsNo
- if totalArgs == 0
- completionBeforeCursor = completionBase
- completionAfterCurspr = ""
- else
- completionBeforeCursor = completionBase + args[0]
- completionAfterCursor = args.slice(1)
-
- return {
- base: base,
- completion: completionBase + args,
- completionBeforeCursor: completionBeforeCursor
- completionAfterCursor: completionAfterCursor
- }
-
diff --git a/services/web/public/coffee/cursors/CursorManager.coffee b/services/web/public/coffee/cursors/CursorManager.coffee
deleted file mode 100644
index 9ba1879ccf..0000000000
--- a/services/web/public/coffee/cursors/CursorManager.coffee
+++ /dev/null
@@ -1,99 +0,0 @@
-define () ->
- class CursorManager
- UPDATE_INTERVAL: 500
-
- constructor: (@ide) ->
- @clients = {}
- @ide.socket.on "clientTracking.clientUpdated", (cursorUpdate) => @onRemoteClientUpdate(cursorUpdate)
- @ide.socket.on "clientTracking.clientDisconnected", (client_id) => @onRemoteClientDisconnect(client_id)
- @ide.editor.on "change:doc", (session) =>
- @bindToAceSession(session)
- @ide.editor.on "mousemove", (e) =>
- @mousePosition = e.position
- @updateVisibleNames()
-
- bindToAceSession: (session) ->
- @clients = {}
- @ide.editor.aceEditor.on "changeSelection", => @onLocalCursorUpdate()
-
- onLocalCursorUpdate: () ->
- if !@cursorUpdateTimeout?
- @cursorUpdateTimeout = setTimeout (=>
- @_sendLocalCursorUpdate()
- delete @cursorUpdateTimeout
- ), @UPDATE_INTERVAL
-
- _sendLocalCursorUpdate: () ->
- cursor = @ide.editor.getCursorPosition()
- if !@currentCursorPosition? or not (cursor.row == @currentCursorPosition.row and cursor.column == @currentCursorPosition.column)
- @currentCursorPosition = cursor
- @ide.socket.emit "clientTracking.updatePosition", {
- row: cursor.row
- column: cursor.column
- doc_id: @ide.editor.getCurrentDocId()
- }
-
- onRemoteClientUpdate: (clientData) ->
- if clientData.id != ide.socket.socket.sessionid
- client = @clients[clientData.id] ||= {}
- client.row = clientData.row
- client.column = clientData.column
- client.name = clientData.name
- client.doc_id = clientData.doc_id
- @redrawCursors()
-
- onRemoteClientDisconnect: (client_id) ->
- @removeCursor(client_id)
- delete @clients[client_id]
-
- removeCursor: (client_id) ->
- client = @clients[client_id]
- return if !client?
- @ide.editor.removeMarker(client.cursorMarkerId)
- delete client.cursorMarkerId
-
- redrawCursors: () ->
- for clientId, clientData of @clients
- do (clientId, clientData) =>
- if clientData.cursorMarkerId?
- @removeCursor(clientId)
- if clientData.doc_id == @ide.editor.getCurrentDocId()
- colorId = @getColorIdFromName(clientData.name)
- clientData.cursorMarkerId = @ide.editor.addMarker {
- row: clientData.row
- column: clientData.column
- length: 1
- }, "sharelatex-remote-cursor", (html, range, left, top, config) ->
- div = """
-
-
-
#{$('
').text(clientData.name).html()}
-
- """
- html.push div
- , true
- setTimeout =>
- @updateVisibleNames()
- , 0
-
- updateVisibleNames: () ->
- for clientId, clientData of @clients
- if @mousePosition? and clientData.row == @mousePosition.row and clientData.column == @mousePosition.column
- $("#cursor-#{clientId}").find(".name").show()
- $("#cursor-#{clientId}").find(".nubbin").hide()
- else
- $("#cursor-#{clientId}").find(".name").hide()
- $("#cursor-#{clientId}").find(".nubbin").show()
-
- getColorIdFromName: (name) ->
- @currentColorId ||= 0
- @colorIds ||= {}
- if !@colorIds[name]?
- @colorIds[name] = @currentColorId
- @currentColorId++
- return @colorIds[name]
-
diff --git a/services/web/public/coffee/debug/DebugManager.coffee b/services/web/public/coffee/debug/DebugManager.coffee
deleted file mode 100644
index 7776fdd000..0000000000
--- a/services/web/public/coffee/debug/DebugManager.coffee
+++ /dev/null
@@ -1,32 +0,0 @@
-define [
- "utils/Modal"
-], (Modal) ->
- class DebugManager
- template: $("#DebugLinkTemplate").html()
-
- constructor: (@ide) ->
- @$el = $(@template)
- $("#toolbar-footer").append(@$el)
- @$el.on "click", (e) =>
- e.preventDefault()
- @showDebugModal()
-
- showDebugModal: () ->
- useragent = navigator.userAgent
- server_id = document.cookie.match(/SERVERID=([^;]*)/)?[1]
- transport = @ide.socket.socket.transport.name
-
- new Modal(
- title: "Debug info"
- message: """
- Please give this information to the ShareLaTeX team:
-
- user-agent: #{useragent}
- server-id: #{server_id}
- transport: #{transport}
-
- """
- buttons: [
- text: "OK"
- ]
- )
\ No newline at end of file
diff --git a/services/web/public/coffee/editor/AceUpdateManager.coffee b/services/web/public/coffee/editor/AceUpdateManager.coffee
deleted file mode 100644
index 5496206961..0000000000
--- a/services/web/public/coffee/editor/AceUpdateManager.coffee
+++ /dev/null
@@ -1,40 +0,0 @@
-define [
- "documentUpdater"
- "ace/range"
-], () ->
- Range = require("ace/range").Range
- Modal = require("utils/Modal")
-
- class AceUpdateManager
- constructor: (@editor) ->
- @ide = @editor.ide
-
- guidGenerator=()->
- S4 = ()->
- return (((1+Math.random())*0x10000)|0).toString(16).substring(1)
- return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4())
- @window_id = guidGenerator()
-
- @bindToServerEvents()
-
- bindToServerEvents: () ->
-
-
-
-
- @ide.socket.on 'reciveTextUpdate', (updating_id, change) =>
- @ide.savingAreaManager.saved()
- if(@window_id == updating_id)
- return
- @ownChange = true
- doc = @editor.getDocument()
- documentUpdater.applyChange doc, change, Range, ()=>
- @ownChange = false
-
- bindToDocument: (doc_id, docLines, version) ->
- @current_doc_id = doc_id
- aceDoc = @editor.getDocument()
- aceDoc.on 'change', (change) =>
- if(!@ownChange)
- @ide.socket.emit 'sendUpdate',
- @current_doc_id, @window_id, change.data
diff --git a/services/web/public/coffee/editor/Document.coffee b/services/web/public/coffee/editor/Document.coffee
deleted file mode 100644
index b0fec4d231..0000000000
--- a/services/web/public/coffee/editor/Document.coffee
+++ /dev/null
@@ -1,257 +0,0 @@
-define [
- "editor/ShareJsDoc"
- "libs/backbone"
- "underscore"
-], (ShareJsDoc) ->
- class Document
- @getDocument: (ide, doc_id) ->
- @openDocs ||= {}
- if !@openDocs[doc_id]?
- @openDocs[doc_id] = new Document(ide, doc_id)
- return @openDocs[doc_id]
-
- @hasUnsavedChanges: () ->
- for doc_id, doc of (@openDocs or {})
- return true if doc.hasBufferedOps()
- return false
-
- constructor: (@ide, @doc_id) ->
- @connected = @ide.socket.socket.connected
- @joined = false
- @wantToBeJoined = false
- @_checkConsistency = _.bind(@_checkConsistency, @)
- @inconsistentCount = 0
- @_bindToEditorEvents()
- @_bindToSocketEvents()
-
- attachToAce: (@ace) ->
- @doc?.attachToAce(@ace)
- editorDoc = @ace.getSession().getDocument()
- editorDoc.on "change", @_checkConsistency
-
- detachFromAce: () ->
- @doc?.detachFromAce()
- editorDoc = @ace?.getSession().getDocument()
- editorDoc?.off "change", @_checkConsistency
-
- _checkConsistency: () ->
- # We've been seeing a lot of errors when I think there shouldn't be
- # any, which may be related to this check happening before the change is
- # applied. If we use a timeout, hopefully we can reduce this.
- setTimeout () =>
- editorValue = @ace?.getValue()
- sharejsValue = @doc?.getSnapshot()
- if editorValue != sharejsValue
- @inconsistentCount++
- else
- @inconsistentCount = 0
-
- if @inconsistentCount >= 3
- @_onError new Error("Editor text does not match server text")
- , 0
-
- getSnapshot: () ->
- @doc?.getSnapshot()
-
- getType: () ->
- @doc?.getType()
-
- getInflightOp: () ->
- @doc?.getInflightOp()
-
- getPendingOp: () ->
- @doc?.getPendingOp()
-
- hasBufferedOps: () ->
- @doc?.hasBufferedOps()
-
- _bindToSocketEvents: () ->
- @_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update)
- @ide.socket.on "otUpdateApplied", @_onUpdateAppliedHandler
- @_onErrorHandler = (error, update) => @_onError(error, update)
- @ide.socket.on "otUpdateError", @_onErrorHandler
- @_onDisconnectHandler = (error) => @_onDisconnect(error)
- @ide.socket.on "disconnect", @_onDisconnectHandler
-
- _bindToEditorEvents: () ->
- @_onReconnectHandler = (update) => @_onReconnect(update)
- @ide.on "afterJoinProject", @_onReconnectHandler
-
- unBindFromSocketEvents: () ->
- @ide.socket.removeListener "otUpdateApplied", @_onUpdateAppliedHandler
- @ide.socket.removeListener "otUpdateError", @_onUpdateErrorHandler
- @ide.socket.removeListener "disconnect", @_onDisconnectHandler
-
- unBindFromEditorEvents: () ->
- @ide.off "afterJoinProject", @_onReconnectHandler
-
- leaveAndCleanUp: () ->
- @leave (error) =>
- @_cleanUp()
-
- join: (callback = (error) ->) ->
- @wantToBeJoined = true
- @_cancelLeave()
- if @connected
- return @_joinDoc callback
- else
- @_joinCallbacks ||= []
- @_joinCallbacks.push callback
-
- leave: (callback = (error) ->) ->
- @wantToBeJoined = false
- @_cancelJoin()
- if (@doc? and @doc.hasBufferedOps())
- @_leaveCallbacks ||= []
- @_leaveCallbacks.push callback
- else if !@connected
- callback()
- else
- @_leaveDoc(callback)
-
- pollSavedStatus: () ->
- # returns false if doc has ops waiting to be acknowledged or
- # sent that haven't changed since the last time we checked.
- # Otherwise returns true.
- inflightOp = @getInflightOp()
- pendingOp = @getPendingOp()
- if !inflightOp? and !pendingOp?
- # there's nothing going on
- saved = true
- else if inflightOp == @oldInflightOp
- saved = false
- else if pendingOp?
- saved = false
- else
- saved = true
-
- @oldInflightOp = inflightOp
- return saved
-
- _cancelLeave: () ->
- if @_leaveCallbacks?
- delete @_leaveCallbacks
-
- _cancelJoin: () ->
- if @_joinCallbacks?
- delete @_joinCallbacks
-
- _onUpdateApplied: (update) ->
- @ide.pushEvent "received-update",
- doc_id: @doc_id
- remote_doc_id: update?.doc
- wantToBeJoined: @wantToBeJoined
- update: update
-
- if Math.random() < (@ide.disconnectRate or 0)
- console.log "Simulating disconnect"
- @ide.connectionManager.disconnect()
- return
-
- if update?.doc == @doc_id and @doc?
- @doc.processUpdateFromServer update
-
- if !@wantToBeJoined
- @leave()
-
- _onDisconnect: () ->
- @connected = false
- @joined = false
- @doc?.updateConnectionState "disconnected"
-
- _onReconnect: () ->
- @ide.pushEvent "reconnected:afterJoinProject"
-
- @connected = true
- if @wantToBeJoined or @doc?.hasBufferedOps()
- @_joinDoc (error) =>
- return @_onError(error) if error?
- @doc.updateConnectionState "ok"
- @doc.flushPendingOps()
- @_callJoinCallbacks()
-
- _callJoinCallbacks: () ->
- for callback in @_joinCallbacks or []
- callback()
- delete @_joinCallbacks
-
- _joinDoc: (callback = (error) ->) ->
- if @doc?
- @ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), (error, docLines, version, updates) =>
- return callback(error) if error?
- @joined = true
- @doc.catchUp( updates )
- callback()
- else
- @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version) =>
- return callback(error) if error?
- @joined = true
- @doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket
- @_bindToShareJsDocEvents()
- callback()
-
- _leaveDoc: (callback = (error) ->) ->
- @ide.socket.emit 'leaveDoc', @doc_id, (error) =>
- return callback(error) if error?
- @joined = false
- for callback in @_leaveCallbacks or []
- callback(error)
- delete @_leaveCallbacks
- callback(error)
-
- _cleanUp: () ->
- delete Document.openDocs[@doc_id]
- @unBindFromEditorEvents()
- @unBindFromSocketEvents()
-
- _bindToShareJsDocEvents: () ->
- @doc.on "error", (error, meta) => @_onError error, meta
- @doc.on "externalUpdate", () =>
- @ide.pushEvent "externalUpdate",
- doc_id: @doc_id
- @trigger "externalUpdate"
- @doc.on "remoteop", () =>
- @ide.pushEvent "remoteop",
- doc_id: @doc_id
- @trigger "remoteop"
- @doc.on "op:sent", (op) =>
- @ide.pushEvent "op:sent",
- doc_id: @doc_id
- op: op
- @trigger "op:sent"
- @doc.on "op:acknowledged", (op) =>
- @ide.pushEvent "op:acknowledged",
- doc_id: @doc_id
- op: op
- @trigger "op:acknowledged"
- @doc.on "op:timeout", (op) =>
- @ide.pushEvent "op:timeout",
- doc_id: @doc_id
- op: op
- @trigger "op:timeout"
- ga?('send', 'event', 'error', "op timeout", "Op was now acknowledged - #{ide.socket.socket.transport.name}" )
- @ide.connectionManager.reconnectImmediately()
- @doc.on "flush", (inflightOp, pendingOp, version) =>
- @ide.pushEvent "flush",
- doc_id: @doc_id,
- inflightOp: inflightOp,
- pendingOp: pendingOp
- v: version
-
- _onError: (error, meta = {}) ->
- console.error "ShareJS error", error, meta
- ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" )
- @ide.socket.disconnect()
- meta.doc_id = @doc_id
- @ide.reportError(error, meta)
- @doc?.clearInflightAndPendingOps()
- @_cleanUp()
- @trigger "error", error
-
- _.extend(Document::, Backbone.Events)
-
- return Document
-
-
-
-
diff --git a/services/web/public/coffee/editor/Editor.coffee b/services/web/public/coffee/editor/Editor.coffee
deleted file mode 100644
index 62fa4daa28..0000000000
--- a/services/web/public/coffee/editor/Editor.coffee
+++ /dev/null
@@ -1,374 +0,0 @@
-define [
- "editor/Document"
- "undo/UndoManager"
- "utils/Modal"
- "ace/ace"
- "ace/edit_session"
- "ace/mode/latex"
- "ace/range"
- "ace/keyboard/vim"
- "ace/keyboard/emacs"
- "libs/backbone"
- "libs/jquery.storage"
-], (Document, UndoManager, Modal) ->
- AceEditor = require("ace/ace")
- EditSession = require('ace/edit_session').EditSession
- LatexMode = require("ace/mode/latex").Mode
- Range = require("ace/range").Range
- Vim = require("ace/keyboard/vim").handler
- Emacs = require("ace/keyboard/emacs").handler
- keybindings = ace: null, vim: Vim, emacs: Emacs
-
- class Editor
- templates:
- editorPanel: $("#editorPanelTemplate").html()
- loadingIndicator: $("#loadingIndicatorTemplate").html()
-
- viewOptions: {flatView:"flatView", splitView:"splitView"}
- currentViewState: undefined
- compilationErrors: {}
-
- constructor: (@ide) ->
- _.extend @, Backbone.Events
- @editorPanel = $(@templates.editorPanel)
- @ide.mainAreaManager.addArea
- identifier: "editor"
- element: @editorPanel
- @initializeEditor()
- @bindToFileTreeEvents()
- @enable()
- @loadingIndicator = $(@templates.loadingIndicator)
- @editorPanel.find("#editor").append(@loadingIndicator)
- @leftPanel = @editorPanel.find("#leftEditorPanel")
- @rightPanel = @editorPanel.find("#rightEditorPanel")
- @initSplitView()
- @switchToFlatView()
-
- bindToFileTreeEvents: () ->
- @ide.fileTreeManager.on "open:doc", (doc_id, options = {}) =>
- if @enabled
- @openDoc doc_id, options
-
- initSplitView: () ->
- @$splitter = splitter = @editorPanel.find("#editorSplitter")
- options =
- spacing_open: 8
- spacing_closed: 16
- east:
- size: "50%"
- maskIframesOnResize: true
- onresize: () =>
- @trigger("resize")
-
- if (state = $.localStorage("layout.editor"))?
- options.east = state.east
-
- splitter.layout options
-
- $(window).unload () =>
- @_saveSplitterState()
-
- _saveSplitterState: () ->
- if $("#editorSplitter").is(":visible")
- state = $("#editorSplitter").layout().readState()
- eastWidth = state.east.size + $("#editorSplitter .ui-layout-resizer-east").width()
- percentWidth = eastWidth / $("#editorSplitter").width() * 100 + "%"
- state.east.size = percentWidth
- $.localStorage("layout.editor", state)
-
- switchToSplitView: () ->
- if @currentViewState != @viewOptions.splitView
- @currentViewState = @viewOptions.splitView
- @leftPanel.prepend(
- @editorPanel.find("#editorWrapper")
- )
- splitter = @editorPanel.find("#editorSplitter")
- splitter.show()
- @ide.layoutManager.resizeAllSplitters()
-
- switchToFlatView: () ->
- if @currentViewState != @viewOptions.flatView
- @_saveSplitterState()
- @currentViewState = @viewOptions.flatView
- @editorPanel.prepend(
- @editorPanel.find("#editorWrapper")
- )
- @editorPanel.find("#editorSplitter").hide()
- @aceEditor.resize(true)
-
- showLoading: () ->
- delay = 600 # ms
- @loading = true
- setTimeout ( =>
- if @loading
- @loadingIndicator.show()
- ), delay
-
- hideLoading: () ->
- @loading = false
- @loadingIndicator.hide()
-
- showUndoConflictWarning: () ->
- $("#editor").prepend($("#undoConflictWarning"))
- $("#undoConflictWarning").show()
- hideBtn = $("#undoConflictWarning .js-hide")
- hideBtn.off("click")
- hideBtn.on "click", (e) ->
- e.preventDefault()
- $("#undoConflictWarning").hide()
- if @hideUndoWarningTimeout?
- clearTimeout @hideUndoWarningTimeout
- delete @hideUndoWarningTimeout
- @hideUndoWarningTimeout = setTimeout ->
- $("#undoConflictWarning").fadeOut("slow")
- , 4000
-
-
- initializeEditor: () ->
- @aceEditor = aceEditor = AceEditor.edit("editor")
-
- @on "resize", => @aceEditor.resize()
- @ide.layoutManager.on "resize", => @trigger "resize"
-
- mode = window.userSettings.mode
- theme = window.userSettings.theme
-
- chosenKeyBindings = keybindings[mode]
- aceEditor.setKeyboardHandler(chosenKeyBindings)
- aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
- aceEditor.setShowPrintMargin(false)
-
- # Prevert Ctrl|Cmd-S from triggering save dialog
- aceEditor.commands.addCommand
- name: "save",
- bindKey: win: "Ctrl-S", mac: "Command-S"
- exec: () ->
- readOnly: true
- aceEditor.commands.removeCommand "transposeletters"
- aceEditor.commands.removeCommand "showSettingsMenu"
- aceEditor.commands.removeCommand "foldall"
-
- aceEditor.showCommandLine = (args...) =>
- @trigger "showCommandLine", aceEditor, args...
-
- aceEditor.on "dblclick", (e) => @trigger "dblclick", e
- aceEditor.on "click", (e) => @trigger "click", e
- aceEditor.on "mousemove", (e) =>
- position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
- e.position = position
- @trigger "mousemove", e
-
- setIdeToEditorPanel: (options = {}) ->
- @aceEditor.focus()
- @aceEditor.resize()
- loadDocument = =>
- @refreshCompilationErrors()
-
- @ide.layoutManager.resizeAllSplitters()
-
- if options.line?
- @gotoLine(options.line)
- else
- pos = $.localStorage("doc.position.#{@current_doc_id}") || {}
- @ignoreCursorPositionChanges = true
- @setCursorPosition(pos.cursorPosition or {row: 0, column: 0})
- @setScrollTop(pos.scrollTop or 0)
- @ignoreCursorPositionChanges = false
- @ide.mainAreaManager.change 'editor', =>
- setTimeout loadDocument, 0
-
- refreshCompilationErrors: () ->
- @getSession().setAnnotations @compilationErrors[@current_doc_id]
-
- openDoc: (doc_id, options = {}) ->
- if @current_doc_id == doc_id && !options.forceReopen
- @setIdeToEditorPanel(line: options.line)
- else
- @showLoading()
- @current_doc_id = doc_id
- @_openNewDocument doc_id, (error, document) =>
- if error?
- @ide.showGenericServerErrorMessage()
- return
-
- @setIdeToEditorPanel(line: options.line)
- @hideLoading()
- @trigger "change:doc", @getSession()
-
- _openNewDocument: (doc_id, callback = (error, document) ->) ->
- if @document?
- @document.leaveAndCleanUp()
- @_unbindFromDocumentEvents(@document)
- @_detachDocumentFromEditor(@document)
-
- @document = Document.getDocument @ide, doc_id
-
- @document.join (error) =>
- return callback(error) if error?
- @_bindToDocumentEvents(@document)
- @_bindDocumentToEditor(@document)
- callback null, @document
-
- _bindToDocumentEvents: (document) ->
- document.on "remoteop", () =>
- @undoManager.nextUpdateIsRemote = true
-
- document.on "error", (error) =>
- @openDoc(document.doc_id, forceReopen: true)
-
- Modal.createModal
- title: "Out of sync"
- message: "Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
- buttons:[
- text: "Ok"
- ]
-
- document.on "externalUpdate", () =>
- Modal.createModal
- title: "Document Updated Externally"
- message: "This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history."
- buttons:[
- text: "Ok"
- ]
-
- _unbindFromDocumentEvents: (document) ->
- document.off()
-
- _bindDocumentToEditor: (document) ->
- $("#editor").show()
- @_bindNewDocToAce(document)
-
- _detachDocumentFromEditor: (document) ->
- document.detachFromAce()
-
- _bindNewDocToAce: (document) ->
- @_createNewSessionFromDocLines(document.getSnapshot().split("\n"))
- @_setReadWritePermission()
- @_bindToAceEvents()
-
- # Updating the doc can cause the cursor to jump around
- # but we shouldn't record that
- @ignoreCursorPositionChanges = true
- document.attachToAce(@aceEditor)
- @ignoreCursorPositionChanges = false
-
- _bindToAceEvents: () ->
- aceDoc = @getDocument()
- aceDoc.on 'change', (change) => @onDocChange(change)
-
- session = @getSession()
- session.on "changeScrollTop", (e) => @onScrollTopChange(e)
- session.selection.on 'changeCursor', (e) => @onCursorChange(e)
-
- _createNewSessionFromDocLines: (docLines) ->
- @aceEditor.setSession(new EditSession(docLines))
- session = @getSession()
- session.setUseWrapMode(true)
- session.setMode(new LatexMode())
- @undoManager = new UndoManager(@)
- session.setUndoManager @undoManager
-
- _setReadWritePermission: () ->
- if !@ide.isAllowedToDoIt 'readAndWrite'
- @makeReadOnly()
- else
- @makeWritable()
-
- onDocChange: (change) ->
- @lastUpdated = new Date()
- @trigger "update:doc", change
-
- onScrollTopChange: (event) ->
- @trigger "scroll", event
- if !@ignoreCursorPositionChanges
- docPosition = $.localStorage("doc.position.#{@current_doc_id}") || {}
- docPosition.scrollTop = @getScrollTop()
- $.localStorage("doc.position.#{@current_doc_id}", docPosition)
-
- onCursorChange: (event) ->
- @trigger "cursor:change", event
- if !@ignoreCursorPositionChanges
- docPosition = $.localStorage("doc.position.#{@current_doc_id}") || {}
- docPosition.cursorPosition = @getCursorPosition()
- $.localStorage("doc.position.#{@current_doc_id}", docPosition)
-
- makeReadOnly: () ->
- @aceEditor.setReadOnly true
-
- makeWritable: () ->
- @aceEditor.setReadOnly false
-
- getSession: () -> @aceEditor.getSession()
-
- getDocument: () -> @getSession().getDocument()
-
- gotoLine: (line) ->
- @aceEditor.gotoLine(line)
-
- getCurrentLine: () ->
- @aceEditor.selection?.getCursor()?.row
-
- getCurrentColumn: () ->
- @aceEditor.selection?.getCursor()?.column
-
- getLines: (from, to) ->
- if from? and to?
- @getSession().doc.getLines(from, to)
- else
- @getSession().doc.getAllLines()
-
- addMarker: (position, klass, type, inFront) ->
- range = new Range(
- position.row, position.column,
- position.row, position.column + position.length
- )
- @getSession().addMarker range, klass, type, inFront
-
- removeMarker: (markerId) ->
- @getSession().removeMarker markerId
-
- getCursorPosition: () -> @aceEditor.getCursorPosition()
- setCursorPosition: (pos) -> @aceEditor.moveCursorToPosition(pos)
-
- getScrollTop: () -> @getSession().getScrollTop()
- setScrollTop: (pos) -> @getSession().setScrollTop(pos)
-
- replaceText: (range, text) ->
- @getSession().replace(new Range(
- range.start.row, range.start.column,
- range.end.row, range.end.column
- ), text)
-
- getContainerElement: () ->
- $(@aceEditor.renderer.getContainerElement())
-
- getCursorElement: () ->
- @getContainerElement().find(".ace_cursor")
-
- textToEditorCoordinates: (x, y) ->
- editorAreaOffset = @getContainerElement().offset()
- {pageX, pageY} = @aceEditor.renderer.textToScreenCoordinates(x, y)
- return {
- x: pageX - editorAreaOffset.left
- y: pageY - editorAreaOffset.top
- }
-
- chaosMonkey: (line = 0, char = "a") ->
- @_cm = setInterval () =>
- @aceEditor.session.insert({row: line, column: 0}, char)
- , 100
-
- clearChaosMonkey: () ->
- clearInterval @_cm
-
- getCurrentDocId: () ->
- @current_doc_id
-
- enable: () ->
- @enabled = true
-
- disable: () ->
- @enabled = false
-
- hasUnsavedChanges: () ->
- Document.hasUnsavedChanges()
diff --git a/services/web/public/coffee/editor/ShareJSHeader.coffee b/services/web/public/coffee/editor/ShareJSHeader.coffee
deleted file mode 100644
index d9944f25cc..0000000000
--- a/services/web/public/coffee/editor/ShareJSHeader.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-WEB = true
-window.sharejs = exports = {}
-types = exports.types = {}
diff --git a/services/web/public/coffee/editor/ShareJsDoc.coffee b/services/web/public/coffee/editor/ShareJsDoc.coffee
deleted file mode 100644
index f264fe6a9c..0000000000
--- a/services/web/public/coffee/editor/ShareJsDoc.coffee
+++ /dev/null
@@ -1,127 +0,0 @@
-define [
- "libs/sharejs"
- "libs/backbone"
-], (ShareJs) ->
- class ShareJsDoc
- constructor: (@doc_id, docLines, version, @socket) ->
- # Dencode any binary bits of data
- # See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
- @type = "text"
- docLines = for line in docLines
- if line.text?
- @type = "json"
- line.text = decodeURIComponent(escape(line.text))
- else
- @type = "text"
- line = decodeURIComponent(escape(line))
- line
-
- if @type == "text"
- snapshot = docLines.join("\n")
- else if @type == "json"
- snapshot = { lines: docLines }
- else
- throw new Error("Unknown type: #{@type}")
-
- @connection = {
- send: (update) =>
- @_startInflightOpTimeout(update)
- @socket.emit "applyOtUpdate", @doc_id, update
- state: "ok"
- id: @socket.socket.sessionid
- }
-
- @_doc = new ShareJs.Doc @connection, @doc_id,
- type: @type
- @_doc.on "change", () =>
- @trigger "change"
- @_doc.on "acknowledge", () =>
- @trigger "acknowledge"
- @_doc.on "remoteop", () =>
- @trigger "remoteop"
-
- @_bindToDocChanges(@_doc)
-
- @processUpdateFromServer
- open: true
- v: version
- snapshot: snapshot
-
- submitOp: (args...) -> @_doc.submitOp(args...)
-
- processUpdateFromServer: (message) ->
- try
- @_doc._onMessage message
- catch error
- # Version mismatches are thrown as errors
- @_handleError(error)
-
- if message?.meta?.type == "external"
- @trigger "externalUpdate"
-
- catchUp: (updates) ->
- for update, i in updates
- update.v = @_doc.version
- update.doc = @doc_id
- @processUpdateFromServer(update)
-
- getSnapshot: () -> @_doc.snapshot
- getVersion: () -> @_doc.version
- getType: () -> @type
-
- clearInflightAndPendingOps: () ->
- @_doc.inflightOp = null
- @_doc.inflightCallbacks = []
- @_doc.pendingOp = null
- @_doc.pendingCallbacks = []
-
- flushPendingOps: () ->
- # This will flush any ops that are pending.
- # If there is an inflight op it will do nothing.
- @_doc.flush()
-
- updateConnectionState: (state) ->
- @connection.state = state
- @connection.id = @socket.socket.sessionid
- @_doc.autoOpen = false
- @_doc._connectionStateChanged(state)
-
- hasBufferedOps: () ->
- @_doc.inflightOp? or @_doc.pendingOp?
-
- getInflightOp: () -> @_doc.inflightOp
- getPendingOp: () -> @_doc.pendingOp
-
- attachToAce: (ace) -> @_doc.attach_ace(ace)
- detachFromAce: () -> @_doc.detach_ace?()
-
- INFLIGHT_OP_TIMEOUT: 10000
- _startInflightOpTimeout: (update) ->
- meta =
- v: update.v
- op_sent_at: new Date()
- timer = setTimeout () =>
- @trigger "op:timeout", update
- , @INFLIGHT_OP_TIMEOUT
- @_doc.inflightCallbacks.push () =>
- clearTimeout timer
-
- _handleError: (error, meta = {}) ->
- @trigger "error", error, meta
-
- _bindToDocChanges: (doc) ->
- submitOp = doc.submitOp
- doc.submitOp = (args...) =>
- @trigger "op:sent", args...
- doc.pendingCallbacks.push () =>
- @trigger "op:acknowledged", args...
- submitOp.apply(doc, args)
-
- flush = doc.flush
- doc.flush = (args...) =>
- @trigger "flush", doc.inflightOp, doc.pendingOp, doc.version
- flush.apply(doc, args)
-
- _.extend(ShareJsDoc::, Backbone.Events)
-
- return ShareJsDoc
diff --git a/services/web/public/coffee/editor/sharejs/client/ace.coffee b/services/web/public/coffee/editor/sharejs/client/ace.coffee
deleted file mode 100644
index f85a1c9982..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/ace.coffee
+++ /dev/null
@@ -1,130 +0,0 @@
-# This is some utility code to connect an ace editor to a sharejs document.
-
-Range = require("ace/range").Range
-
-# Convert an ace delta into an op understood by share.js
-applyToShareJS = (editorDoc, delta, doc) ->
- # Get the start position of the range, in no. of characters
- getStartOffsetPosition = (range) ->
- # This is quite inefficient - getLines makes a copy of the entire
- # lines array in the document. It would be nice if we could just
- # access them directly.
- lines = editorDoc.getLines 0, range.start.row
-
- offset = 0
-
- for line, i in lines
- offset += if i < range.start.row
- line.length
- else
- range.start.column
-
- # Add the row number to include newlines.
- offset + range.start.row
-
- pos = getStartOffsetPosition(delta.range)
-
- switch delta.action
- when 'insertText' then doc.insert pos, delta.text
- when 'removeText' then doc.del pos, delta.text.length
-
- when 'insertLines'
- text = delta.lines.join('\n') + '\n'
- doc.insert pos, text
-
- when 'removeLines'
- text = delta.lines.join('\n') + '\n'
- doc.del pos, text.length
-
- else throw new Error "unknown action: #{delta.action}"
-
- return
-
-# Attach an ace editor to the document. The editor's contents are replaced
-# with the document's contents unless keepEditorContents is true. (In which case the document's
-# contents are nuked and replaced with the editor's).
-window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents) ->
- throw new Error 'Only text documents can be attached to ace' unless @provides['text']
-
- doc = this
- editorDoc = editor.getSession().getDocument()
- editorDoc.setNewLineMode 'unix'
-
- check = ->
- window.setTimeout ->
- editorText = editorDoc.getValue()
- otText = doc.getText()
-
- if editorText != otText
- console.error "Text does not match!"
- console.error "editor: #{editorText}"
- console.error "ot: #{otText}"
- # Should probably also replace the editor text with the doc snapshot.
- , 0
-
- # MODIFIED by James: We will set the doc contents ourselves to
- # avoid an extra entry in the undo stack.
- # if keepEditorContents
- # doc.del 0, doc.getText().length
- # doc.insert 0, editorDoc.getValue()
- # else
- # editorDoc.setValue doc.getText()
-
- check()
-
- # When we apply ops from sharejs, ace emits edit events. We need to ignore those
- # to prevent an infinite typing loop.
- suppress = false
-
- # Listen for edits in ace
- editorListener = (change) ->
- return if suppress
- applyToShareJS editorDoc, change.data, doc
-
- check()
-
- editorDoc.on 'change', editorListener
-
- # Listen for remote ops on the sharejs document
- docListener = (op) ->
- suppress = true
- applyToDoc editorDoc, op
- suppress = false
-
- check()
-
-
- # Horribly inefficient.
- offsetToPos = (offset) ->
- # Again, very inefficient.
- lines = editorDoc.getAllLines()
-
- row = 0
- for line, row in lines
- break if offset <= line.length
-
- # +1 for the newline.
- offset -= lines[row].length + 1
-
- row:row, column:offset
-
- doc.on 'insert', (pos, text) ->
- suppress = true
- editorDoc.insert offsetToPos(pos), text
- suppress = false
- check()
-
- doc.on 'delete', (pos, text) ->
- suppress = true
- range = Range.fromPoints offsetToPos(pos), offsetToPos(pos + text.length)
- editorDoc.remove range
- suppress = false
- check()
-
- doc.detach_ace = ->
- doc.removeListener 'remoteop', docListener
- editorDoc.removeListener 'change', editorListener
- delete doc.detach_ace
-
- return
-
diff --git a/services/web/public/coffee/editor/sharejs/client/ace.js b/services/web/public/coffee/editor/sharejs/client/ace.js
deleted file mode 100644
index 642210fdea..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/ace.js
+++ /dev/null
@@ -1,119 +0,0 @@
-// Generated by CoffeeScript 1.4.0
-(function() {
- var Range, applyToShareJS;
-
- Range = require("ace/range").Range;
-
- applyToShareJS = function(editorDoc, delta, doc) {
- var getStartOffsetPosition, pos, text;
- getStartOffsetPosition = function(range) {
- var i, line, lines, offset, _i, _len;
- lines = editorDoc.getLines(0, range.start.row);
- offset = 0;
- for (i = _i = 0, _len = lines.length; _i < _len; i = ++_i) {
- line = lines[i];
- offset += i < range.start.row ? line.length : range.start.column;
- }
- return offset + range.start.row;
- };
- pos = getStartOffsetPosition(delta.range);
- switch (delta.action) {
- case 'insertText':
- doc.insert(pos, delta.text);
- break;
- case 'removeText':
- doc.del(pos, delta.text.length);
- break;
- case 'insertLines':
- text = delta.lines.join('\n') + '\n';
- doc.insert(pos, text);
- break;
- case 'removeLines':
- text = delta.lines.join('\n') + '\n';
- doc.del(pos, text.length);
- break;
- default:
- throw new Error("unknown action: " + delta.action);
- }
- };
-
- window.sharejs.extendDoc('attach_ace', function(editor, keepEditorContents) {
- var check, doc, docListener, editorDoc, editorListener, offsetToPos, suppress;
- if (!this.provides['text']) {
- throw new Error('Only text documents can be attached to ace');
- }
- doc = this;
- editorDoc = editor.getSession().getDocument();
- editorDoc.setNewLineMode('unix');
- check = function() {
- return window.setTimeout(function() {
- var editorText, otText;
- editorText = editorDoc.getValue();
- otText = doc.getText();
- if (editorText !== otText) {
- console.error("Text does not match!");
- console.error("editor: " + editorText);
- return console.error("ot: " + otText);
- }
- }, 0);
- };
- if (keepEditorContents) {
- doc.del(0, doc.getText().length);
- doc.insert(0, editorDoc.getValue());
- } else {
- editorDoc.setValue(doc.getText());
- }
- check();
- suppress = false;
- editorListener = function(change) {
- if (suppress) {
- return;
- }
- applyToShareJS(editorDoc, change.data, doc);
- return check();
- };
- editorDoc.on('change', editorListener);
- docListener = function(op) {
- suppress = true;
- applyToDoc(editorDoc, op);
- suppress = false;
- return check();
- };
- offsetToPos = function(offset) {
- var line, lines, row, _i, _len;
- lines = editorDoc.getAllLines();
- row = 0;
- for (row = _i = 0, _len = lines.length; _i < _len; row = ++_i) {
- line = lines[row];
- if (offset <= line.length) {
- break;
- }
- offset -= lines[row].length + 1;
- }
- return {
- row: row,
- column: offset
- };
- };
- doc.on('insert', function(pos, text) {
- suppress = true;
- editorDoc.insert(offsetToPos(pos), text);
- suppress = false;
- return check();
- });
- doc.on('delete', function(pos, text) {
- var range;
- suppress = true;
- range = Range.fromPoints(offsetToPos(pos), offsetToPos(pos + text.length));
- editorDoc.remove(range);
- suppress = false;
- return check();
- });
- doc.detach_ace = function() {
- doc.removeListener('remoteop', docListener);
- editorDoc.removeListener('change', editorListener);
- return delete doc.detach_ace;
- };
- });
-
-}).call(this);
diff --git a/services/web/public/coffee/editor/sharejs/client/cm.coffee b/services/web/public/coffee/editor/sharejs/client/cm.coffee
deleted file mode 100644
index 3c79e4ecfb..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/cm.coffee
+++ /dev/null
@@ -1,94 +0,0 @@
-# This is some utility code to connect a CodeMirror editor
-# to a sharejs document.
-# It is heavily inspired from the Ace editor hook.
-
-# Convert a CodeMirror delta into an op understood by share.js
-applyToShareJS = (editorDoc, delta, doc) ->
- # CodeMirror deltas give a text replacement.
- # I tuned this operation a little bit, for speed.
- startPos = 0 # Get character position from # of chars in each line.
- i = 0 # i goes through all lines.
-
- while i < delta.from.line
- startPos += editorDoc.lineInfo(i).text.length + 1 # Add 1 for '\n'
- i++
-
- startPos += delta.from.ch
-
- if delta.to.line == delta.from.line &&
- delta.to.ch == delta.from.ch # Then nothing was removed.
- doc.insert startPos, delta.text.join '\n'
- else
- delLen = delta.to.ch - delta.from.ch
- while i < delta.to.line
- delLen += editorDoc.lineInfo(i).text.length + 1 # Add 1 for '\n'
- i++
- doc.del startPos, delLen
- doc.insert startPos, delta.text.join '\n' if delta.text
-
- applyToShareJS editorDoc, delta.next, doc if delta.next
-
-# Attach a CodeMirror editor to the document. The editor's contents are replaced
-# with the document's contents unless keepEditorContents is true. (In which case
-# the document's contents are nuked and replaced with the editor's).
-window.sharejs.extendDoc 'attach_cm', (editor, keepEditorContents) ->
- unless @provides.text
- throw new Error 'Only text documents can be attached to CodeMirror2'
-
- sharedoc = @
- check = ->
- window.setTimeout ->
- editorText = editor.getValue()
- otText = sharedoc.getValue()
-
- if editorText != otText
- console.error "Text does not match!"
- console.error "editor: #{editorText}"
- console.error "ot: #{otText}"
- # Replace the editor text with the doc snapshot.
- editor.setValue sharedoc.getValue()
- , 0
-
- if keepEditorContents
- @del 0, sharedoc.getText().length
- @insert 0, editor.getValue()
- else
- editor.setValue sharedoc.getText()
-
- check()
-
- # When we apply ops from sharejs, CodeMirror emits edit events.
- # We need to ignore those to prevent an infinite typing loop.
- suppress = false
-
- # Listen for edits in CodeMirror.
- editorListener = (ed, change) ->
- return if suppress
- applyToShareJS editor, change, sharedoc
-
- check()
-
- editor.setOption 'onChange', editorListener
-
- @on 'insert', (pos, text) ->
- suppress = true
- # All the primitives we need are already in CM's API.
- editor.replaceRange text, editor.posFromIndex(pos)
- suppress = false
- check()
-
- @on 'delete', (pos, text) ->
- suppress = true
- from = editor.posFromIndex pos
- to = editor.posFromIndex (pos + text.length)
- editor.replaceRange '', from, to
- suppress = false
- check()
-
- @detach_cm = ->
- # TODO: can we remove the insert and delete event callbacks?
- editor.setOption 'onChange', null
- delete @detach_cm
-
- return
-
diff --git a/services/web/public/coffee/editor/sharejs/client/connection.coffee b/services/web/public/coffee/editor/sharejs/client/connection.coffee
deleted file mode 100644
index 4c77ad4581..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/connection.coffee
+++ /dev/null
@@ -1,167 +0,0 @@
-# A Connection wraps a persistant BC connection to a sharejs server.
-#
-# This class implements the client side of the protocol defined here:
-# https://github.com/josephg/ShareJS/wiki/Wire-Protocol
-#
-# The equivalent server code is in src/server/browserchannel.coffee.
-#
-# This file is a bit of a mess. I'm dreadfully sorry about that. It passes all the tests,
-# so I have hope that its *correct* even if its not clean.
-#
-# Most of Connection exists to support the open() method, which creates a new document
-# reference.
-
-if WEB?
- types = exports.types
- throw new Error 'Must load browserchannel before this library' unless window.BCSocket
- {BCSocket} = window
-else
- types = require '../types'
- {BCSocket} = require 'browserchannel'
- Doc = require('./doc').Doc
-
-class Connection
- constructor: (host) ->
- # Map of docname -> doc
- @docs = {}
-
- # States:
- # - 'connecting': The connection is being established
- # - 'handshaking': The connection has been established, but we don't have the auth ID yet
- # - 'ok': We have connected and recieved our client ID. Ready for data.
- # - 'disconnected': The connection is closed, but it will not reconnect automatically.
- # - 'stopped': The connection is closed, and will not reconnect.
- @state = 'connecting'
- @socket = new BCSocket host, reconnect:true
-
- @socket.onmessage = (msg) =>
- if msg.auth is null
- # Auth failed.
- @lastError = msg.error # 'forbidden'
- @disconnect()
- return @emit 'connect failed', msg.error
- else if msg.auth
- # Our very own client id.
- @id = msg.auth
- @setState 'ok'
- return
-
- docName = msg.doc
-
- if docName isnt undefined
- @lastReceivedDoc = docName
- else
- msg.doc = docName = @lastReceivedDoc
-
- if @docs[docName]
- @docs[docName]._onMessage msg
- else
- console?.error 'Unhandled message', msg
-
- @connected = false
- @socket.onclose = (reason) =>
- #console.warn 'onclose', reason
- @setState 'disconnected', reason
- if reason in ['Closed', 'Stopped by server']
- @setState 'stopped', @lastError or reason
-
- @socket.onerror = (e) =>
- #console.warn 'onerror', e
- @emit 'error', e
-
- @socket.onopen = =>
- #console.warn 'onopen'
- @lastError = @lastReceivedDoc = @lastSentDoc = null
- @setState 'handshaking'
-
- @socket.onconnecting = =>
- #console.warn 'connecting'
- @setState 'connecting'
-
- setState: (state, data) ->
- return if @state is state
- @state = state
-
- delete @id if state is 'disconnected'
- @emit state, data
-
- # Documents could just subscribe to the state change events, but there's less state to
- # clean up when you close a document if I just notify the doucments directly.
- for docName, doc of @docs
- doc._connectionStateChanged state, data
-
- send: (data) ->
- docName = data.doc
-
- if docName is @lastSentDoc
- delete data.doc
- else
- @lastSentDoc = docName
-
- #console.warn 'c->s', data
- @socket.send data
-
- disconnect: ->
- # This will call @socket.onclose(), which in turn will emit the 'disconnected' event.
- #console.warn 'calling close on the socket'
- @socket.close()
-
- # *** Doc management
-
- makeDoc: (name, data, callback) ->
- throw new Error("Doc #{name} already open") if @docs[name]
- doc = new Doc(@, name, data)
- @docs[name] = doc
-
- doc.open (error) =>
- delete @docs[name] if error
- callback error, (doc unless error)
-
- # Open a document that already exists
- # callback(error, doc)
- openExisting: (docName, callback) ->
- return callback 'connection closed' if @state is 'stopped'
- return callback null, @docs[docName] if @docs[docName]
- doc = @makeDoc docName, {}, callback
-
- # Open a document. It will be created if it doesn't already exist.
- # Callback is passed a document or an error
- # type is either a type name (eg 'text' or 'simple') or the actual type object.
- # Types must be supported by the server.
- # callback(error, doc)
- open: (docName, type, callback) ->
- return callback 'connection closed' if @state is 'stopped'
-
- if typeof type is 'function'
- callback = type
- type = 'text'
-
- callback ||= ->
-
- type = types[type] if typeof type is 'string'
-
- throw new Error "OT code for document type missing" unless type
-
- throw new Error 'Server-generated random doc names are not currently supported' unless docName?
-
- if @docs[docName]
- doc = @docs[docName]
- if doc.type == type
- callback null, doc
- else
- callback 'Type mismatch', doc
- return
-
- @makeDoc docName, {create:true, type:type.name}, callback
-
-# Not currently working.
-# create: (type, callback) ->
-# open null, type, callback
-
-# Make connections event emitters.
-unless WEB?
- MicroEvent = require './microevent'
-
-MicroEvent.mixin Connection
-
-exports.Connection = Connection
diff --git a/services/web/public/coffee/editor/sharejs/client/doc.coffee b/services/web/public/coffee/editor/sharejs/client/doc.coffee
deleted file mode 100644
index a8cb7be3c1..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/doc.coffee
+++ /dev/null
@@ -1,327 +0,0 @@
-unless WEB?
- types = require '../types'
-
-if WEB?
- exports.extendDoc = (name, fn) ->
- Doc::[name] = fn
-
-# A Doc is a client's view on a sharejs document.
-#
-# Documents are created by calling Connection.open().
-#
-# Documents are event emitters - use doc.on(eventname, fn) to subscribe.
-#
-# Documents get mixed in with their type's API methods. So, you can .insert('foo', 0) into
-# a text document and stuff like that.
-#
-# Events:
-# - remoteop (op)
-# - changed (op)
-# - acknowledge (op)
-# - error
-# - open, closing, closed. 'closing' is not guaranteed to fire before closed.
-class Doc
- # connection is a Connection object.
- # name is the documents' docName.
- # data can optionally contain known document data, and initial open() call arguments:
- # {v[erson], snapshot={...}, type, create=true/false/undefined}
- # callback will be called once the document is first opened.
- constructor: (@connection, @name, openData) ->
- # Any of these can be null / undefined at this stage.
- openData ||= {}
- @version = openData.v
- @snapshot = openData.snaphot
- @_setType openData.type if openData.type
-
- @state = 'closed'
- @autoOpen = false
-
- # Has the document already been created?
- @_create = openData.create
-
- # The op that is currently roundtripping to the server, or null.
- #
- # When the connection reconnects, the inflight op is resubmitted.
- @inflightOp = null
- @inflightCallbacks = []
- # The auth ids which the client has previously used to attempt to send inflightOp. This is
- # usually empty.
- @inflightSubmittedIds = []
-
- # All ops that are waiting for the server to acknowledge @inflightOp
- @pendingOp = null
- @pendingCallbacks = []
-
- # Some recent ops, incase submitOp is called with an old op version number.
- @serverOps = {}
-
- # Transform a server op by a client op, and vice versa.
- _xf: (client, server) ->
- if @type.transformX
- @type.transformX(client, server)
- else
- client_ = @type.transform client, server, 'left'
- server_ = @type.transform server, client, 'right'
- return [client_, server_]
-
- _otApply: (docOp, isRemote) ->
- oldSnapshot = @snapshot
- @snapshot = @type.apply(@snapshot, docOp)
-
- # Its important that these event handlers are called with oldSnapshot.
- # The reason is that the OT type APIs might need to access the snapshots to
- # determine information about the received op.
- @emit 'change', docOp, oldSnapshot
- @emit 'remoteop', docOp, oldSnapshot if isRemote
-
- _connectionStateChanged: (state, data) ->
- switch state
- when 'disconnected'
- @state = 'closed'
- # This is used by the server to make sure that when an op is resubmitted it
- # doesn't end up getting applied twice.
- @inflightSubmittedIds.push @connection.id if @inflightOp
-
- @emit 'closed'
-
- when 'ok' # Might be able to do this when we're connecting... that would save a roundtrip.
- @open() if @autoOpen
-
- when 'stopped'
- @_openCallback? data
-
- @emit state, data
-
- _setType: (type) ->
- if typeof type is 'string'
- type = types[type]
-
- throw new Error 'Support for types without compose() is not implemented' unless type and type.compose
-
- @type = type
- if type.api
- this[k] = v for k, v of type.api
- @_register?()
- else
- @provides = {}
-
- _onMessage: (msg) ->
- #console.warn 's->c', msg
- if msg.open == true
- # The document has been successfully opened.
- @state = 'open'
- @_create = false # Don't try and create the document again next time open() is called.
- unless @created?
- @created = !!msg.create
-
- @_setType msg.type if msg.type
- if msg.create
- @created = true
- @snapshot = @type.create()
- else
- @created = false unless @created is true
- @snapshot = msg.snapshot if msg.snapshot isnt undefined
-
- @version = msg.v if msg.v?
-
- # Resend any previously queued operation.
- if @inflightOp
- response =
- doc: @name
- op: @inflightOp
- v: @version
- response.dupIfSource = @inflightSubmittedIds if @inflightSubmittedIds.length
- @connection.send response
- else
- @flush()
-
- @emit 'open'
-
- @_openCallback? null
-
- else if msg.open == false
- # The document has either been closed, or an open request has failed.
- if msg.error
- # An error occurred opening the document.
- console?.error "Could not open document: #{msg.error}"
- @emit 'error', msg.error
- @_openCallback? msg.error
-
- @state = 'closed'
- @emit 'closed'
-
- @_closeCallback?()
- @_closeCallback = null
-
- else if msg.op is null and error is 'Op already submitted'
- # We've tried to resend an op to the server, which has already been received successfully. Do nothing.
- # The op will be confirmed normally when we get the op itself was echoed back from the server
- # (handled below).
-
- else if (msg.op is undefined and msg.v isnt undefined) or (msg.op and msg.meta.source in @inflightSubmittedIds)
- # Our inflight op has been acknowledged.
- oldInflightOp = @inflightOp
- @inflightOp = null
- @inflightSubmittedIds.length = 0
-
- error = msg.error
- if error
- # The server has rejected an op from the client for some reason.
- # We'll send the error message to the user and roll back the change.
- #
- # If the server isn't going to allow edits anyway, we should probably
- # figure out some way to flag that (readonly:true in the open request?)
-
- if @type.invert
- undo = @type.invert oldInflightOp
-
- # Now we have to transform the undo operation by any server ops & pending ops
- if @pendingOp
- [@pendingOp, undo] = @_xf @pendingOp, undo
-
- # ... and apply it locally, reverting the changes.
- #
- # This call will also call @emit 'remoteop'. I'm still not 100% sure about this
- # functionality, because its really a local op. Basically, the problem is that
- # if the client's op is rejected by the server, the editor window should update
- # to reflect the undo.
- @_otApply undo, true
- else
- @emit 'error', "Op apply failed (#{error}) and the op could not be reverted"
-
- callback error for callback in @inflightCallbacks
- else
- # The op applied successfully.
- throw new Error('Invalid version from server') unless msg.v == @version
-
- @serverOps[@version] = oldInflightOp
- @version++
- @emit 'acknowledge', oldInflightOp
- callback null, oldInflightOp for callback in @inflightCallbacks
-
- # Send the next op.
- @flush()
-
- else if msg.op
- # We got a new op from the server.
- # msg is {doc:, op:, v:}
-
- # There is a bug in socket.io (produced on firefox 3.6) which causes messages
- # to be duplicated sometimes.
- # We'll just silently drop subsequent messages.
- return if msg.v < @version
-
- return @emit 'error', "Expected docName '#{@name}' but got #{msg.doc}" unless msg.doc == @name
- return @emit 'error', "Expected version #{@version} but got #{msg.v}" unless msg.v == @version
-
- # p "if: #{i @inflightOp} pending: #{i @pendingOp} doc '#{@snapshot}' op: #{i msg.op}"
-
- op = msg.op
- @serverOps[@version] = op
-
- docOp = op
- if @inflightOp != null
- [@inflightOp, docOp] = @_xf @inflightOp, docOp
- if @pendingOp != null
- [@pendingOp, docOp] = @_xf @pendingOp, docOp
-
- @version++
- # Finally, apply the op to @snapshot and trigger any event listeners
- @_otApply docOp, true
-
- else if msg.meta
- {path, value} = msg.meta
-
- switch path?[0]
- when 'shout'
- return @emit 'shout', value
- else
- console?.warn 'Unhandled meta op:', msg
-
- else
- console?.warn 'Unhandled document message:', msg
-
-
- # Send ops to the server, if appropriate.
- #
- # Only one op can be in-flight at a time, so if an op is already on its way then
- # this method does nothing.
- flush: =>
- return unless @connection.state == 'ok' and @inflightOp == null and @pendingOp != null
-
- # Rotate null -> pending -> inflight
- @inflightOp = @pendingOp
- @inflightCallbacks = @pendingCallbacks
-
- @pendingOp = null
- @pendingCallbacks = []
-
- @connection.send {doc:@name, op:@inflightOp, v:@version}
-
- # Submit an op to the server. The op maybe held for a little while before being sent, as only one
- # op can be inflight at any time.
- submitOp: (op, callback) ->
- op = @type.normalize(op) if @type.normalize?
-
- # If this throws an exception, no changes should have been made to the doc
- @snapshot = @type.apply @snapshot, op
-
- if @pendingOp != null
- @pendingOp = @type.compose(@pendingOp, op)
- else
- @pendingOp = op
-
- @pendingCallbacks.push callback if callback
-
- @emit 'change', op
-
- # A timeout is used so if the user sends multiple ops at the same time, they'll be composed
- # & sent together.
- setTimeout @flush, 0
-
- shout: (msg) =>
- # Meta ops don't have to queue, they can go direct. Good/bad idea?
- @connection.send {doc:@name, meta: { path: ['shout'], value: msg } }
-
- # Open a document. The document starts closed.
- open: (callback) ->
- @autoOpen = true
- return unless @state is 'closed'
-
- message =
- doc: @name
- open: true
-
- message.snapshot = null if @snapshot is undefined
- message.type = @type.name if @type
- message.v = @version if @version?
- message.create = true if @_create
-
- @connection.send message
-
- @state = 'opening'
-
- @_openCallback = (error) =>
- @_openCallback = null
- callback? error
-
- # Close a document.
- close: (callback) ->
- @autoOpen = false
- return callback?() if @state is 'closed'
-
- @connection.send {doc:@name, open:false}
-
- # Should this happen immediately or when we get open:false back from the server?
- @state = 'closed'
-
- @emit 'closing'
- @_closeCallback = callback
-
-# Make documents event emitters
-unless WEB?
- MicroEvent = require './microevent'
-
-MicroEvent.mixin Doc
-
-exports.Doc = Doc
diff --git a/services/web/public/coffee/editor/sharejs/client/doc.js b/services/web/public/coffee/editor/sharejs/client/doc.js
deleted file mode 100644
index e59df71583..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/doc.js
+++ /dev/null
@@ -1,332 +0,0 @@
-// Generated by CoffeeScript 1.4.0
-(function() {
- var Doc, MicroEvent, types,
- __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
- __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
-
- if (typeof WEB === "undefined" || WEB === null) {
- types = require('../types');
- }
-
- if (typeof WEB !== "undefined" && WEB !== null) {
- exports.extendDoc = function(name, fn) {
- return Doc.prototype[name] = fn;
- };
- }
-
- Doc = (function() {
-
- function Doc(connection, name, openData) {
- this.connection = connection;
- this.name = name;
- this.shout = __bind(this.shout, this);
-
- this.flush = __bind(this.flush, this);
-
- openData || (openData = {});
- this.version = openData.v;
- this.snapshot = openData.snaphot;
- if (openData.type) {
- this._setType(openData.type);
- }
- this.state = 'closed';
- this.autoOpen = false;
- this._create = openData.create;
- this.inflightOp = null;
- this.inflightCallbacks = [];
- this.inflightSubmittedIds = [];
- this.pendingOp = null;
- this.pendingCallbacks = [];
- this.serverOps = {};
- }
-
- Doc.prototype._xf = function(client, server) {
- var client_, server_;
- if (this.type.transformX) {
- return this.type.transformX(client, server);
- } else {
- client_ = this.type.transform(client, server, 'left');
- server_ = this.type.transform(server, client, 'right');
- return [client_, server_];
- }
- };
-
- Doc.prototype._otApply = function(docOp, isRemote) {
- var oldSnapshot;
- oldSnapshot = this.snapshot;
- this.snapshot = this.type.apply(this.snapshot, docOp);
- this.emit('change', docOp, oldSnapshot);
- if (isRemote) {
- return this.emit('remoteop', docOp, oldSnapshot);
- }
- };
-
- Doc.prototype._connectionStateChanged = function(state, data) {
- switch (state) {
- case 'disconnected':
- this.state = 'closed';
- if (this.inflightOp) {
- this.inflightSubmittedIds.push(this.connection.id);
- }
- this.emit('closed');
- break;
- case 'ok':
- if (this.autoOpen) {
- this.open();
- }
- break;
- case 'stopped':
- if (typeof this._openCallback === "function") {
- this._openCallback(data);
- }
- }
- return this.emit(state, data);
- };
-
- Doc.prototype._setType = function(type) {
- var k, v, _ref;
- if (typeof type === 'string') {
- type = types[type];
- }
- if (!(type && type.compose)) {
- throw new Error('Support for types without compose() is not implemented');
- }
- this.type = type;
- if (type.api) {
- _ref = type.api;
- for (k in _ref) {
- v = _ref[k];
- this[k] = v;
- }
- return typeof this._register === "function" ? this._register() : void 0;
- } else {
- return this.provides = {};
- }
- };
-
- Doc.prototype._onMessage = function(msg) {
- var callback, docOp, error, oldInflightOp, op, path, response, undo, value, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6;
- if (msg.open === true) {
- this.state = 'open';
- this._create = false;
- if (this.created == null) {
- this.created = !!msg.create;
- }
- if (msg.type) {
- this._setType(msg.type);
- }
- if (msg.create) {
- this.created = true;
- this.snapshot = this.type.create();
- } else {
- if (this.created !== true) {
- this.created = false;
- }
- if (msg.snapshot !== void 0) {
- this.snapshot = msg.snapshot;
- }
- }
- if (msg.v != null) {
- this.version = msg.v;
- }
- if (this.inflightOp) {
- response = {
- doc: this.name,
- op: this.inflightOp,
- v: this.version
- };
- if (this.inflightSubmittedIds.length) {
- response.dupIfSource = this.inflightSubmittedIds;
- }
- this.connection.send(response);
- } else {
- this.flush();
- }
- this.emit('open');
- return typeof this._openCallback === "function" ? this._openCallback(null) : void 0;
- } else if (msg.open === false) {
- if (msg.error) {
- if (typeof console !== "undefined" && console !== null) {
- console.error("Could not open document: " + msg.error);
- }
- this.emit('error', msg.error);
- if (typeof this._openCallback === "function") {
- this._openCallback(msg.error);
- }
- }
- this.state = 'closed';
- this.emit('closed');
- if (typeof this._closeCallback === "function") {
- this._closeCallback();
- }
- return this._closeCallback = null;
- } else if (msg.op === null && error === 'Op already submitted') {
-
- } else if ((msg.op === void 0 && msg.v !== void 0) || (msg.op && (_ref = msg.meta.source, __indexOf.call(this.inflightSubmittedIds, _ref) >= 0))) {
- oldInflightOp = this.inflightOp;
- this.inflightOp = null;
- this.inflightSubmittedIds.length = 0;
- error = msg.error;
- if (error) {
- if (this.type.invert) {
- undo = this.type.invert(oldInflightOp);
- if (this.pendingOp) {
- _ref1 = this._xf(this.pendingOp, undo), this.pendingOp = _ref1[0], undo = _ref1[1];
- }
- this._otApply(undo, true);
- } else {
- this.emit('error', "Op apply failed (" + error + ") and the op could not be reverted");
- }
- _ref2 = this.inflightCallbacks;
- for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
- callback = _ref2[_i];
- callback(error);
- }
- } else {
- if (msg.v !== this.version) {
- throw new Error('Invalid version from server');
- }
- this.serverOps[this.version] = oldInflightOp;
- this.version++;
- this.emit('acknowledge', oldInflightOp);
- _ref3 = this.inflightCallbacks;
- for (_j = 0, _len1 = _ref3.length; _j < _len1; _j++) {
- callback = _ref3[_j];
- callback(null, oldInflightOp);
- }
- }
- return this.flush();
- } else if (msg.op) {
- if (msg.v < this.version) {
- return;
- }
- if (msg.doc !== this.name) {
- return this.emit('error', "Expected docName '" + this.name + "' but got " + msg.doc);
- }
- if (msg.v !== this.version) {
- return this.emit('error', "Expected version " + this.version + " but got " + msg.v);
- }
- op = msg.op;
- this.serverOps[this.version] = op;
- docOp = op;
- if (this.inflightOp !== null) {
- _ref4 = this._xf(this.inflightOp, docOp), this.inflightOp = _ref4[0], docOp = _ref4[1];
- }
- if (this.pendingOp !== null) {
- _ref5 = this._xf(this.pendingOp, docOp), this.pendingOp = _ref5[0], docOp = _ref5[1];
- }
- this.version++;
- return this._otApply(docOp, true);
- } else if (msg.meta) {
- _ref6 = msg.meta, path = _ref6.path, value = _ref6.value;
- switch (path != null ? path[0] : void 0) {
- case 'shout':
- return this.emit('shout', value);
- default:
- return typeof console !== "undefined" && console !== null ? console.warn('Unhandled meta op:', msg) : void 0;
- }
- } else {
- return typeof console !== "undefined" && console !== null ? console.warn('Unhandled document message:', msg) : void 0;
- }
- };
-
- Doc.prototype.flush = function() {
- if (!(this.connection.state === 'ok' && this.inflightOp === null && this.pendingOp !== null)) {
- return;
- }
- this.inflightOp = this.pendingOp;
- this.inflightCallbacks = this.pendingCallbacks;
- this.pendingOp = null;
- this.pendingCallbacks = [];
- return this.connection.send({
- doc: this.name,
- op: this.inflightOp,
- v: this.version
- });
- };
-
- Doc.prototype.submitOp = function(op, callback) {
- if (this.type.normalize != null) {
- op = this.type.normalize(op);
- }
- this.snapshot = this.type.apply(this.snapshot, op);
- if (this.pendingOp !== null) {
- this.pendingOp = this.type.compose(this.pendingOp, op);
- } else {
- this.pendingOp = op;
- }
- if (callback) {
- this.pendingCallbacks.push(callback);
- }
- this.emit('change', op);
- return setTimeout(this.flush, 0);
- };
-
- Doc.prototype.shout = function(msg) {
- return this.connection.send({
- doc: this.name,
- meta: {
- path: ['shout'],
- value: msg
- }
- });
- };
-
- Doc.prototype.open = function(callback) {
- var message,
- _this = this;
- this.autoOpen = true;
- if (this.state !== 'closed') {
- return;
- }
- message = {
- doc: this.name,
- open: true
- };
- if (this.snapshot === void 0) {
- message.snapshot = null;
- }
- if (this.type) {
- message.type = this.type.name;
- }
- if (this.version != null) {
- message.v = this.version;
- }
- if (this._create) {
- message.create = true;
- }
- this.connection.send(message);
- this.state = 'opening';
- return this._openCallback = function(error) {
- _this._openCallback = null;
- return typeof callback === "function" ? callback(error) : void 0;
- };
- };
-
- Doc.prototype.close = function(callback) {
- this.autoOpen = false;
- if (this.state === 'closed') {
- return typeof callback === "function" ? callback() : void 0;
- }
- this.connection.send({
- doc: this.name,
- open: false
- });
- this.state = 'closed';
- this.emit('closing');
- return this._closeCallback = callback;
- };
-
- return Doc;
-
- })();
-
- if (typeof WEB === "undefined" || WEB === null) {
- MicroEvent = require('./microevent');
- }
-
- MicroEvent.mixin(Doc);
-
- exports.Doc = Doc;
-
-}).call(this);
diff --git a/services/web/public/coffee/editor/sharejs/client/index.coffee b/services/web/public/coffee/editor/sharejs/client/index.coffee
deleted file mode 100644
index d44741fe79..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/index.coffee
+++ /dev/null
@@ -1,73 +0,0 @@
-# This file implements the sharejs client, as defined here:
-# https://github.com/josephg/ShareJS/wiki/Client-API
-#
-# It works from both a node.js context and a web context (though in the latter case,
-# it needs to be compiled to work.)
-#
-# It should become a little nicer once I start using more of the new RPC features added
-# in socket.io 0.7.
-#
-# Note that anything declared in the global scope here is shared with other files
-# built by closure. Be careful what you put in this namespace.
-
-unless WEB?
- Connection = require('./connection').Connection
-
-# Open a document with the given name. The connection is created implicitly and reused.
-#
-# This function uses a local (private) set of connections to support .open().
-#
-# Open returns the connection its using to access the document.
-exports.open = do ->
- # This is a private connection pool for implicitly created connections.
- connections = {}
-
- getConnection = (origin) ->
- if WEB?
- location = window.location
- origin ?= "#{location.protocol}//#{location.host}/channel"
-
- unless connections[origin]
- c = new Connection origin
-
- del = -> delete connections[origin]
- c.on 'disconnecting', del
- c.on 'connect failed', del
- connections[origin] = c
-
- connections[origin]
-
- # If you're using the bare API, connections are cleaned up as soon as there's no
- # documents using them.
- maybeClose = (c) ->
- numDocs = 0
- for name, doc of c.docs
- numDocs++ if doc.state isnt 'closed' || doc.autoOpen
-
- if numDocs == 0
- c.disconnect()
-
- (docName, type, origin, callback) ->
- if typeof origin == 'function'
- callback = origin
- origin = null
-
- c = getConnection origin
- c.numDocs++
- c.open docName, type, (error, doc) ->
- if error
- callback error
- maybeClose c
- else
- doc.on 'closed', -> maybeClose c
-
- callback null, doc
-
- c.on 'connect failed'
- return c
-
-
-unless WEB?
- exports.Doc = require('./doc').Doc
- exports.Connection = require('./connection').Connection
-
diff --git a/services/web/public/coffee/editor/sharejs/client/microevent.coffee b/services/web/public/coffee/editor/sharejs/client/microevent.coffee
deleted file mode 100644
index 8e5e2bc6ea..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/microevent.coffee
+++ /dev/null
@@ -1,46 +0,0 @@
-# This is a simple port of microevent.js to Coffeescript. I've changed the
-# function names to be consistent with node.js EventEmitter.
-#
-# microevent.js is copyright Jerome Etienne, and licensed under the MIT license:
-# https://github.com/jeromeetienne/microevent.js
-
-nextTick = if WEB? then (fn) -> setTimeout fn, 0 else process['nextTick']
-
-class MicroEvent
- on: (event, fct) ->
- @_events ||= {}
- @_events[event] ||= []
- @_events[event].push(fct)
- this
-
- removeListener: (event, fct) ->
- @_events ||= {}
- listeners = (@_events[event] ||= [])
-
- # Sadly, there's no IE8- support for indexOf.
- i = 0
- while i < listeners.length
- listeners[i] = undefined if listeners[i] == fct
- i++
-
- nextTick => @_events[event] = (x for x in @_events[event] when x)
-
- this
-
- emit: (event, args...) ->
- return this unless @_events?[event]
- fn.apply this, args for fn in @_events[event] when fn
- this
-
-# mixin will delegate all MicroEvent.js function in the destination object
-MicroEvent.mixin = (obj) ->
- proto = obj.prototype || obj
-
- # Damn closure compiler :/
- proto.on = MicroEvent.prototype.on
- proto.removeListener = MicroEvent.prototype.removeListener
- proto.emit = MicroEvent.prototype.emit
- obj
-
-module.exports = MicroEvent unless WEB?
-
diff --git a/services/web/public/coffee/editor/sharejs/client/microevent.js b/services/web/public/coffee/editor/sharejs/client/microevent.js
deleted file mode 100644
index ba429c9c5b..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/microevent.js
+++ /dev/null
@@ -1,85 +0,0 @@
-// Generated by CoffeeScript 1.4.0
-(function() {
- var MicroEvent, nextTick,
- __slice = [].slice;
-
- nextTick = typeof WEB !== "undefined" && WEB !== null ? function(fn) {
- return setTimeout(fn, 0);
- } : process['nextTick'];
-
- MicroEvent = (function() {
-
- function MicroEvent() {}
-
- MicroEvent.prototype.on = function(event, fct) {
- var _base;
- this._events || (this._events = {});
- (_base = this._events)[event] || (_base[event] = []);
- this._events[event].push(fct);
- return this;
- };
-
- MicroEvent.prototype.removeListener = function(event, fct) {
- var i, listeners, _base,
- _this = this;
- this._events || (this._events = {});
- listeners = ((_base = this._events)[event] || (_base[event] = []));
- i = 0;
- while (i < listeners.length) {
- if (listeners[i] === fct) {
- listeners[i] = void 0;
- }
- i++;
- }
- nextTick(function() {
- var x;
- return _this._events[event] = (function() {
- var _i, _len, _ref, _results;
- _ref = this._events[event];
- _results = [];
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- x = _ref[_i];
- if (x) {
- _results.push(x);
- }
- }
- return _results;
- }).call(_this);
- });
- return this;
- };
-
- MicroEvent.prototype.emit = function() {
- var args, event, fn, _i, _len, _ref, _ref1;
- event = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
- if (!((_ref = this._events) != null ? _ref[event] : void 0)) {
- return this;
- }
- _ref1 = this._events[event];
- for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
- fn = _ref1[_i];
- if (fn) {
- fn.apply(this, args);
- }
- }
- return this;
- };
-
- return MicroEvent;
-
- })();
-
- MicroEvent.mixin = function(obj) {
- var proto;
- proto = obj.prototype || obj;
- proto.on = MicroEvent.prototype.on;
- proto.removeListener = MicroEvent.prototype.removeListener;
- proto.emit = MicroEvent.prototype.emit;
- return obj;
- };
-
- if (typeof WEB === "undefined" || WEB === null) {
- module.exports = MicroEvent;
- }
-
-}).call(this);
diff --git a/services/web/public/coffee/editor/sharejs/client/textarea.coffee b/services/web/public/coffee/editor/sharejs/client/textarea.coffee
deleted file mode 100644
index a07150b453..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/textarea.coffee
+++ /dev/null
@@ -1,69 +0,0 @@
-# Create an op which converts oldval -> newval.
-#
-# This function should be called every time the text element is changed. Because changes are
-# always localised, the diffing is quite easy.
-#
-# This algorithm is O(N), but I suspect you could speed it up somehow using regular expressions.
-applyChange = (doc, oldval, newval) ->
- return if oldval == newval
- commonStart = 0
- commonStart++ while oldval.charAt(commonStart) == newval.charAt(commonStart)
-
- commonEnd = 0
- commonEnd++ while oldval.charAt(oldval.length - 1 - commonEnd) == newval.charAt(newval.length - 1 - commonEnd) and
- commonEnd + commonStart < oldval.length and commonEnd + commonStart < newval.length
-
- doc.del commonStart, oldval.length - commonStart - commonEnd unless oldval.length == commonStart + commonEnd
- doc.insert commonStart, newval[commonStart ... newval.length - commonEnd] unless newval.length == commonStart + commonEnd
-
-window.sharejs.extendDoc 'attach_textarea', (elem) ->
- doc = this
- elem.value = @getText()
- prevvalue = elem.value
-
- replaceText = (newText, transformCursor) ->
- newSelection = [
- transformCursor elem.selectionStart
- transformCursor elem.selectionEnd
- ]
-
- scrollTop = elem.scrollTop
- elem.value = newText
- elem.scrollTop = scrollTop if elem.scrollTop != scrollTop
- [elem.selectionStart, elem.selectionEnd] = newSelection
-
- @on 'insert', (pos, text) ->
- transformCursor = (cursor) ->
- if pos < cursor
- cursor + text.length
- else
- cursor
- #for IE8 and Opera that replace \n with \r\n.
- prevvalue = elem.value.replace /\r\n/g, '\n'
- replaceText prevvalue[...pos] + text + prevvalue[pos..], transformCursor
-
- @on 'delete', (pos, text) ->
- transformCursor = (cursor) ->
- if pos < cursor
- cursor - Math.min(text.length, cursor - pos)
- else
- cursor
- #for IE8 and Opera that replace \n with \r\n.
- prevvalue = elem.value.replace /\r\n/g, '\n'
- replaceText prevvalue[...pos] + prevvalue[pos + text.length..], transformCursor
-
- genOp = (event) ->
- onNextTick = (fn) -> setTimeout fn, 0
- onNextTick ->
- if elem.value != prevvalue
- # IE constantly replaces unix newlines with \r\n. ShareJS docs
- # should only have unix newlines.
- prevvalue = elem.value
- applyChange doc, doc.getText(), elem.value.replace /\r\n/g, '\n'
-
- for event in ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste']
- if elem.addEventListener
- elem.addEventListener event, genOp, false
- else
- elem.attachEvent 'on'+event, genOp
-
diff --git a/services/web/public/coffee/editor/sharejs/client/web-prelude.coffee b/services/web/public/coffee/editor/sharejs/client/web-prelude.coffee
deleted file mode 100644
index 9872197bca..0000000000
--- a/services/web/public/coffee/editor/sharejs/client/web-prelude.coffee
+++ /dev/null
@@ -1,13 +0,0 @@
-# This file is included at the top of the compiled client-side javascript
-
-# This way all the modules can add stuff to exports, and for the web client they'll all get exported.
-window.sharejs = exports =
- 'version': '0.5.0'
-
-# This is compiled out when compiled with uglifyjs, but its important for the share.uncompressed.js.
-#
-# Maybe I should rename WEB to __SHAREJS_WEB or something, but its only relevant for testing
-# anyway.
-if typeof WEB == 'undefined'
- # This will put WEB in the global scope in a browser.
- window.WEB = true
diff --git a/services/web/public/coffee/editor/sharejs/index.coffee b/services/web/public/coffee/editor/sharejs/index.coffee
deleted file mode 100644
index 8c2441aa70..0000000000
--- a/services/web/public/coffee/editor/sharejs/index.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-exports.server = require './server'
-exports.client = require './client'
-exports.types = require './types'
-
-exports.version = '0.5.0'
diff --git a/services/web/public/coffee/editor/sharejs/server/browserchannel.coffee b/services/web/public/coffee/editor/sharejs/server/browserchannel.coffee
deleted file mode 100644
index 7a813bb296..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/browserchannel.coffee
+++ /dev/null
@@ -1,333 +0,0 @@
-# This implements the network API for ShareJS.
-#
-# The wire protocol is speccced out here:
-# https://github.com/josephg/ShareJS/wiki/Wire-Protocol
-#
-# When a client connects the server first authenticates it and sends:
-#
-# S: {auth:}
-# or
-# S: {auth:null, error:'forbidden'}
-#
-# After that, the client can open documents:
-#
-# C: {doc:'foo', open:true, snapshot:null, create:true, type:'text'}
-# S: {doc:'foo', open:true, snapshot:{snapshot:'hi there', v:5, meta:{}}, create:false}
-#
-# ...
-#
-# The client can send open requests as soon as the socket has opened - it doesn't need to
-# wait for auth.
-#
-# The wire protocol is documented here:
-# https://github.com/josephg/ShareJS/wiki/Wire-Protocol
-
-browserChannel = require('browserchannel').server
-util = require 'util'
-hat = require 'hat'
-
-syncQueue = require './syncqueue'
-
-# Attach the streaming protocol to the supplied http.Server.
-#
-# Options = {}
-module.exports = (createAgent, options) ->
- options or= {}
-
- browserChannel options, (session) ->
- #console.log "New BC session from #{session.address} with id #{session.id}"
- data =
- headers: session.headers
- remoteAddress: session.address
-
- # This is the user agent through which a connecting client acts. It is set when the
- # session is authenticated. The agent is responsible for making sure client requests are
- # properly authorized, and metadata is kept up to date.
- agent = null
-
- # To save on network traffic, the agent & server can leave out the docName with each message to mean
- # 'same as the last message'
- lastSentDoc = null
- lastReceivedDoc = null
-
- # Map from docName -> {queue, listener if open}
- docState = {}
-
- # We'll only handle one message from each client at a time.
- handleMessage = (query) ->
- #console.log "Message from #{session.id}", query
-
- error = null
- error = 'Invalid docName' unless query.doc is null or typeof query.doc is 'string' or (query.doc is undefined and lastReceivedDoc)
- error = "'create' must be true or missing" unless query.create in [true, undefined]
- error = "'open' must be true, false or missing" unless query.open in [true, false, undefined]
- error = "'snapshot' must be null or missing" unless query.snapshot in [null, undefined]
- error = "'type' invalid" unless query.type is undefined or typeof query.type is 'string'
- error = "'v' invalid" unless query.v is undefined or (typeof query.v is 'number' and query.v >= 0)
-
- if error
- console.warn "Invalid query #{JSON.stringify query} from #{agent.sessionId}: #{error}"
- session.abort()
- return callback()
-
- # The agent can specify null as the docName to get a random doc name.
- if query.doc is null
- query.doc = lastReceivedDoc = hat()
- else if query.doc != undefined
- lastReceivedDoc = query.doc
- else
- unless lastReceivedDoc
- console.warn "msg.doc missing in query #{JSON.stringify query} from #{agent.sessionId}"
- # The disconnect handler will be called when we do this, which will clean up the open docs.
- return session.abort()
-
- query.doc = lastReceivedDoc
-
- docState[query.doc] or= queue: syncQueue (query, callback) ->
- # When the session is closed, we'll nuke docState. When that happens, no more messages
- # should be handled.
- return callback() unless docState
-
- # Close messages are {open:false}
- if query.open == false
- handleClose query, callback
-
- # Open messages are {open:true}. There's a lot of shared logic with getting snapshots
- # and creating documents. These operations can be done together; and I'll handle them
- # together.
- else if query.open or query.snapshot is null or query.create
- # You can open, request a snapshot and create all in the same
- # request. They're all handled together.
- handleOpenCreateSnapshot query, callback
-
- # The socket is submitting an op.
- else if query.op? or query.meta?.path?
- handleOp query, callback
-
- else
- console.warn "Invalid query #{JSON.stringify query} from #{agent.sessionId}"
- session.abort()
- callback()
-
- # ... And add the message to the queue.
- docState[query.doc].queue query
-
-
- # # Some utility methods for message handlers
-
- # Send a message to the socket.
- # msg _must_ have the doc:DOCNAME property set. We'll remove it if its the same as lastReceivedDoc.
- send = (response) ->
- if response.doc is lastSentDoc
- delete response.doc
- else
- lastSentDoc = response.doc
-
- # Its invalid to send a message to a closed session. We'll silently drop messages if the
- # session has closed.
- if session.state isnt 'closed'
- #console.log "Sending", response
- session.send response
-
- # Open the given document name, at the requested version.
- # callback(error, version)
- open = (docName, version, callback) ->
- return callback 'Session closed' unless docState
- return callback 'Document already open' if docState[docName].listener
- #p "Registering listener on #{docName} by #{socket.id} at #{version}"
-
- docState[docName].listener = listener = (opData) ->
- throw new Error 'Consistency violation - doc listener invalid' unless docState[docName].listener == listener
-
- #p "listener doc:#{docName} opdata:#{i opData} v:#{version}"
-
- # Skip the op if this socket sent it.
- return if opData.meta.source is agent.sessionId
-
- opMsg =
- doc: docName
- op: opData.op
- v: opData.v
- meta: opData.meta
-
- send opMsg
-
- # Tell the socket the doc is open at the requested version
- agent.listen docName, version, listener, (error, v) ->
- delete docState[docName].listener if error
- callback error, v
-
- # Close the named document.
- # callback([error])
- close = (docName, callback) ->
- #p "Closing #{docName}"
- return callback 'Session closed' unless docState
- listener = docState[docName].listener
- return callback 'Doc already closed' unless listener?
-
- agent.removeListener docName
- delete docState[docName].listener
- callback()
-
- # Handles messages with any combination of the open:true, create:true and snapshot:null parameters
- handleOpenCreateSnapshot = (query, finished) ->
- docName = query.doc
- msg = doc:docName
-
- callback = (error) ->
- if error
- close(docName) if msg.open == true
- msg.open = false if query.open == true
- msg.snapshot = null if query.snapshot != undefined
- delete msg.create
-
- msg.error = error
-
- send msg
- finished()
-
- return callback 'No docName specified' unless query.doc?
-
- if query.create == true
- if typeof query.type != 'string'
- return callback 'create:true requires type specified'
-
- if query.meta != undefined
- unless typeof query.meta == 'object' and Array.isArray(query.meta) == false
- return callback 'meta must be an object'
-
- docData = undefined
-
- # This is implemented with a series of cascading methods for each different type of
- # thing this method can handle. This would be so much nicer with an async library. Welcome to
- # callback hell.
-
- step1Create = ->
- return step2Snapshot() if query.create != true
-
- # The document obviously already exists if we have a snapshot.
- if docData
- msg.create = false
- step2Snapshot()
- else
- agent.create docName, query.type, query.meta || {}, (error) ->
- if error is 'Document already exists'
- # We've called getSnapshot (-> null), then create (-> already exists). Its possible
- # another agent has called create() between our getSnapshot and create() calls.
- agent.getSnapshot docName, (error, data) ->
- return callback error if error
-
- docData = data
- msg.create = false
- step2Snapshot()
- else if error
- callback error
- else
- msg.create = true
- step2Snapshot()
-
- # The socket requested a document snapshot
- step2Snapshot = ->
-# if query.create or query.open or query.snapshot == null
-# msg.meta = docData.meta
-
- # Skip inserting a snapshot if the document was just created.
- if query.snapshot != null or msg.create == true
- step3Open()
- return
-
- if docData
- msg.v = docData.v
- msg.type = docData.type.name unless query.type == docData.type.name
- msg.snapshot = docData.snapshot
- else
- return callback 'Document does not exist'
-
- step3Open()
-
- # Attempt to open a document with a given name. Version is optional.
- # callback(opened at version) or callback(null, errormessage)
- step3Open = ->
- return callback() if query.open != true
-
- # Verify the type matches
- return callback 'Type mismatch' if query.type and docData and query.type != docData.type.name
-
- open docName, query.v, (error, version) ->
- return callback error if error
-
- # + Should fail if the type is wrong.
-
- #p "Opened #{docName} at #{version} by #{socket.id}"
- msg.open = true
- msg.v = version
- callback()
-
- # Technically, we don't need a snapshot if the user called create but not open or createSnapshot,
- # but no clients do that yet anyway.
- if query.snapshot == null or query.open == true #and query.type
- agent.getSnapshot query.doc, (error, data) ->
- return callback error if error and error != 'Document does not exist'
-
- docData = data
- step1Create()
- else
- step1Create()
-
- # The socket closes a document
- handleClose = (query, callback) ->
- close query.doc, (error) ->
- if error
- # An error closing still results in the doc being closed.
- send {doc:query.doc, open:false, error:error}
- else
- send {doc:query.doc, open:false}
-
- callback()
-
- # We received an op from the socket
- handleOp = (query, callback) ->
- # ...
- #throw new Error 'No version specified' unless query.v?
-
- opData = {v:query.v, op:query.op, meta:query.meta, dupIfSource:query.dupIfSource}
-
- # If it's a metaOp don't send a response
- agent.submitOp query.doc, opData, if (not opData.op? and opData.meta?.path?) then callback else (error, appliedVersion) ->
- msg = if error
- #p "Sending error to socket: #{error}"
- {doc:query.doc, v:null, error:error}
- else
- {doc:query.doc, v:appliedVersion}
-
- send msg
- callback()
-
- # We don't process any messages from the agent until they've authorized. Instead,
- # they are stored in this buffer.
- buffer = []
- session.on 'message', bufferMsg = (msg) -> buffer.push msg
-
- createAgent data, (error, agent_) ->
- if error
- # The client is not authorized, so they shouldn't try and reconnect.
- session.send {auth:null, error}
- session.stop()
- else
- agent = agent_
- session.send auth:agent.sessionId
-
- # Ok. Now we can handle all the messages in the buffer. They'll go straight to
- # handleMessage from now on.
- session.removeListener 'message', bufferMsg
- handleMessage msg for msg in buffer
- buffer = null
- session.on 'message', handleMessage
-
- session.on 'close', ->
- return unless agent
- #console.log "Client #{agent.sessionId} disconnected"
- for docName, {listener} of docState
- agent.removeListener docName if listener
- docState = null
-
diff --git a/services/web/public/coffee/editor/sharejs/server/db/couchdb.coffee b/services/web/public/coffee/editor/sharejs/server/db/couchdb.coffee
deleted file mode 100644
index 02d27b7351..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/db/couchdb.coffee
+++ /dev/null
@@ -1,149 +0,0 @@
-# OT storage for CouchDB
-# Author: Max Ogden (@maxogden)
-#
-# The couchdb database contains two kinds of documents:
-#
-# - Document snapshots have a key which is doc:the document name
-# - Document ops have a random key, but docName: defined.
-
-request = require('request').defaults json: true
-
-# Helper method to parse errors out of couchdb. There's way more ways
-# things can go wrong, but I think this catches all the ones I care about.
-#
-# callback(error) or callback()
-parseError = (err, resp, body, callback) ->
- body = body[0] if Array.isArray body and body.length >= 1
-
- if err
- # This indicates an HTTP error
- callback err
- else if resp.statusCode is 404
- callback 'Document does not exist'
- else if resp.statusCode is 403
- callback 'forbidden'
- else if typeof body is 'object'
- if body.error is 'conflict'
- callback 'Document already exists'
- else if body.error
- callback "#{body.error} reason: #{body.reason}"
- else
- callback()
- else
- callback()
-
-module.exports = (options) ->
- options ?= {}
- db = options.uri or "http://localhost:5984/sharejs"
-
- uriForDoc = (docName) -> "#{db}/doc:#{encodeURIComponent docName}"
- uriForOps = (docName, start, end, include_docs) ->
- startkey = encodeURIComponent(JSON.stringify [docName, start])
- # {} is sorted after all numbers - so this will get all ops in the case that end is null.
- endkey = encodeURIComponent(JSON.stringify [docName, end ? {}])
-
- # Another way to write this method would be to use node's builtin uri-encoder.
- extra = if include_docs then '&include_docs=true' else ''
- "#{db}/_design/sharejs/_view/operations?startkey=#{startkey}&endkey=#{endkey}&inclusive_end=false#{extra}"
-
- # Helper method to get the revision of a document snapshot.
- getRev = (docName, dbMeta, callback) ->
- if dbMeta?.rev
- callback null, dbMeta.rev
- else
- # JSON defaults to true, and that makes request think I'm trying to sneak a request
- # body in. Ugh.
- request.head {uri:uriForDoc(docName), json:false}, (err, resp, body) ->
- parseError err, resp, body, (error) ->
- if error
- callback error
- else
- # The etag is the rev in quotes.
- callback null, JSON.parse(resp.headers.etag)
-
- writeSnapshotInternal = (docName, data, rev, callback) ->
- body = data
- body.fieldType = 'Document'
- body._rev = rev if rev?
-
- request.put uri:(uriForDoc docName), body:body, (err, resp, body) ->
- parseError err, resp, body, (error) ->
- if error
- #console.log 'create error'
- # This will send write conflicts as 'document already exists'. Thats kinda wierd, but
- # it shouldn't happen anyway
- callback? error
- else
- # We pass the document revision back to the db cache so it can give it back to couchdb on subsequent requests.
- callback? null, {rev: body.rev}
-
- # getOps returns all ops between start and end. end can be null.
- getOps: (docName, start, end, callback) ->
- return callback null, [] if start == end
-
- # Its a bit gross having this end parameter here....
- endkey = if end? then [docName, end - 1]
-
- request uriForOps(docName, start, end), (err, resp, body) ->
- # Rows look like this:
- # {"id":"","key":["doc name",0],"value":{"op":[{"p":0,"i":"hi"}],"meta":{}}}
- data = ({op: row.value.op, meta: row.value.meta} for row in body.rows)
- callback null, data
-
- # callback(error, db metadata)
- create: (docName, data, callback) ->
- writeSnapshotInternal docName, data, null, callback
-
- delete: del = (docName, dbMeta, callback) ->
- getRev docName, dbMeta, (error, rev) ->
- return callback? error if error
-
- docs = [{_id:"doc:#{docName}", _rev:rev, _deleted:true}]
- # Its annoying, but we need to get the revision from the document. I don't think there's a simple way to do this.
- # This request will get all the ops twice.
- request uriForOps(docName, 0, null, true), (err, resp, body) ->
- # Rows look like this:
- # {"id":"","key":["doc name",0],"value":{"op":[{"p":0,"i":"hi"}],"meta":{}},
- # "doc":{"_id":"","_rev":"1-21a40c56ebd5d424ffe56950e77bc847","op":[{"p":0,"i":"hi"}],"v":0,"meta":{},"docName":"doc6"}}
- for row in body.rows
- row.doc._deleted = true
- docs.push row.doc
-
- request.post url: "#{db}/_bulk_docs", body: {docs}, (err, resp, body) ->
- if body[0].error is 'conflict'
- # Somebody has edited the document since we did a GET on the revision information. Recurse.
- # By passing null to dbMeta I'm forcing the revision information to be reacquired.
- del docName, null, callback
- else
- parseError err, resp, body, (error) -> callback? error
-
- writeOp: (docName, opData, callback) ->
- body =
- docName: docName
- op: opData.op
- v: opData.v
- meta: opData.meta
-
- request.post url:db, body:body, (err, resp, body) ->
- parseError err, resp, body, callback
-
- writeSnapshot: (docName, docData, dbMeta, callback) ->
- getRev docName, dbMeta, (error, rev) ->
- return callback? error if error
-
- writeSnapshotInternal docName, docData, rev, callback
-
- getSnapshot: (docName, callback) ->
- request uriForDoc(docName), (err, resp, body) ->
- parseError err, resp, body, (error) ->
- if error
- callback error
- else
- callback null,
- snapshot: body.snapshot
- type: body.type
- meta: body.meta
- v: body.v
- , {rev: body._rev} # dbMeta
-
- close: ->
diff --git a/services/web/public/coffee/editor/sharejs/server/db/index.coffee b/services/web/public/coffee/editor/sharejs/server/db/index.coffee
deleted file mode 100644
index cd11c66939..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/db/index.coffee
+++ /dev/null
@@ -1,28 +0,0 @@
-# This is a simple switch for the different database implementations.
-#
-# The interface is the same as the regular database implementations, except
-# the options object can have another type: parameter which specifies
-# which type of database to use.
-#
-# Example usage:
-# require('server/db').create {type:'redis'}
-
-defaultType = 'redis'
-
-module.exports = (options) ->
- options ?= {}
- type = options.type ? defaultType
-
- console.warn "Database type: 'memory' detected. This has been deprecated and will
- be removed in a future version. Use 'none' instead, or just remove the db:{} block
- from your options." if type is 'memory'
-
- if type in ['none', 'memory']
- null
- else
- Db = switch type
- when 'redis' then require './redis'
- when 'couchdb' then require './couchdb'
- when 'pg' then require './pg'
- else throw new Error "Invalid or unsupported database type: '#{type}'"
- new Db options
diff --git a/services/web/public/coffee/editor/sharejs/server/db/pg.coffee b/services/web/public/coffee/editor/sharejs/server/db/pg.coffee
deleted file mode 100644
index 26c1e465b9..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/db/pg.coffee
+++ /dev/null
@@ -1,198 +0,0 @@
-# This is an implementation of the OT data backend for PostgreSQL. It requires
-# that you have two tables defined in your schema: one for the snapshots
-# and one for the operations. You must also install the 'pg' package.
-#
-#
-# Example usage:
-#
-# var connect = require('connect');
-# var share = require('share').server;
-#
-# var server = connect(connect.logger());
-#
-# var options = {
-# db: {
-# type: 'pg',
-# uri: 'tcp://josh:@localhost/sharejs',
-# create_tables_automatically: true
-# }
-# };
-#
-# share.attach(server, options);
-# server.listen(9000);
-#
-# You can run bin/setup_pg to create the SQL tables initially.
-
-pg = require('pg').native
-
-defaultOptions =
- schema: 'sharejs'
- create_tables_automatically: true
- operations_table: 'ops'
- snapshot_table: 'snapshots'
-
-module.exports = PgDb = (options) ->
- return new Db if !(this instanceof PgDb)
-
- options ?= {}
- options[k] ?= v for k, v of defaultOptions
-
- client = new pg.Client options.uri
- client.connect()
-
- snapshot_table = options.schema and "#{options.schema}.#{options.snapshot_table}" or options.snapshot_table
- operations_table = options.schema and "#{options.schema}.#{options.operations_table}" or options.operations_table
-
- @close = ->
- client.end()
-
- @initialize = (callback) ->
- console.warn 'Creating postgresql database tables'
-
- sql = """
- CREATE SCHEMA #{options.schema};
-
- CREATE TABLE #{snapshot_table} (
- doc text NOT NULL,
- v int4 NOT NULL,
- type text NOT NULL,
- snapshot text NOT NULL,
- meta text NOT NULL,
- created_at timestamp(6) NOT NULL,
- CONSTRAINT snapshots_pkey PRIMARY KEY (doc, v)
- );
-
- CREATE TABLE #{operations_table} (
- doc text NOT NULL,
- v int4 NOT NULL,
- op text NOT NULL,
- meta text NOT NULL,
- CONSTRAINT operations_pkey PRIMARY KEY (doc, v)
- );
- """
- client.query sql, (error, result) ->
- callback? error?.message
-
- # This will perminantly delete all data in the database.
- @dropTables = (callback) ->
- sql = "DROP SCHEMA #{options.schema} CASCADE;"
- client.query sql, (error, result) ->
- callback? error.message
-
- @create = (docName, docData, callback) ->
- sql = """
- INSERT INTO #{snapshot_table} ("doc", "v", "snapshot", "meta", "type", "created_at")
- VALUES ($1, $2, $3, $4, $5, now())
- """
- values = [docName, docData.v, JSON.stringify(docData.snapshot), JSON.stringify(docData.meta), docData.type]
- client.query sql, values, (error, result) ->
- if !error?
- callback?()
- else if error.toString().match "duplicate key value violates unique constraint"
- callback? "Document already exists"
- else
- callback? error?.message
-
- @delete = (docName, dbMeta, callback) ->
- sql = """
- DELETE FROM #{operations_table}
- WHERE "doc" = $1
- RETURNING *
- """
- values = [docName]
- client.query sql, values, (error, result) ->
- if !error?
- sql = """
- DELETE FROM #{snapshot_table}
- WHERE "doc" = $1
- RETURNING *
- """
- client.query sql, values, (error, result) ->
- if !error? and result.rows.length > 0
- callback?()
- else if !error?
- callback? "Document does not exist"
- else
- callback? error?.message
- else
- callback? error?.message
-
- @getSnapshot = (docName, callback) ->
- sql = """
- SELECT *
- FROM #{snapshot_table}
- WHERE "doc" = $1
- ORDER BY "v" DESC
- LIMIT 1
- """
- values = [docName]
- client.query sql, values, (error, result) ->
- if !error? and result.rows.length > 0
- row = result.rows[0]
- data =
- v: row.v
- snapshot: JSON.parse(row.snapshot)
- meta: JSON.parse(row.meta)
- type: row.type
- callback? null, data
- else if !error?
- callback? "Document does not exist"
- else
- callback? error?.message
-
- @writeSnapshot = (docName, docData, dbMeta, callback) ->
- sql = """
- UPDATE #{snapshot_table}
- SET "v" = $2, "snapshot" = $3, "meta" = $4
- WHERE "doc" = $1
- """
- values = [docName, docData.v, JSON.stringify(docData.snapshot), JSON.stringify(docData.meta)]
- client.query sql, values, (error, result) ->
- if !error?
- callback?()
- else
- callback? error?.message
-
- @getOps = (docName, start, end, callback) ->
- end = if end? then end - 1 else 2147483647
- sql = """
- SELECT *
- FROM #{operations_table}
- WHERE "v" BETWEEN $1 AND $2
- AND "doc" = $3
- ORDER BY "v" ASC
- """
- values = [start, end, docName]
- client.query sql, values, (error, result) ->
- if !error?
- data = result.rows.map (row) ->
- return {
- op: JSON.parse row.op
- # v: row.version
- meta: JSON.parse row.meta
- }
- callback? null, data
- else
- callback? error?.message
-
- @writeOp = (docName, opData, callback) ->
- sql = """
- INSERT INTO #{operations_table} ("doc", "op", "v", "meta")
- VALUES ($1, $2, $3, $4)
- """
- values = [docName, JSON.stringify(opData.op), opData.v, JSON.stringify(opData.meta)]
- client.query sql, values, (error, result) ->
- if !error?
- callback?()
- else
- callback? error?.message
-
- # Immediately try and create the database tables if need be. Its possible that a query
- # which happens immediately will happen before the database has been initialized.
- #
- # But, its not really a big problem.
- if options.create_tables_automatically
- client.query "SELECT * from #{snapshot_table} LIMIT 0", (error, result) =>
- @initialize() if error?.message.match "does not exist"
-
- this
diff --git a/services/web/public/coffee/editor/sharejs/server/db/redis.coffee b/services/web/public/coffee/editor/sharejs/server/db/redis.coffee
deleted file mode 100644
index 6be198e390..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/db/redis.coffee
+++ /dev/null
@@ -1,140 +0,0 @@
-# This is an implementation of the OT data backend for redis.
-# http://redis.io/
-#
-# This implementation isn't written to support multiple frontends
-# talking to a single redis backend using redis's transactions.
-
-redis = require 'redis'
-
-defaultOptions = {
- # Prefix for all database keys.
- prefix: 'ShareJS:'
-
- # Inherit the default options from redis. (Hostname: 127.0.0.1, port: 6379)
- hostname: null
- port: null
- redisOptions: null
- auth: null
-
- # If this is set to true, the client will select db 15 and wipe all data in
- # this database.
- testing: false
-}
-
-# Valid options as above.
-module.exports = RedisDb = (options) ->
- return new Db if !(this instanceof RedisDb)
-
- options ?= {}
- options[k] ?= v for k, v of defaultOptions
-
- keyForOps = (docName) -> "#{options.prefix}ops:#{docName}"
- keyForDoc = (docName) -> "#{options.prefix}doc:#{docName}"
-
- client = redis.createClient options.port, options.hostname, options.redisOptions
-
- if options.auth and typeof options.auth == "string"
- client.auth(if ":" in options.auth then options.auth.split(":").pop() else options.auth)
-
- client.select 15 if options.testing
-
- # Creates a new document.
- # data = {snapshot, type:typename, [meta]}
- # calls callback(true) if the document was created or callback(false) if a document with that name
- # already exists.
- @create = (docName, data, callback) ->
- value = JSON.stringify(data)
- client.setnx keyForDoc(docName), value, (err, result) ->
- return callback? err if err
-
- if result
- callback?()
- else
- callback? 'Document already exists'
-
- # Get all ops with version = start to version = end. Noninclusive.
- # end is trimmed to the size of the document.
- # If any documents are passed to the callback, the first one has v = start
- # end can be null. If so, returns all documents from start onwards.
- # Each document returned is in the form {op:o, meta:m, v:version}.
- @getOps = (docName, start, end, callback) ->
- if start == end
- callback null, []
- return
-
- # In redis, lrange values are inclusive.
- if end?
- end--
- else
- end = -1
-
- client.lrange keyForOps(docName), start, end, (err, values) ->
- throw err if err?
- ops = (JSON.parse value for value in values)
- callback null, ops
-
- # Write an op to a document.
- #
- # opData = {op:the op to append, v:version, meta:optional metadata object containing author, etc.}
- # callback = callback when op committed
- #
- # opData.v MUST be the subsequent version for the document.
- #
- # This function has UNDEFINED BEHAVIOUR if you call append before calling create().
- # (its either that, or I have _another_ check when you append an op that the document already exists
- # ... and that would slow it down a bit.)
- @writeOp = (docName, opData, callback) ->
- # ****** NOT SAFE FOR MULTIPLE PROCESSES. Rewrite me using transactions or something.
-
- # The version isn't stored.
- json = JSON.stringify {op:opData.op, meta:opData.meta}
- client.rpush keyForOps(docName), json, (err, response) ->
- return callback err if err
-
- if response == opData.v + 1
- callback()
- else
- # The document has been corrupted by the change. For now, throw an exception.
- # Later, rebuild the snapshot.
- callback "Version mismatch in db.append. '#{docName}' is corrupted."
-
- # Write new snapshot data to the database.
- #
- # docData = resultant document snapshot data. {snapshot:s, type:t, meta}
- #
- # The callback just takes an optional error.
- #
- # This function has UNDEFINED BEHAVIOUR if you call append before calling create().
- @writeSnapshot = (docName, docData, dbMeta, callback) ->
- client.set keyForDoc(docName), JSON.stringify(docData), (err, response) ->
- callback? err
-
- # Data = {v, snapshot, type}. Snapshot == null and v = 0 if the document doesn't exist.
- @getSnapshot = (docName, callback) ->
- client.get keyForDoc(docName), (err, response) ->
- throw err if err?
-
- if response != null
- docData = JSON.parse(response)
- callback null, docData
- else
- callback 'Document does not exist'
-
- # Perminantly deletes a document. There is no undo.
- # Callback takes a single argument which is true iff something was deleted.
- @delete = (docName, dbMeta, callback) ->
- client.del keyForOps(docName)
- client.del keyForDoc(docName), (err, response) ->
- throw err if err?
- if callback
- if response == 1
- # Something was deleted.
- callback null
- else
- callback 'Document does not exist'
-
- # Close the connection to the database
- @close = ->
- client.quit()
-
- this
diff --git a/services/web/public/coffee/editor/sharejs/server/index.coffee b/services/web/public/coffee/editor/sharejs/server/index.coffee
deleted file mode 100644
index fdf12751fc..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/index.coffee
+++ /dev/null
@@ -1,57 +0,0 @@
-# The server module...
-
-connect = require 'connect'
-
-Model = require './model'
-createDb = require './db'
-
-rest = require './rest'
-socketio = require './socketio'
-browserChannel = require './browserchannel'
-
-# Create an HTTP server and attach whatever frontends are specified in the options.
-#
-# The model will be created based on options if it is not specified.
-module.exports = create = (options, model = createModel(options)) ->
- attach(connect(), options, model)
-
-# Create an OT document model attached to a database.
-create.createModel = createModel = (options) ->
- dbOptions = options?.db
-
- db = createDb dbOptions
- new Model db, options
-
-# Attach the OT server frontends to the provided Node HTTP server. Use this if you
-# already have a http.Server or https.Server and want to make some URL paths do OT.
-#
-# The options object specifies options for everything. If settings are missing,
-# defaults will be provided.
-#
-# Set options.rest == null or options.socketio == null to turn off that frontend.
-create.attach = attach = (server, options, model = createModel(options)) ->
- options ?= {}
- options.staticpath ?= '/share'
-
- server.model = model
- server.on 'close', -> model.closeDb()
-
- server.use options.staticpath, connect.static("#{__dirname}/../../webclient") if options.staticpath != null
-
- createAgent = require('./useragent') model, options
-
- # The client frontend doesn't get access to the model at all, to make sure security stuff is
- # done properly.
- server.use rest(createAgent, options.rest) if options.rest != null
-
- # Socketio frontend is now disabled by default.
- socketio.attach(server, createAgent, options.socketio or {}) if options.socketio?
-
- if options.browserChannel != null
- options.browserChannel ?= {}
- #options.browserChannel.base ?= '/sjs'
- options.browserChannel.server = server
- server.use browserChannel(createAgent, options.browserChannel)
-
- server
-
diff --git a/services/web/public/coffee/editor/sharejs/server/model.coffee b/services/web/public/coffee/editor/sharejs/server/model.coffee
deleted file mode 100644
index 284d6fd770..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/model.coffee
+++ /dev/null
@@ -1,603 +0,0 @@
-# The model of all the ops. Responsible for applying & transforming remote deltas
-# and managing the storage layer.
-#
-# Actual storage is handled by the database wrappers in db/*, wrapped by DocCache
-
-{EventEmitter} = require 'events'
-
-queue = require './syncqueue'
-types = require '../types'
-
-isArray = (o) -> Object.prototype.toString.call(o) == '[object Array]'
-
-# This constructor creates a new Model object. There will be one model object
-# per server context.
-#
-# The model object is responsible for a lot of things:
-#
-# - It manages the interactions with the database
-# - It maintains (in memory) a set of all active documents
-# - It calls out to the OT functions when necessary
-#
-# The model is an event emitter. It emits the following events:
-#
-# create(docName, data): A document has been created with the specified name & data
-module.exports = Model = (db, options) ->
- # db can be null if the user doesn't want persistance.
-
- return new Model(db, options) if !(this instanceof Model)
-
- model = this
-
- options ?= {}
-
- # This is a cache of 'live' documents.
- #
- # The cache is a map from docName -> {
- # ops:[{op, meta}]
- # snapshot
- # type
- # v
- # meta
- # eventEmitter
- # reapTimer
- # committedVersion: v
- # snapshotWriteLock: bool to make sure writeSnapshot isn't re-entrant
- # dbMeta: database specific data
- # opQueue: syncQueue for processing ops
- # }
- #
- # The ops list contains the document's last options.numCachedOps ops. (Or all
- # of them if we're using a memory store).
- #
- # Documents are stored in this set so long as the document has been accessed in
- # the last few seconds (options.reapTime) OR at least one client has the document
- # open. I don't know if I should keep open (but not being edited) documents live -
- # maybe if a client has a document open but the document isn't being edited, I should
- # flush it from the cache.
- #
- # In any case, the API to model is designed such that if we want to change that later
- # it should be pretty easy to do so without any external-to-the-model code changes.
- docs = {}
-
- # This is a map from docName -> [callback]. It is used when a document hasn't been
- # cached and multiple getSnapshot() / getVersion() requests come in. All requests
- # are added to the callback list and called when db.getSnapshot() returns.
- #
- # callback(error, snapshot data)
- awaitingGetSnapshot = {}
-
- # The time that documents which no clients have open will stay in the cache.
- # Should be > 0.
- options.reapTime ?= 3000
-
- # The number of operations the cache holds before reusing the space
- options.numCachedOps ?= 10
-
- # This option forces documents to be reaped, even when there's no database backend.
- # This is useful when you don't care about persistance and don't want to gradually
- # fill memory.
- #
- # You might want to set reapTime to a day or something.
- options.forceReaping ?= false
-
- # Until I come up with a better strategy, we'll save a copy of the document snapshot
- # to the database every ~20 submitted ops.
- options.opsBeforeCommit ?= 20
-
- # It takes some processing time to transform client ops. The server will punt ops back to the
- # client to transform if they're too old.
- options.maximumAge ?= 40
-
- # **** Cache API methods
-
- # Its important that all ops are applied in order. This helper method creates the op submission queue
- # for a single document. This contains the logic for transforming & applying ops.
- makeOpQueue = (docName, doc) -> queue (opData, callback) ->
- return callback 'Version missing' unless opData.v >= 0
- return callback 'Op at future version' if opData.v > doc.v
-
- # Punt the transforming work back to the client if the op is too old.
- return callback 'Op too old' if opData.v + options.maximumAge < doc.v
-
- opData.meta ||= {}
- opData.meta.ts = Date.now()
-
- # We'll need to transform the op to the current version of the document. This
- # calls the callback immediately if opVersion == doc.v.
- getOps docName, opData.v, doc.v, (error, ops) ->
- return callback error if error
-
- unless doc.v - opData.v == ops.length
- # This should never happen. It indicates that we didn't get all the ops we
- # asked for. Its important that the submitted op is correctly transformed.
- console.error "Could not get old ops in model for document #{docName}"
- console.error "Expected ops #{opData.v} to #{doc.v} and got #{ops.length} ops"
- return callback 'Internal error'
-
- if ops.length > 0
- try
- # If there's enough ops, it might be worth spinning this out into a webworker thread.
- for oldOp in ops
- # Dup detection works by sending the id(s) the op has been submitted with previously.
- # If the id matches, we reject it. The client can also detect the op has been submitted
- # already if it sees its own previous id in the ops it sees when it does catchup.
- if oldOp.meta.source and opData.dupIfSource and oldOp.meta.source in opData.dupIfSource
- return callback 'Op already submitted'
-
- opData.op = doc.type.transform opData.op, oldOp.op, 'left'
- opData.v++
- catch error
- console.error error.stack
- return callback error.message
-
- try
- snapshot = doc.type.apply doc.snapshot, opData.op
- catch error
- console.error error.stack
- return callback error.message
-
- # The op data should be at the current version, and the new document data should be at
- # the next version.
- #
- # This should never happen in practice, but its a nice little check to make sure everything
- # is hunky-dory.
- unless opData.v == doc.v
- # This should never happen.
- console.error "Version mismatch detected in model. File a ticket - this is a bug."
- console.error "Expecting #{opData.v} == #{doc.v}"
- return callback 'Internal error'
-
- #newDocData = {snapshot, type:type.name, v:opVersion + 1, meta:docData.meta}
- writeOp = db?.writeOp or (docName, newOpData, callback) -> callback()
-
- writeOp docName, opData, (error) ->
- if error
- # The user should probably know about this.
- console.warn "Error writing ops to database: #{error}"
- return callback error
-
- options.stats?.writeOp?()
-
- # This is needed when we emit the 'change' event, below.
- oldSnapshot = doc.snapshot
-
- # All the heavy lifting is now done. Finally, we'll update the cache with the new data
- # and (maybe!) save a new document snapshot to the database.
-
- doc.v = opData.v + 1
- doc.snapshot = snapshot
-
- doc.ops.push opData
- doc.ops.shift() if db and doc.ops.length > options.numCachedOps
-
- model.emit 'applyOp', docName, opData, snapshot, oldSnapshot
- doc.eventEmitter.emit 'op', opData, snapshot, oldSnapshot
-
- # The callback is called with the version of the document at which the op was applied.
- # This is the op.v after transformation, and its doc.v - 1.
- callback null, opData.v
-
- # I need a decent strategy here for deciding whether or not to save the snapshot.
- #
- # The 'right' strategy looks something like "Store the snapshot whenever the snapshot
- # is smaller than the accumulated op data". For now, I'll just store it every 20
- # ops or something. (Configurable with doc.committedVersion)
- if !doc.snapshotWriteLock and doc.committedVersion + options.opsBeforeCommit <= doc.v
- tryWriteSnapshot docName, (error) ->
- console.warn "Error writing snapshot #{error}. This is nonfatal" if error
-
- # Add the data for the given docName to the cache. The named document shouldn't already
- # exist in the doc set.
- #
- # Returns the new doc.
- add = (docName, error, data, committedVersion, ops, dbMeta) ->
- callbacks = awaitingGetSnapshot[docName]
- delete awaitingGetSnapshot[docName]
-
- if error
- callback error for callback in callbacks if callbacks
- else
- doc = docs[docName] =
- snapshot: data.snapshot
- v: data.v
- type: data.type
- meta: data.meta
-
- # Cache of ops
- ops: ops or []
-
- eventEmitter: new EventEmitter
-
- # Timer before the document will be invalidated from the cache (if the document has no
- # listeners)
- reapTimer: null
-
- # Version of the snapshot thats in the database
- committedVersion: committedVersion ? data.v
- snapshotWriteLock: false
- dbMeta: dbMeta
-
- doc.opQueue = makeOpQueue docName, doc
-
- refreshReapingTimeout docName
- model.emit 'add', docName, data
- callback null, doc for callback in callbacks if callbacks
-
- doc
-
- # This is a little helper wrapper around db.getOps. It does two things:
- #
- # - If there's no database set, it returns an error to the callback
- # - It adds version numbers to each op returned from the database
- # (These can be inferred from context so the DB doesn't store them, but its useful to have them).
- getOpsInternal = (docName, start, end, callback) ->
- return callback? 'Document does not exist' unless db
-
- db.getOps docName, start, end, (error, ops) ->
- return callback? error if error
-
- v = start
- op.v = v++ for op in ops
-
- callback? null, ops
-
- # Load the named document into the cache. This function is re-entrant.
- #
- # The callback is called with (error, doc)
- load = (docName, callback) ->
- if docs[docName]
- # The document is already loaded. Return immediately.
- options.stats?.cacheHit? 'getSnapshot'
- return callback null, docs[docName]
-
- # We're a memory store. If we don't have it, nobody does.
- return callback 'Document does not exist' unless db
-
- callbacks = awaitingGetSnapshot[docName]
-
- # The document is being loaded already. Add ourselves as a callback.
- return callbacks.push callback if callbacks
-
- options.stats?.cacheMiss? 'getSnapshot'
-
- # The document isn't loaded and isn't being loaded. Load it.
- awaitingGetSnapshot[docName] = [callback]
- db.getSnapshot docName, (error, data, dbMeta) ->
- return add docName, error if error
-
- type = types[data.type]
- unless type
- console.warn "Type '#{data.type}' missing"
- return callback "Type not found"
- data.type = type
-
- committedVersion = data.v
-
- # The server can close without saving the most recent document snapshot.
- # In this case, there are extra ops which need to be applied before
- # returning the snapshot.
- getOpsInternal docName, data.v, null, (error, ops) ->
- return callback error if error
-
- if ops.length > 0
- console.log "Catchup #{docName} #{data.v} -> #{data.v + ops.length}"
-
- try
- for op in ops
- data.snapshot = type.apply data.snapshot, op.op
- data.v++
- catch e
- # This should never happen - it indicates that whats in the
- # database is invalid.
- console.error "Op data invalid for #{docName}: #{e.stack}"
- return callback 'Op data invalid'
-
- model.emit 'load', docName, data
- add docName, error, data, committedVersion, ops, dbMeta
-
- # This makes sure the cache contains a document. If the doc cache doesn't contain
- # a document, it is loaded from the database and stored.
- #
- # Documents are stored so long as either:
- # - They have been accessed within the past #{PERIOD}
- # - At least one client has the document open
- refreshReapingTimeout = (docName) ->
- doc = docs[docName]
- return unless doc
-
- # I want to let the clients list be updated before this is called.
- process.nextTick ->
- # This is an awkward way to find out the number of clients on a document. If this
- # causes performance issues, add a numClients field to the document.
- #
- # The first check is because its possible that between refreshReapingTimeout being called and this
- # event being fired, someone called delete() on the document and hence the doc is something else now.
- if doc == docs[docName] and
- doc.eventEmitter.listeners('op').length == 0 and
- (db or options.forceReaping) and
- doc.opQueue.busy is false
-
- clearTimeout doc.reapTimer
- doc.reapTimer = reapTimer = setTimeout ->
- tryWriteSnapshot docName, ->
- # If the reaping timeout has been refreshed while we're writing the snapshot, or if we're
- # in the middle of applying an operation, don't reap.
- delete docs[docName] if docs[docName].reapTimer is reapTimer and doc.opQueue.busy is false
- , options.reapTime
-
- tryWriteSnapshot = (docName, callback) ->
- return callback?() unless db
-
- doc = docs[docName]
-
- # The doc is closed
- return callback?() unless doc
-
- # The document is already saved.
- return callback?() if doc.committedVersion is doc.v
-
- return callback? 'Another snapshot write is in progress' if doc.snapshotWriteLock
-
- doc.snapshotWriteLock = true
-
- options.stats?.writeSnapshot?()
-
- writeSnapshot = db?.writeSnapshot or (docName, docData, dbMeta, callback) -> callback()
-
- data =
- v: doc.v
- meta: doc.meta
- snapshot: doc.snapshot
- # The database doesn't know about object types.
- type: doc.type.name
-
- # Commit snapshot.
- writeSnapshot docName, data, doc.dbMeta, (error, dbMeta) ->
- doc.snapshotWriteLock = false
-
- # We have to use data.v here because the version in the doc could
- # have been updated between the call to writeSnapshot() and now.
- doc.committedVersion = data.v
- doc.dbMeta = dbMeta
-
- callback? error
-
- # *** Model interface methods
-
- # Create a new document.
- #
- # data should be {snapshot, type, [meta]}. The version of a new document is 0.
- @create = (docName, type, meta, callback) ->
- [meta, callback] = [{}, meta] if typeof meta is 'function'
-
- return callback? 'Invalid document name' if docName.match /\//
- return callback? 'Document already exists' if docs[docName]
-
- type = types[type] if typeof type == 'string'
- return callback? 'Type not found' unless type
-
- data =
- snapshot:type.create()
- type:type.name
- meta:meta or {}
- v:0
-
- done = (error, dbMeta) ->
- # dbMeta can be used to cache extra state needed by the database to access the document, like an ID or something.
- return callback? error if error
-
- # From here on we'll store the object version of the type name.
- data.type = type
- add docName, null, data, 0, [], dbMeta
- model.emit 'create', docName, data
- callback?()
-
- if db
- db.create docName, data, done
- else
- done()
-
- # Perminantly deletes the specified document.
- # If listeners are attached, they are removed.
- #
- # The callback is called with (error) if there was an error. If error is null / undefined, the
- # document was deleted.
- #
- # WARNING: This isn't well supported throughout the code. (Eg, streaming clients aren't told about the
- # deletion. Subsequent op submissions will fail).
- @delete = (docName, callback) ->
- doc = docs[docName]
-
- if doc
- clearTimeout doc.reapTimer
- delete docs[docName]
-
- done = (error) ->
- model.emit 'delete', docName unless error
- callback? error
-
- if db
- db.delete docName, doc?.dbMeta, done
- else
- done (if !doc then 'Document does not exist')
-
- # This gets all operations from [start...end]. (That is, its not inclusive.)
- #
- # end can be null. This means 'get me all ops from start'.
- #
- # Each op returned is in the form {op:o, meta:m, v:version}.
- #
- # Callback is called with (error, [ops])
- #
- # If the document does not exist, getOps doesn't necessarily return an error. This is because
- # its awkward to figure out whether or not the document exists for things
- # like the redis database backend. I guess its a bit gross having this inconsistant
- # with the other DB calls, but its certainly convenient.
- #
- # Use getVersion() to determine if a document actually exists, if thats what you're
- # after.
- @getOps = getOps = (docName, start, end, callback) ->
- # getOps will only use the op cache if its there. It won't fill the op cache in.
- throw new Error 'start must be 0+' unless start >= 0
-
- [end, callback] = [null, end] if typeof end is 'function'
-
- ops = docs[docName]?.ops
-
- if ops
- version = docs[docName].v
-
- # Ops contains an array of ops. The last op in the list is the last op applied
- end ?= version
- start = Math.min start, end
-
- return callback null, [] if start == end
-
- # Base is the version number of the oldest op we have cached
- base = version - ops.length
-
- # If the database is null, we'll trim to the ops we do have and hope thats enough.
- if start >= base or db is null
- refreshReapingTimeout docName
- options.stats?.cacheHit 'getOps'
-
- return callback null, ops[(start - base)...(end - base)]
-
- options.stats?.cacheMiss 'getOps'
-
- getOpsInternal docName, start, end, callback
-
- # Gets the snapshot data for the specified document.
- # getSnapshot(docName, callback)
- # Callback is called with (error, {v: , type: , snapshot: , meta: })
- @getSnapshot = (docName, callback) ->
- load docName, (error, doc) ->
- callback error, if doc then {v:doc.v, type:doc.type, snapshot:doc.snapshot, meta:doc.meta}
-
- # Gets the latest version # of the document.
- # getVersion(docName, callback)
- # callback is called with (error, version).
- @getVersion = (docName, callback) ->
- load docName, (error, doc) -> callback error, doc?.v
-
- # Apply an op to the specified document.
- # The callback is passed (error, applied version #)
- # opData = {op:op, v:v, meta:metadata}
- #
- # Ops are queued before being applied so that the following code applies op C before op B:
- # model.applyOp 'doc', OPA, -> model.applyOp 'doc', OPB
- # model.applyOp 'doc', OPC
- @applyOp = (docName, opData, callback) ->
- # All the logic for this is in makeOpQueue, above.
- load docName, (error, doc) ->
- return callback error if error
-
- process.nextTick -> doc.opQueue opData, (error, newVersion) ->
- refreshReapingTimeout docName
- callback? error, newVersion
-
- # TODO: store (some) metadata in DB
- # TODO: op and meta should be combineable in the op that gets sent
- @applyMetaOp = (docName, metaOpData, callback) ->
- {path, value} = metaOpData.meta
-
- return callback? "path should be an array" unless isArray path
-
- load docName, (error, doc) ->
- if error?
- callback? error
- else
- applied = false
- switch path[0]
- when 'shout'
- doc.eventEmitter.emit 'op', metaOpData
- applied = true
-
- model.emit 'applyMetaOp', docName, path, value if applied
- callback? null, doc.v
-
- # Listen to all ops from the specified version. If version is in the past, all
- # ops since that version are sent immediately to the listener.
- #
- # The callback is called once the listener is attached, but before any ops have been passed
- # to the listener.
- #
- # This will _not_ edit the document metadata.
- #
- # If there are any listeners, we don't purge the document from the cache. But be aware, this behaviour
- # might change in a future version.
- #
- # version is the document version at which the document is opened. It can be left out if you want to open
- # the document at the most recent version.
- #
- # listener is called with (opData) each time an op is applied.
- #
- # callback(error, openedVersion)
- @listen = (docName, version, listener, callback) ->
- [version, listener, callback] = [null, version, listener] if typeof version is 'function'
-
- load docName, (error, doc) ->
- return callback? error if error
-
- clearTimeout doc.reapTimer
-
- if version?
- getOps docName, version, null, (error, data) ->
- return callback? error if error
-
- doc.eventEmitter.on 'op', listener
- callback? null, version
- for op in data
- listener op
-
- # The listener may well remove itself during the catchup phase. If this happens, break early.
- # This is done in a quite inefficient way. (O(n) where n = #listeners on doc)
- break unless listener in doc.eventEmitter.listeners 'op'
-
- else # Version is null / undefined. Just add the listener.
- doc.eventEmitter.on 'op', listener
- callback? null, doc.v
-
- # Remove a listener for a particular document.
- #
- # removeListener(docName, listener)
- #
- # This is synchronous.
- @removeListener = (docName, listener) ->
- # The document should already be loaded.
- doc = docs[docName]
- throw new Error 'removeListener called but document not loaded' unless doc
-
- doc.eventEmitter.removeListener 'op', listener
- refreshReapingTimeout docName
-
- # Flush saves all snapshot data to the database. I'm not sure whether or not this is actually needed -
- # sharejs will happily replay uncommitted ops when documents are re-opened anyway.
- @flush = (callback) ->
- return callback?() unless db
-
- pendingWrites = 0
-
- for docName, doc of docs
- if doc.committedVersion < doc.v
- pendingWrites++
- # I'm hoping writeSnapshot will always happen in another thread.
- tryWriteSnapshot docName, ->
- process.nextTick ->
- pendingWrites--
- callback?() if pendingWrites is 0
-
- # If nothing was queued, terminate immediately.
- callback?() if pendingWrites is 0
-
- # Close the database connection. This is needed so nodejs can shut down cleanly.
- @closeDb = ->
- db?.close?()
- db = null
-
- return
-
-# Model inherits from EventEmitter.
-Model:: = new EventEmitter
-
diff --git a/services/web/public/coffee/editor/sharejs/server/rest.coffee b/services/web/public/coffee/editor/sharejs/server/rest.coffee
deleted file mode 100644
index d77a13cca4..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/rest.coffee
+++ /dev/null
@@ -1,148 +0,0 @@
-# A REST-ful frontend to the OT server.
-#
-# See the docs for details and examples about how the protocol works.
-
-http = require 'http'
-url = require 'url'
-
-connect = require 'connect'
-
-send403 = (res, message = 'Forbidden\n') ->
- res.writeHead 403, {'Content-Type': 'text/plain'}
- res.end message
-
-send404 = (res, message = '404: Your document could not be found.\n') ->
- res.writeHead 404, {'Content-Type': 'text/plain'}
- res.end message
-
-sendError = (res, message, head = false) ->
- if message == 'forbidden'
- if head
- send403 res, ""
- else
- send403 res
- else if message == 'Document does not exist'
- if head
- send404 res, ""
- else
- send404 res
- else
- console.warn "REST server does not know how to send error: '#{message}'"
- if head
- res.writeHead 500, {'Content-Type': 'text/plain'}
- res.end "Error: #{message}\n"
- else
- res.writeHead 500, {}
- res.end ""
-
-send400 = (res, message) ->
- res.writeHead 400, {'Content-Type': 'text/plain'}
- res.end message
-
-send200 = (res, message = "OK\n") ->
- res.writeHead 200, {'Content-Type': 'text/plain'}
- res.end message
-
-sendJSON = (res, obj) ->
- res.writeHead 200, {'Content-Type': 'application/json'}
- res.end JSON.stringify(obj) + '\n'
-
-# Callback is only called if the object was indeed JSON
-expectJSONObject = (req, res, callback) ->
- pump req, (data) ->
- try
- obj = JSON.parse data
- catch error
- send400 res, 'Supplied JSON invalid'
- return
-
- callback(obj)
-
-pump = (req, callback) ->
- data = ''
- req.on 'data', (chunk) -> data += chunk
- req.on 'end', () -> callback(data)
-
-# connect.router will be removed in connect 2.0 - this code will have to be rewritten or
-# more libraries pulled in.
-# https://github.com/senchalabs/connect/issues/262
-router = (app, createClient, options) ->
- auth = (req, res, next) ->
- data =
- headers: req.headers
- remoteAddress: req.connection.remoteAddress
-
- createClient data, (error, client) ->
- if client
- req._client = client
- next()
- else
- sendError res, error
-
- # GET returns the document snapshot. The version and type are sent as headers.
- # I'm not sure what to do with document metadata - it is inaccessable for now.
- app.get '/doc/:name', auth, (req, res) ->
- req._client.getSnapshot req.params.name, (error, doc) ->
- if doc
- res.setHeader 'X-OT-Type', doc.type.name
- res.setHeader 'X-OT-Version', doc.v
- if req.method == "HEAD"
- send200 res, ""
- else
- if typeof doc.snapshot == 'string'
- send200 res, doc.snapshot
- else
- sendJSON res, doc.snapshot
- else
- if req.method == "HEAD"
- sendError res, error, true
- else
- sendError res, error
-
- # Put is used to create a document. The contents are a JSON object with {type:TYPENAME, meta:{...}}
- app.put '/doc/:name', auth, (req, res) ->
- expectJSONObject req, res, (obj) ->
- type = obj?.type
- meta = obj?.meta
-
- unless typeof type == 'string' and (meta == undefined or typeof meta == 'object')
- send400 res, 'Type invalid'
- else
- req._client.create req.params.name, type, meta, (error) ->
- if error
- sendError res, error
- else
- send200 res
-
- # POST submits an op to the document.
- app.post '/doc/:name', auth, (req, res) ->
- query = url.parse(req.url, true).query
-
- version = if query?.v?
- parseInt query?.v
- else
- parseInt req.headers['x-ot-version']
-
- unless version? and version >= 0
- send400 res, 'Version required - attach query parameter ?v=X on your URL or set the X-OT-Version header'
- else
- expectJSONObject req, res, (obj) ->
- opData = {v:version, op:obj, meta:{source:req.socket.remoteAddress}}
- req._client.submitOp req.params.name, opData, (error, newVersion) ->
- if error?
- sendError res, error
- else
- sendJSON res, {v:newVersion}
-
- app.delete '/doc/:name', auth, (req, res) ->
- req._client.delete req.params.name, (error) ->
- if error
- sendError res, error
- else
- send200 res
-
-# Attach the frontend to the supplied http.Server.
-#
-# As of sharejs 0.4.0, options is ignored. To control the deleting of documents, specify an auth() function.
-module.exports = (createClient, options) ->
- connect.router (app) -> router(app, createClient, options)
diff --git a/services/web/public/coffee/editor/sharejs/server/socketio.coffee b/services/web/public/coffee/editor/sharejs/server/socketio.coffee
deleted file mode 100644
index 862d095bd2..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/socketio.coffee
+++ /dev/null
@@ -1,336 +0,0 @@
-# This implements the socketio-based network API for ShareJS.
-#
-# This is the frontend used by the javascript socket implementation.
-#
-# See documentation for this protocol is in doc/protocol.md
-# Tests are in test/socketio.coffee
-#
-# This code will be removed in a future version of sharejs because socket.io is
-# too buggy.
-
-socketio = require 'socket.io'
-util = require 'util'
-hat = require 'hat'
-
-p = ->#util.debug
-i = ->#util.inspect
-
-# Attach the streaming protocol to the supplied http.Server.
-#
-# Options = {}
-exports.attach = (server, createClient, options) ->
- io = socketio.listen server
-
- io.configure ->
- io.set 'log level', 1
- for option of options
- io.set option, options[option]
-
- authClient = (handshakeData, callback) ->
- data =
- headers: handshakeData.headers
- remoteAddress: handshakeData.address.address
- secure: handshakeData.secure
-
- createClient data, (error, client) ->
- if error
- # Its important that we don't pass the error message to the client here - leaving it as null
- # will ensure the client recieves the normal 'not_authorized' message and thus
- # emits 'connect_failed' instead of 'error'
- callback null, false
- else
- handshakeData.client = client
- callback null, true
-
- io.of('/sjs').authorization(authClient).on 'connection', (socket) ->
- client = socket.handshake.client
-
- # There seems to be a bug in socket.io where socket.request isn't set sometimes.
- p "New socket connected from #{socket.request.socket.remoteAddress} with id #{socket.id}" if socket.request?
-
- lastSentDoc = null
- lastReceivedDoc = null
-
- # Map from docName -> {listener:fn, queue:[msg], busy:bool}
- docState = {}
- closed = false
-
- # Send a message to the socket.
- # msg _must_ have the doc:DOCNAME property set. We'll remove it if its the same as lastReceivedDoc.
- send = (msg) ->
- if msg.doc == lastSentDoc
- delete msg.doc
- else
- lastSentDoc = msg.doc
-
- p "Sending #{i msg}"
- socket.json.send msg
-
- # Open the given document name, at the requested version.
- # callback(error, version)
- open = (docName, version, callback) ->
- callback 'Doc already opened' if docState[docName].listener?
- p "Registering listener on #{docName} by #{socket.id} at #{version}"
-
- # This passes op events to the client
- docState[docName].listener = listener = (opData) ->
- throw new Error 'Consistency violation - doc listener invalid' unless docState[docName].listener == listener
-
- p "listener doc:#{docName} opdata:#{i opData} v:#{version}"
-
- # Skip the op if this socket sent it.
- return if opData.meta?.source == client.id
-
- opMsg =
- doc: docName
- op: opData.op
- v: opData.v
- meta: opData.meta
-
- send opMsg
-
- # Tell the socket the doc is open at the requested version
- client.listen docName, version, listener, (error, v) ->
- delete docState[docName].listener if error
- callback error, v
-
- # Close the named document.
- # callback([error])
- close = (docName, callback) ->
- p "Closing #{docName}"
- listener = docState[docName].listener
- return callback 'Doc already closed' unless listener?
-
- client.removeListener docName
- docState[docName].listener = null
- callback()
-
- # Handles messages with any combination of the open:true, create:true and snapshot:null parameters
- handleOpenCreateSnapshot = (query, finished) ->
- docName = query.doc
- msg = doc:docName
-
- callback = (error) ->
- if error
- close(docName) if msg.open == true
- msg.open = false if query.open == true
- msg.snapshot = null if query.snapshot != undefined
- delete msg.create
-
- msg.error = error
-
- send msg
- finished()
-
- return callback 'No docName specified' unless query.doc?
-
- if query.create == true
- if typeof query.type != 'string'
- return callback 'create:true requires type specified'
-
- if query.meta != undefined
- unless typeof query.meta == 'object' and Array.isArray(query.meta) == false
- return callback 'meta must be an object'
-
- docData = undefined
- # Technically, we don't need a snapshot if the user called create but not open or createSnapshot,
- # but no clients do that yet anyway.
- #
- # It might be nice to add a 'createOrGet()' method to model / db manager. But most
- # of the time clients are opening an existing document rather than creating a new one anyway.
- ###
- model.clientGetSnapshot client, query.doc, (error, data) ->
- maybeCreate = (callback) ->
- if query.create and error is 'Document does not exist'
- model.clientCreate client, docName, query.type, query.meta or {}, callback
- else
- callback error, data
-
- maybeCreate (error, data) ->
- if query.create
- msg.create = !!error
- if error is 'Document already exists'
- msg.create = false
- else if error and (!msg.create or error isnt 'Document already exists')
- # This is the real final callback, to say an error has occurred.
- return callback error
- else if query.create or query.snapshot is null
-
-
- if query.snapshot isnt null
- ###
-
- # This is implemented with a series of cascading methods for each different type of
- # thing this method can handle. This would be so much nicer with an async library. Welcome to
- # callback hell.
-
- step1Create = ->
- return step2Snapshot() if query.create != true
-
- # The document obviously already exists if we have a snapshot.
- if docData
- msg.create = false
- step2Snapshot()
- else
- client.create docName, query.type, query.meta || {}, (error) ->
- if error is 'Document already exists'
- # We've called getSnapshot (-> null), then createClient (-> already exists). Its possible
- # another client has called createClient first.
- client.getSnapshot docName, (error, data) ->
- return callback error if error
-
- docData = data
- msg.create = false
- step2Snapshot()
- else if error
- callback error
- else
- msg.create = !error
- step2Snapshot()
-
- # The socket requested a document snapshot
- step2Snapshot = ->
- # Skip inserting a snapshot if the document was just created.
- if query.snapshot != null or msg.create == true
- step3Open()
- return
-
- if docData
- msg.v = docData.v
- msg.type = docData.type.name unless query.type == docData.type.name
- msg.snapshot = docData.snapshot
- else
- return callback 'Document does not exist'
-
- step3Open()
-
- # Attempt to open a document with a given name. Version is optional.
- # callback(opened at version) or callback(null, errormessage)
- step3Open = ->
- return callback() if query.open != true
-
- # Verify the type matches
- return callback 'Type mismatch' if query.type and docData and query.type != docData.type.name
-
- open docName, query.v, (error, version) ->
- return callback error if error
-
- # + Should fail if the type is wrong.
-
- p "Opened #{docName} at #{version} by #{socket.id}"
- msg.open = true
- msg.v = version
- callback()
-
- if query.snapshot == null or (query.open == true and query.type)
- client.getSnapshot query.doc, (error, data) ->
- return callback error if error and error != 'Document does not exist'
-
- docData = data
- step1Create()
- else
- step1Create()
-
- # The socket closes a document
- handleClose = (query, callback) ->
- close query.doc, (error) ->
- if error
- # An error closing still results in the doc being closed.
- send {doc:query.doc, open:false, error:error}
- else
- send {doc:query.doc, open:false}
-
- callback()
-
- # We received an op from the socket
- handleOp = (query, callback) ->
- throw new Error 'No docName specified' unless query.doc?
- throw new Error 'No version specified' unless query.v? or (query.meta?.path? and query.meta?.value?)
-
- op_data = {v:query.v, op:query.op}
- op_data.meta = query.meta || {}
- op_data.meta.source = socket.id
-
- client.submitOp query.doc, op_data, (error, appliedVersion) ->
- msg = if error
- p "Sending error to socket: #{error}"
- {doc:query.doc, v:null, error:error}
- else
- {doc:query.doc, v:appliedVersion}
-
- p "sending #{i msg}"
- send msg
- callback()
-
- flush = (state) ->
- return if state.busy || state.queue.length == 0
- state.busy = true
-
- query = state.queue.shift()
-
- callback = ->
- state.busy = false
- flush state
-
- p "processing query #{i query}"
- try
- if query.open == false
- handleClose query, callback
-
- else if query.open != undefined or query.snapshot != undefined or query.create
- # You can open, request a snapshot and create all in the same
- # request. They're all handled together.
- handleOpenCreateSnapshot query, callback
-
- else if query.op? or query.meta? # The socket is applying an op.
- handleOp query, callback
-
- else
- util.debug "Unknown message received: #{util.inspect query}"
-
- catch error
- util.debug error.stack
- # ... And disconnect the socket?
- callback()
-
- # And now the actual message handler.
- messageListener = (query) ->
- p "Server recieved message #{i query}"
- # There seems to be a bug in socket.io where messages are detected
- # after the client disconnects.
- if closed
- console.warn "WARNING: received query from socket after the socket disconnected."
- console.warn socket
- return
-
- try
- query = JSON.parse query if typeof(query) == 'string'
-
- if query.doc == null
- lastReceivedDoc = null
- query.doc = hat()
- else if query.doc != undefined
- lastReceivedDoc = query.doc
- else
- throw new Error 'msg.doc missing. Probably the client reconnected without telling us - this is a socket.io bug.' unless lastReceivedDoc
- query.doc = lastReceivedDoc
- catch error
- util.debug error.stack
- return
-
- docState[query.doc] ||= {listener:null, queue:[], busy:no}
- docState[query.doc].queue.push query
- flush docState[query.doc]
-
- socket.on 'message', messageListener
- socket.on 'disconnect', ->
- p "socket #{socket.id} disconnected"
- closed = true
- for docName, state of docState
- state.busy = true
- state.queue = []
- client.removeListener docName if state.listener?
- socket.removeListener 'message', messageListener
- docState = {}
-
- server
diff --git a/services/web/public/coffee/editor/sharejs/server/syncqueue.coffee b/services/web/public/coffee/editor/sharejs/server/syncqueue.coffee
deleted file mode 100644
index 746450b010..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/syncqueue.coffee
+++ /dev/null
@@ -1,42 +0,0 @@
-# A synchronous processing queue. The queue calls process on the arguments,
-# ensuring that process() is only executing once at a time.
-#
-# process(data, callback) _MUST_ eventually call its callback.
-#
-# Example:
-#
-# queue = require 'syncqueue'
-#
-# fn = queue (data, callback) ->
-# asyncthing data, ->
-# callback(321)
-#
-# fn(1)
-# fn(2)
-# fn(3, (result) -> console.log(result))
-#
-# ^--- async thing will only be running once at any time.
-
-module.exports = (process) ->
- throw new Error('process is not a function') unless typeof process == 'function'
- queue = []
-
- enqueue = (data, callback) ->
- queue.push [data, callback]
- flush()
-
- enqueue.busy = false
-
- flush = ->
- return if enqueue.busy or queue.length == 0
-
- enqueue.busy = true
- [data, callback] = queue.shift()
- process data, (result...) -> # TODO: Make this not use varargs - varargs are really slow.
- enqueue.busy = false
- # This is called after busy = false so a user can check if enqueue.busy is set in the callback.
- callback.apply null, result if callback
- flush()
-
- enqueue
-
diff --git a/services/web/public/coffee/editor/sharejs/server/useragent.coffee b/services/web/public/coffee/editor/sharejs/server/useragent.coffee
deleted file mode 100644
index 8f53fd8bc8..0000000000
--- a/services/web/public/coffee/editor/sharejs/server/useragent.coffee
+++ /dev/null
@@ -1,146 +0,0 @@
-# A useragent is assigned to each client when the client connects. The useragent is responsible for making
-# sure all requests are authorized and maintaining document metadata.
-#
-# This is used by all the client frontends to interact with the server.
-
-hat = require 'hat'
-types = require '../types'
-
-# This module exports a function which you can call with the model and options. Calling the function
-# returns _another_ function which you can call when clients connect.
-module.exports = (model, options) ->
- # By default, accept all connections + data submissions.
- # Don't let anyone delete documents though.
- auth = options.auth or (agent, action) ->
- if action.type in ['connect', 'read', 'create', 'update'] then action.accept() else action.reject()
-
- class UserAgent
- constructor: (data) ->
- @sessionId = hat()
- @connectTime = new Date
- @headers = data.headers
- @remoteAddress = data.remoteAddress
-
- # This is a map from docName -> listener function
- @listeners = {}
-
- # Should be manually set by the auth function.
- @name = null
-
- # We have access to these with socket.io, but I'm not sure we can support
- # these properties on the REST API or sockjs, etc.
- #xdomain: data.xdomain
- #secure: data.secure
-
- # This is a helper method which wraps auth() above. It creates the action and calls
- # auth. If authentication succeeds, acceptCallback() is called if it exists. otherwise
- # userCallback(true) is called.
- #
- # If authentication fails, userCallback('forbidden', null) is called.
- #
- # If supplied, actionData is turned into the action object passed to auth.
- doAuth: (actionData, name, userCallback, acceptCallback) ->
- action = actionData || {}
- action.name = name
- action.type = switch name
- when 'connect' then 'connect'
- when 'create' then 'create'
- when 'get snapshot', 'get ops', 'open' then 'read'
- when 'submit op' then 'update'
- when 'submit meta' then 'update'
- when 'delete' then 'delete'
- else throw new Error "Invalid action name #{name}"
-
- responded = false
- action.reject = ->
- throw new Error 'Multiple accept/reject calls made' if responded
- responded = true
- userCallback 'forbidden', null
- action.accept = ->
- throw new Error 'Multiple accept/reject calls made' if responded
- responded = true
- acceptCallback()
-
- auth this, action
-
- disconnect: ->
- model.removeListener docName, listener for docName, listener of @listeners
-
- getOps: (docName, start, end, callback) ->
- @doAuth {docName, start, end}, 'get ops', callback, ->
- model.getOps docName, start, end, callback
-
- getSnapshot: (docName, callback) ->
- @doAuth {docName}, 'get snapshot', callback, ->
- model.getSnapshot docName, callback
-
- create: (docName, type, meta, callback) ->
- # We don't check that types[type.name] == type. That might be important at some point.
- type = types[type] if typeof type == 'string'
-
- # I'm not sure what client-specified metadata should be allowed in the document metadata
- # object. For now, I'm going to ignore all create metadata until I know how it should work.
- meta = {}
-
- meta.creator = @name if @name
- meta.ctime = meta.mtime = Date.now()
-
- # The action object has a 'type' property already. Hence the doc type is renamed to 'docType'
- @doAuth {docName, docType:type, meta}, 'create', callback, =>
- model.create docName, type, meta, callback
-
- submitOp: (docName, opData, callback) ->
- opData.meta ||= {}
- opData.meta.source = @sessionId
- dupIfSource = opData.dupIfSource or []
-
- # If ops and meta get coalesced, they should be separated here.
- if opData.op
- @doAuth {docName, op:opData.op, v:opData.v, meta:opData.meta, dupIfSource}, 'submit op', callback, =>
- model.applyOp docName, opData, callback
- else
- @doAuth {docName, meta:opData.meta}, 'submit meta', callback, =>
- model.applyMetaOp docName, opData, callback
-
- # Delete the named operation.
- # Callback is passed (deleted?, error message)
- delete: (docName, callback) ->
- @doAuth {docName}, 'delete', callback, =>
- model.delete docName, callback
-
- # Open the named document for reading. Just like model.listen, version is optional.
- listen: (docName, version, listener, callback) ->
- authOps = if version?
- # If the specified version is older than the current version, we have to also check that the
- # agent is allowed to get ops from the specified version.
- #
- # We _could_ check the version number of the document and then only check getOps if
- # the specified version is old, but an auth check is almost certainly faster than a db roundtrip.
- (c) => @doAuth {docName, start:version, end:null}, 'get ops', callback, c
- else
- (c) -> c()
-
- authOps =>
- @doAuth {docName, v:version if version?}, 'open', callback, =>
- return callback? 'Document is already open' if @listeners[docName]
- @listeners[docName] = listener
-
- model.listen docName, version, listener, (error, v) =>
- if error
- delete @listeners[docName]
-
- callback? error, v
-
- removeListener: (docName) ->
- throw new Error 'Document is not open' unless @listeners[docName]
- model.removeListener docName, @listeners[docName]
- delete @listeners[docName]
-
- # Finally, return a function which takes client data and returns an authenticated useragent object
- # through a callback.
- (data, callback) ->
- agent = new UserAgent data
- agent.doAuth null, 'connect', callback, ->
- # Maybe store a set of agents? Is that useful?
- callback null, agent
-
diff --git a/services/web/public/coffee/editor/sharejs/types/README.md b/services/web/public/coffee/editor/sharejs/types/README.md
deleted file mode 100644
index 22e68842dd..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-This directory contains all the operational transform code. Each file defines a type.
-
-Most of the types in here are for testing or demonstration. The only types which are sent to the webclient
-are `text` and `json`.
-
-
-# An OT type
-
-All OT types have the following fields:
-
-`name`: _(string)_ Name of the type. Should match the filename.
-`create() -> snapshot`: Function which creates and returns a new document snapshot
-
-`apply(snapshot, op) -> snapshot`: A function which creates a new document snapshot with the op applied
-`transform(op1, op2, side) -> op1'`: OT transform function.
-
-Given op1, op2, `apply(s, op2, transform(op1, op2, 'left')) == apply(s, op1, transform(op2, op1, 'right'))`.
-
-Transform and apply must never modify their arguments.
-
-
-Optional properties:
-
-`tp2`: _(bool)_ True if the transform function supports TP2. This allows p2p architectures to work.
-`compose(op1, op2) -> op`: Create and return a new op which has the same effect as op1 + op2.
-`serialize(snapshot) -> JSON object`: Serialize a document to something we can JSON.stringify()
-`deserialize(object) -> snapshot`: Deserialize a JSON object into the document's internal snapshot format
-`prune(op1', op2, side) -> op1`: Inserse transform function. Only required for TP2 types.
-`normalize(op) -> op`: Fix up an op to make it valid. Eg, remove skips of size zero.
-`api`: _(object)_ Set of helper methods which will be mixed in to the client document object for manipulating documents. See below.
-
-
-# Examples
-
-`count` and `simple` are two trivial OT type definitions if you want to take a look. JSON defines
-the ot-for-JSON type (see the wiki for documentation) and all the text types define different text
-implementations. (I still have no idea which one I like the most, and they're fun to write!)
-
-
-# API
-
-Types can also define API functions. These methods are mixed into the client's Doc object when a document is created.
-You can use them to help construct ops programatically (so users don't need to understand how ops are structured).
-
-For example, the three text types defined here (text, text-composable and text-tp2) all provide the text API, supplying
-`.insert()`, `.del()`, `.getLength` and `.getText` methods.
-
-See text-api.coffee for an example.
diff --git a/services/web/public/coffee/editor/sharejs/types/count.coffee b/services/web/public/coffee/editor/sharejs/types/count.coffee
deleted file mode 100644
index da28355efb..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/count.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-# This is a simple type used for testing other OT code. Each op is [expectedSnapshot, increment]
-
-exports.name = 'count'
-exports.create = -> 1
-
-exports.apply = (snapshot, op) ->
- [v, inc] = op
- throw new Error "Op #{v} != snapshot #{snapshot}" unless snapshot == v
- snapshot + inc
-
-# transform op1 by op2. Return transformed version of op1.
-exports.transform = (op1, op2) ->
- throw new Error "Op1 #{op1[0]} != op2 #{op2[0]}" unless op1[0] == op2[0]
- [op1[0] + op2[1], op1[1]]
-
-exports.compose = (op1, op2) ->
- throw new Error "Op1 #{op1} + 1 != op2 #{op2}" unless op1[0] + op1[1] == op2[0]
- [op1[0], op1[1] + op2[1]]
-
-exports.generateRandomOp = (doc) ->
- [[doc, 1], doc + 1]
-
diff --git a/services/web/public/coffee/editor/sharejs/types/helpers.coffee b/services/web/public/coffee/editor/sharejs/types/helpers.coffee
deleted file mode 100644
index 093b32e1bb..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/helpers.coffee
+++ /dev/null
@@ -1,65 +0,0 @@
-# These methods let you build a transform function from a transformComponent function
-# for OT types like text and JSON in which operations are lists of components
-# and transforming them requires N^2 work.
-
-# Add transform and transformX functions for an OT type which has transformComponent defined.
-# transformComponent(destination array, component, other component, side)
-exports['_bt'] = bootstrapTransform = (type, transformComponent, checkValidOp, append) ->
- transformComponentX = (left, right, destLeft, destRight) ->
- transformComponent destLeft, left, right, 'left'
- transformComponent destRight, right, left, 'right'
-
- # Transforms rightOp by leftOp. Returns ['rightOp', clientOp']
- type.transformX = type['transformX'] = transformX = (leftOp, rightOp) ->
- checkValidOp leftOp
- checkValidOp rightOp
-
- newRightOp = []
-
- for rightComponent in rightOp
- # Generate newLeftOp by composing leftOp by rightComponent
- newLeftOp = []
-
- k = 0
- while k < leftOp.length
- nextC = []
- transformComponentX leftOp[k], rightComponent, newLeftOp, nextC
- k++
-
- if nextC.length == 1
- rightComponent = nextC[0]
- else if nextC.length == 0
- append newLeftOp, l for l in leftOp[k..]
- rightComponent = null
- break
- else
- # Recurse.
- [l_, r_] = transformX leftOp[k..], nextC
- append newLeftOp, l for l in l_
- append newRightOp, r for r in r_
- rightComponent = null
- break
-
- append newRightOp, rightComponent if rightComponent?
- leftOp = newLeftOp
-
- [leftOp, newRightOp]
-
- # Transforms op with specified type ('left' or 'right') by otherOp.
- type.transform = type['transform'] = (op, otherOp, type) ->
- throw new Error "type must be 'left' or 'right'" unless type == 'left' or type == 'right'
-
- return op if otherOp.length == 0
-
- # TODO: Benchmark with and without this line. I _think_ it'll make a big difference...?
- return transformComponent [], op[0], otherOp[0], type if op.length == 1 and otherOp.length == 1
-
- if type == 'left'
- [left, _] = transformX op, otherOp
- left
- else
- [_, right] = transformX otherOp, op
- right
-
-if typeof WEB is 'undefined'
- exports.bootstrapTransform = bootstrapTransform
diff --git a/services/web/public/coffee/editor/sharejs/types/index.coffee b/services/web/public/coffee/editor/sharejs/types/index.coffee
deleted file mode 100644
index 6f3bb8ec20..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/index.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-
-register = (file) ->
- type = require file
- exports[type.name] = type
- try require "#{file}-api"
-
-# Import all the built-in types.
-register './simple'
-register './count'
-
-register './text'
-register './text-composable'
-register './text-tp2'
-
-register './json'
diff --git a/services/web/public/coffee/editor/sharejs/types/json-api.coffee b/services/web/public/coffee/editor/sharejs/types/json-api.coffee
deleted file mode 100644
index 8819dee798..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/json-api.coffee
+++ /dev/null
@@ -1,180 +0,0 @@
-# API for JSON OT
-
-json = require './json' if typeof WEB is 'undefined'
-
-if WEB?
- extendDoc = exports.extendDoc
- exports.extendDoc = (name, fn) ->
- SubDoc::[name] = fn
- extendDoc name, fn
-
-depath = (path) ->
- if path.length == 1 and path[0].constructor == Array
- path[0]
- else path
-
-class SubDoc
- constructor: (@doc, @path) ->
- at: (path...) -> @doc.at @path.concat depath path
- get: -> @doc.getAt @path
- # for objects and lists
- set: (value, cb) -> @doc.setAt @path, value, cb
- # for strings and lists.
- insert: (pos, value, cb) -> @doc.insertAt @path, pos, value, cb
- # for strings
- del: (pos, length, cb) -> @doc.deleteTextAt @path, length, pos, cb
- # for objects and lists
- remove: (cb) -> @doc.removeAt @path, cb
- push: (value, cb) -> @insert @get().length, value, cb
- move: (from, to, cb) -> @doc.moveAt @path, from, to, cb
- add: (amount, cb) -> @doc.addAt @path, amount, cb
- on: (event, cb) -> @doc.addListener @path, event, cb
- removeListener: (l) -> @doc.removeListener l
-
- # text API compatibility
- getLength: -> @get().length
- getText: -> @get()
-
-traverse = (snapshot, path) ->
- container = data:snapshot
- key = 'data'
- elem = container
- for p in path
- elem = elem[key]
- key = p
- throw new Error 'bad path' if typeof elem == 'undefined'
- {elem, key}
-
-pathEquals = (p1, p2) ->
- return false if p1.length != p2.length
- for e,i in p1
- return false if e != p2[i]
- true
-
-json.api =
- provides: {json:true}
-
- at: (path...) -> new SubDoc this, depath path
-
- get: -> @snapshot
- set: (value, cb) -> @setAt [], value, cb
-
- getAt: (path) ->
- {elem, key} = traverse @snapshot, path
- return elem[key]
-
- setAt: (path, value, cb) ->
- {elem, key} = traverse @snapshot, path
- op = {p:path}
- if elem.constructor == Array
- op.li = value
- op.ld = elem[key] if typeof elem[key] != 'undefined'
- else if typeof elem == 'object'
- op.oi = value
- op.od = elem[key] if typeof elem[key] != 'undefined'
- else throw new Error 'bad path'
- @submitOp [op], cb
-
- removeAt: (path, cb) ->
- {elem, key} = traverse @snapshot, path
- throw new Error 'no element at that path' unless typeof elem[key] != 'undefined'
- op = {p:path}
- if elem.constructor == Array
- op.ld = elem[key]
- else if typeof elem == 'object'
- op.od = elem[key]
- else throw new Error 'bad path'
- @submitOp [op], cb
-
- insertAt: (path, pos, value, cb) ->
- {elem, key} = traverse @snapshot, path
- op = {p:path.concat pos}
- if elem[key].constructor == Array
- op.li = value
- else if typeof elem[key] == 'string'
- op.si = value
- @submitOp [op], cb
-
- moveAt: (path, from, to, cb) ->
- op = [{p:path.concat(from), lm:to}]
- @submitOp op, cb
-
- addAt: (path, amount, cb) ->
- op = [{p:path, na:amount}]
- @submitOp op, cb
-
- deleteTextAt: (path, length, pos, cb) ->
- {elem, key} = traverse @snapshot, path
- op = [{p:path.concat(pos), sd:elem[key][pos...(pos + length)]}]
- @submitOp op, cb
-
- addListener: (path, event, cb) ->
- l = {path, event, cb}
- @_listeners.push l
- l
- removeListener: (l) ->
- i = @_listeners.indexOf l
- return false if i < 0
- @_listeners.splice i, 1
- return true
- _register: ->
- @_listeners = []
- @on 'change', (op) ->
- for c in op
- if c.na != undefined or c.si != undefined or c.sd != undefined
- # no change to structure
- continue
- to_remove = []
- for l, i in @_listeners
- # Transform a dummy op by the incoming op to work out what
- # should happen to the listener.
- dummy = {p:l.path, na:0}
- xformed = @type.transformComponent [], dummy, c, 'left'
- if xformed.length == 0
- # The op was transformed to noop, so we should delete the listener.
- to_remove.push i
- else if xformed.length == 1
- # The op remained, so grab its new path into the listener.
- l.path = xformed[0].p
- else
- throw new Error "Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components."
- to_remove.sort (a, b) -> b - a
- for i in to_remove
- @_listeners.splice i, 1
- @on 'remoteop', (op) ->
- for c in op
- match_path = if c.na == undefined then c.p[...c.p.length-1] else c.p
- for {path, event, cb} in @_listeners
- if pathEquals path, match_path
- switch event
- when 'insert'
- if c.li != undefined and c.ld == undefined
- cb(c.p[c.p.length-1], c.li)
- else if c.oi != undefined and c.od == undefined
- cb(c.p[c.p.length-1], c.oi)
- else if c.si != undefined
- cb(c.p[c.p.length-1], c.si)
- when 'delete'
- if c.li == undefined and c.ld != undefined
- cb(c.p[c.p.length-1], c.ld)
- else if c.oi == undefined and c.od != undefined
- cb(c.p[c.p.length-1], c.od)
- else if c.sd != undefined
- cb(c.p[c.p.length-1], c.sd)
- when 'replace'
- if c.li != undefined and c.ld != undefined
- cb(c.p[c.p.length-1], c.ld, c.li)
- else if c.oi != undefined and c.od != undefined
- cb(c.p[c.p.length-1], c.od, c.oi)
- when 'move'
- if c.lm != undefined
- cb(c.p[c.p.length-1], c.lm)
- when 'add'
- if c.na != undefined
- cb(c.na)
- else if (common = @type.commonPath match_path, path)?
- if event == 'child op'
- if match_path.length == path.length == common
- throw new Error "paths match length and have commonality, but aren't equal?"
- child_path = c.p[common+1..]
- cb(child_path, c)
diff --git a/services/web/public/coffee/editor/sharejs/types/json.coffee b/services/web/public/coffee/editor/sharejs/types/json.coffee
deleted file mode 100644
index b03b0947ef..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/json.coffee
+++ /dev/null
@@ -1,441 +0,0 @@
-# This is the implementation of the JSON OT type.
-#
-# Spec is here: https://github.com/josephg/ShareJS/wiki/JSON-Operations
-
-if WEB?
- text = exports.types.text
-else
- text = require './text'
-
-json = {}
-
-json.name = 'json'
-
-json.create = -> null
-
-json.invertComponent = (c) ->
- c_ = {p: c.p}
- c_.sd = c.si if c.si != undefined
- c_.si = c.sd if c.sd != undefined
- c_.od = c.oi if c.oi != undefined
- c_.oi = c.od if c.od != undefined
- c_.ld = c.li if c.li != undefined
- c_.li = c.ld if c.ld != undefined
- c_.na = -c.na if c.na != undefined
- if c.lm != undefined
- c_.lm = c.p[c.p.length-1]
- c_.p = c.p[0...c.p.length - 1].concat([c.lm])
- c_
-
-json.invert = (op) -> json.invertComponent c for c in op.slice().reverse()
-
-json.checkValidOp = (op) ->
-
-isArray = (o) -> Object.prototype.toString.call(o) == '[object Array]'
-json.checkList = (elem) ->
- throw new Error 'Referenced element not a list' unless isArray(elem)
-
-json.checkObj = (elem) ->
- throw new Error "Referenced element not an object (it was #{JSON.stringify elem})" unless elem.constructor is Object
-
-json.apply = (snapshot, op) ->
- json.checkValidOp op
- op = clone op
-
- container = {data: clone snapshot}
-
- try
- for c, i in op
- parent = null
- parentkey = null
- elem = container
- key = 'data'
-
- for p in c.p
- parent = elem
- parentkey = key
- elem = elem[key]
- key = p
-
- throw new Error 'Path invalid' unless parent?
-
- if c.na != undefined
- # Number add
- throw new Error 'Referenced element not a number' unless typeof elem[key] is 'number'
- elem[key] += c.na
-
- else if c.si != undefined
- # String insert
- throw new Error "Referenced element not a string (it was #{JSON.stringify elem})" unless typeof elem is 'string'
- parent[parentkey] = elem[...key] + c.si + elem[key..]
- else if c.sd != undefined
- # String delete
- throw new Error 'Referenced element not a string' unless typeof elem is 'string'
- throw new Error 'Deleted string does not match' unless elem[key...key + c.sd.length] == c.sd
- parent[parentkey] = elem[...key] + elem[key + c.sd.length..]
-
- else if c.li != undefined && c.ld != undefined
- # List replace
- json.checkList elem
-
- # Should check the list element matches c.ld
- elem[key] = c.li
- else if c.li != undefined
- # List insert
- json.checkList elem
-
- elem.splice key, 0, c.li
- else if c.ld != undefined
- # List delete
- json.checkList elem
-
- # Should check the list element matches c.ld here too.
- elem.splice key, 1
- else if c.lm != undefined
- # List move
- json.checkList elem
- if c.lm != key
- e = elem[key]
- # Remove it...
- elem.splice key, 1
- # And insert it back.
- elem.splice c.lm, 0, e
-
- else if c.oi != undefined
- # Object insert / replace
- json.checkObj elem
-
- # Should check that elem[key] == c.od
- elem[key] = c.oi
- else if c.od != undefined
- # Object delete
- json.checkObj elem
-
- # Should check that elem[key] == c.od
- delete elem[key]
- else
- throw new Error 'invalid / missing instruction in op'
- catch error
- # TODO: Roll back all already applied changes. Write tests before implementing this code.
- throw error
-
- container.data
-
-# Checks if two paths, p1 and p2 match.
-json.pathMatches = (p1, p2, ignoreLast) ->
- return false unless p1.length == p2.length
-
- for p, i in p1
- return false if p != p2[i] and (!ignoreLast or i != p1.length - 1)
-
- true
-
-json.append = (dest, c) ->
- c = clone c
- if dest.length != 0 and json.pathMatches c.p, (last = dest[dest.length - 1]).p
- if last.na != undefined and c.na != undefined
- dest[dest.length - 1] = { p: last.p, na: last.na + c.na }
- else if last.li != undefined and c.li == undefined and c.ld == last.li
- # insert immediately followed by delete becomes a noop.
- if last.ld != undefined
- # leave the delete part of the replace
- delete last.li
- else
- dest.pop()
- else if last.od != undefined and last.oi == undefined and
- c.oi != undefined and c.od == undefined
- last.oi = c.oi
- else if c.lm != undefined and c.p[c.p.length-1] == c.lm
- null # don't do anything
- else
- dest.push c
- else
- dest.push c
-
-json.compose = (op1, op2) ->
- json.checkValidOp op1
- json.checkValidOp op2
-
- newOp = clone op1
- json.append newOp, c for c in op2
-
- newOp
-
-json.normalize = (op) ->
- newOp = []
-
- op = [op] unless isArray op
-
- for c in op
- c.p ?= []
- json.append newOp, c
-
- newOp
-
-# hax, copied from test/types/json. Apparently this is still the fastest way to deep clone an object, assuming
-# we have browser support for JSON.
-# http://jsperf.com/cloning-an-object/12
-clone = (o) -> JSON.parse(JSON.stringify o)
-
-json.commonPath = (p1, p2) ->
- p1 = p1.slice()
- p2 = p2.slice()
- p1.unshift('data')
- p2.unshift('data')
- p1 = p1[...p1.length-1]
- p2 = p2[...p2.length-1]
- return -1 if p2.length == 0
- i = 0
- while p1[i] == p2[i] && i < p1.length
- i++
- if i == p2.length
- return i-1
- return
-
-# transform c so it applies to a document with otherC applied.
-json.transformComponent = (dest, c, otherC, type) ->
- c = clone c
- c.p.push(0) if c.na != undefined
- otherC.p.push(0) if otherC.na != undefined
-
- common = json.commonPath c.p, otherC.p
- common2 = json.commonPath otherC.p, c.p
-
- cplength = c.p.length
- otherCplength = otherC.p.length
-
- c.p.pop() if c.na != undefined # hax
- otherC.p.pop() if otherC.na != undefined
-
- if otherC.na
- if common2? && otherCplength >= cplength && otherC.p[common2] == c.p[common2]
- if c.ld != undefined
- oc = clone otherC
- oc.p = oc.p[cplength..]
- c.ld = json.apply clone(c.ld), [oc]
- else if c.od != undefined
- oc = clone otherC
- oc.p = oc.p[cplength..]
- c.od = json.apply clone(c.od), [oc]
- json.append dest, c
- return dest
-
- if common2? && otherCplength > cplength && c.p[common2] == otherC.p[common2]
- # transform based on c
- if c.ld != undefined
- oc = clone otherC
- oc.p = oc.p[cplength..]
- c.ld = json.apply clone(c.ld), [oc]
- else if c.od != undefined
- oc = clone otherC
- oc.p = oc.p[cplength..]
- c.od = json.apply clone(c.od), [oc]
-
-
- if common?
- commonOperand = cplength == otherCplength
- # transform based on otherC
- if otherC.na != undefined
- # this case is handled above due to icky path hax
- else if otherC.si != undefined || otherC.sd != undefined
- # String op vs string op - pass through to text type
- if c.si != undefined || c.sd != undefined
- throw new Error("must be a string?") unless commonOperand
-
- # Convert an op component to a text op component
- convert = (component) ->
- newC = p:component.p[component.p.length - 1]
- if component.si
- newC.i = component.si
- else
- newC.d = component.sd
- newC
-
- tc1 = convert c
- tc2 = convert otherC
-
- res = []
- text._tc res, tc1, tc2, type
- for tc in res
- jc = { p: c.p[...common] }
- jc.p.push(tc.p)
- jc.si = tc.i if tc.i?
- jc.sd = tc.d if tc.d?
- json.append dest, jc
- return dest
- else if otherC.li != undefined && otherC.ld != undefined
- if otherC.p[common] == c.p[common]
- # noop
- if !commonOperand
- # we're below the deleted element, so -> noop
- return dest
- else if c.ld != undefined
- # we're trying to delete the same element, -> noop
- if c.li != undefined and type == 'left'
- # we're both replacing one element with another. only one can
- # survive!
- c.ld = clone otherC.li
- else
- return dest
- else if otherC.li != undefined
- if c.li != undefined and c.ld == undefined and commonOperand and c.p[common] == otherC.p[common]
- # in li vs. li, left wins.
- if type == 'right'
- c.p[common]++
- else if otherC.p[common] <= c.p[common]
- c.p[common]++
-
- if c.lm != undefined
- if commonOperand
- # otherC edits the same list we edit
- if otherC.p[common] <= c.lm
- c.lm++
- # changing c.from is handled above.
- else if otherC.ld != undefined
- if c.lm != undefined
- if commonOperand
- if otherC.p[common] == c.p[common]
- # they deleted the thing we're trying to move
- return dest
- # otherC edits the same list we edit
- p = otherC.p[common]
- from = c.p[common]
- to = c.lm
- if p < to || (p == to && from < to)
- c.lm--
-
- if otherC.p[common] < c.p[common]
- c.p[common]--
- else if otherC.p[common] == c.p[common]
- if otherCplength < cplength
- # we're below the deleted element, so -> noop
- return dest
- else if c.ld != undefined
- if c.li != undefined
- # we're replacing, they're deleting. we become an insert.
- delete c.ld
- else
- # we're trying to delete the same element, -> noop
- return dest
- else if otherC.lm != undefined
- if c.lm != undefined and cplength == otherCplength
- # lm vs lm, here we go!
- from = c.p[common]
- to = c.lm
- otherFrom = otherC.p[common]
- otherTo = otherC.lm
- if otherFrom != otherTo
- # if otherFrom == otherTo, we don't need to change our op.
-
- # where did my thing go?
- if from == otherFrom
- # they moved it! tie break.
- if type == 'left'
- c.p[common] = otherTo
- if from == to # ugh
- c.lm = otherTo
- else
- return dest
- else
- # they moved around it
- if from > otherFrom
- c.p[common]--
- if from > otherTo
- c.p[common]++
- else if from == otherTo
- if otherFrom > otherTo
- c.p[common]++
- if from == to # ugh, again
- c.lm++
-
- # step 2: where am i going to put it?
- if to > otherFrom
- c.lm--
- else if to == otherFrom
- if to > from
- c.lm--
- if to > otherTo
- c.lm++
- else if to == otherTo
- # if we're both moving in the same direction, tie break
- if (otherTo > otherFrom and to > from) or
- (otherTo < otherFrom and to < from)
- if type == 'right'
- c.lm++
- else
- if to > from
- c.lm++
- else if to == otherFrom
- c.lm--
- else if c.li != undefined and c.ld == undefined and commonOperand
- # li
- from = otherC.p[common]
- to = otherC.lm
- p = c.p[common]
- if p > from
- c.p[common]--
- if p > to
- c.p[common]++
- else
- # ld, ld+li, si, sd, na, oi, od, oi+od, any li on an element beneath
- # the lm
- #
- # i.e. things care about where their item is after the move.
- from = otherC.p[common]
- to = otherC.lm
- p = c.p[common]
- if p == from
- c.p[common] = to
- else
- if p > from
- c.p[common]--
- if p > to
- c.p[common]++
- else if p == to
- if from > to
- c.p[common]++
- else if otherC.oi != undefined && otherC.od != undefined
- if c.p[common] == otherC.p[common]
- if c.oi != undefined and commonOperand
- # we inserted where someone else replaced
- if type == 'right'
- # left wins
- return dest
- else
- # we win, make our op replace what they inserted
- c.od = otherC.oi
- else
- # -> noop if the other component is deleting the same object (or any
- # parent)
- return dest
- else if otherC.oi != undefined
- if c.oi != undefined and c.p[common] == otherC.p[common]
- # left wins if we try to insert at the same place
- if type == 'left'
- json.append dest, {p:c.p, od:otherC.oi}
- else
- return dest
- else if otherC.od != undefined
- if c.p[common] == otherC.p[common]
- return dest if !commonOperand
- if c.oi != undefined
- delete c.od
- else
- return dest
-
- json.append dest, c
- return dest
-
-if WEB?
- exports.types ||= {}
-
- # This is kind of awful - come up with a better way to hook this helper code up.
- exports._bt(json, json.transformComponent, json.checkValidOp, json.append)
-
- # [] is used to prevent closure from renaming types.text
- exports.types.json = json
-else
- module.exports = json
-
- require('./helpers').bootstrapTransform(json, json.transformComponent, json.checkValidOp, json.append)
-
diff --git a/services/web/public/coffee/editor/sharejs/types/simple.coffee b/services/web/public/coffee/editor/sharejs/types/simple.coffee
deleted file mode 100644
index 996b1a5ddc..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/simple.coffee
+++ /dev/null
@@ -1,38 +0,0 @@
-# This is a really simple OT type. Its not compiled with the web client, but it could be.
-#
-# Its mostly included for demonstration purposes and its used in a lot of unit tests.
-#
-# This defines a really simple text OT type which only allows inserts. (No deletes).
-#
-# Ops look like:
-# {position:#, text:"asdf"}
-#
-# Document snapshots look like:
-# {str:string}
-
-module.exports =
- # The name of the OT type. The type is stored in types[type.name]. The name can be
- # used in place of the actual type in all the API methods.
- name: 'simple'
-
- # Create a new document snapshot
- create: -> {str:""}
-
- # Apply the given op to the document snapshot. Returns the new snapshot.
- #
- # The original snapshot should not be modified.
- apply: (snapshot, op) ->
- throw new Error 'Invalid position' unless 0 <= op.position <= snapshot.str.length
-
- str = snapshot.str
- str = str.slice(0, op.position) + op.text + str.slice(op.position)
- {str}
-
- # transform op1 by op2. Return transformed version of op1.
- # sym describes the symmetry of the op. Its 'left' or 'right' depending on whether the
- # op being transformed comes from the client or the server.
- transform: (op1, op2, sym) ->
- pos = op1.position
- pos += op2.text.length if op2.position < pos or (op2.position == pos and sym is 'left')
-
- return {position:pos, text:op1.text}
diff --git a/services/web/public/coffee/editor/sharejs/types/text-api.coffee b/services/web/public/coffee/editor/sharejs/types/text-api.coffee
deleted file mode 100644
index 96243ceffb..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/text-api.coffee
+++ /dev/null
@@ -1,32 +0,0 @@
-# Text document API for text
-
-text = require './text' if typeof WEB is 'undefined'
-
-text.api =
- provides: {text:true}
-
- # The number of characters in the string
- getLength: -> @snapshot.length
-
- # Get the text contents of a document
- getText: -> @snapshot
-
- insert: (pos, text, callback) ->
- op = [{p:pos, i:text}]
-
- @submitOp op, callback
- op
-
- del: (pos, length, callback) ->
- op = [{p:pos, d:@snapshot[pos...(pos + length)]}]
-
- @submitOp op, callback
- op
-
- _register: ->
- @on 'remoteop', (op) ->
- for component in op
- if component.i != undefined
- @emit 'insert', component.p, component.i
- else
- @emit 'delete', component.p, component.d
diff --git a/services/web/public/coffee/editor/sharejs/types/text-api.js b/services/web/public/coffee/editor/sharejs/types/text-api.js
deleted file mode 100644
index e953a9cfbb..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/text-api.js
+++ /dev/null
@@ -1,58 +0,0 @@
-// Generated by CoffeeScript 1.4.0
-(function() {
- var text;
-
- if (typeof WEB === 'undefined') {
- text = require('./text');
- }
-
- text.api = {
- provides: {
- text: true
- },
- getLength: function() {
- return this.snapshot.length;
- },
- getText: function() {
- return this.snapshot;
- },
- insert: function(pos, text, callback) {
- var op;
- op = [
- {
- p: pos,
- i: text
- }
- ];
- this.submitOp(op, callback);
- return op;
- },
- del: function(pos, length, callback) {
- var op;
- op = [
- {
- p: pos,
- d: this.snapshot.slice(pos, pos + length)
- }
- ];
- this.submitOp(op, callback);
- return op;
- },
- _register: function() {
- return this.on('remoteop', function(op) {
- var component, _i, _len, _results;
- _results = [];
- for (_i = 0, _len = op.length; _i < _len; _i++) {
- component = op[_i];
- if (component.i !== void 0) {
- _results.push(this.emit('insert', component.p, component.i));
- } else {
- _results.push(this.emit('delete', component.p, component.d));
- }
- }
- return _results;
- });
- }
- };
-
-}).call(this);
diff --git a/services/web/public/coffee/editor/sharejs/types/text-composable-api.coffee b/services/web/public/coffee/editor/sharejs/types/text-composable-api.coffee
deleted file mode 100644
index 7b27ac163a..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/text-composable-api.coffee
+++ /dev/null
@@ -1,43 +0,0 @@
-# Text document API for text
-
-if WEB?
- type = exports.types['text-composable']
-else
- type = require './text-composable'
-
-type.api =
- provides: {'text':true}
-
- # The number of characters in the string
- 'getLength': -> @snapshot.length
-
- # Get the text contents of a document
- 'getText': -> @snapshot
-
- 'insert': (pos, text, callback) ->
- op = type.normalize [pos, 'i':text, (@snapshot.length - pos)]
-
- @submitOp op, callback
- op
-
- 'del': (pos, length, callback) ->
- op = type.normalize [pos, 'd':@snapshot[pos...(pos + length)], (@snapshot.length - pos - length)]
-
- @submitOp op, callback
- op
-
- _register: ->
- @on 'remoteop', (op) ->
- pos = 0
- for component in op
- if typeof component is 'number'
- pos += component
- else if component.i != undefined
- @emit 'insert', pos, component.i
- pos += component.i.length
- else
- # delete
- @emit 'delete', pos, component.d
- # We don't increment pos, because the position
- # specified is after the delete has happened.
-
diff --git a/services/web/public/coffee/editor/sharejs/types/text-composable.coffee b/services/web/public/coffee/editor/sharejs/types/text-composable.coffee
deleted file mode 100644
index 992b567bf0..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/text-composable.coffee
+++ /dev/null
@@ -1,261 +0,0 @@
-# An alternate composable implementation for text. This is much closer
-# to the implementation used by google wave.
-#
-# Ops are lists of components which iterate over the whole document.
-# Components are either:
-# A number N: Skip N characters in the original document
-# {i:'str'}: Insert 'str' at the current position in the document
-# {d:'str'}: Delete 'str', which appears at the current position in the document
-#
-# Eg: [3, {i:'hi'}, 5, {d:'internet'}]
-#
-# Snapshots are strings.
-
-p = -> #require('util').debug
-i = -> #require('util').inspect
-
-exports = if WEB? then {} else module.exports
-
-exports.name = 'text-composable'
-
-exports.create = -> ''
-
-# -------- Utility methods
-
-checkOp = (op) ->
- throw new Error('Op must be an array of components') unless Array.isArray(op)
- last = null
- for c in op
- if typeof(c) == 'object'
- throw new Error("Invalid op component: #{i c}") unless (c.i? && c.i.length > 0) or (c.d? && c.d.length > 0)
- else
- throw new Error('Op components must be objects or numbers') unless typeof(c) == 'number'
- throw new Error('Skip components must be a positive number') unless c > 0
- throw new Error('Adjacent skip components should be added') if typeof(last) == 'number'
-
- last = c
-
-# Makes a function for appending components to a given op.
-# Exported for the randomOpGenerator.
-exports._makeAppend = makeAppend = (op) -> (component) ->
- if component == 0 || component.i == '' || component.d == ''
- return
- else if op.length == 0
- op.push component
- else if typeof(component) == 'number' && typeof(op[op.length - 1]) == 'number'
- op[op.length - 1] += component
- else if component.i? && op[op.length - 1].i?
- op[op.length - 1].i += component.i
- else if component.d? && op[op.length - 1].d?
- op[op.length - 1].d += component.d
- else
- op.push component
-
-# checkOp op
-
-# Makes 2 functions for taking components from the start of an op, and for peeking
-# at the next op that could be taken.
-makeTake = (op) ->
- # The index of the next component to take
- idx = 0
- # The offset into the component
- offset = 0
-
- # Take up to length n from the front of op. If n is null, take the next
- # op component. If indivisableField == 'd', delete components won't be separated.
- # If indivisableField == 'i', insert components won't be separated.
- take = (n, indivisableField) ->
- return null if idx == op.length
- #assert.notStrictEqual op.length, i, 'The op is too short to traverse the document'
-
- if typeof(op[idx]) == 'number'
- if !n? or op[idx] - offset <= n
- c = op[idx] - offset
- ++idx; offset = 0
- c
- else
- offset += n
- n
- else
- # Take from the string
- field = if op[idx].i then 'i' else 'd'
- c = {}
- if !n? or op[idx][field].length - offset <= n or field == indivisableField
- c[field] = op[idx][field][offset..]
- ++idx; offset = 0
- else
- c[field] = op[idx][field][offset...(offset + n)]
- offset += n
- c
-
- peekType = () ->
- op[idx]
-
- [take, peekType]
-
-# Find and return the length of an op component
-componentLength = (component) ->
- if typeof(component) == 'number'
- component
- else if component.i?
- component.i.length
- else
- component.d.length
-
-# Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
-# adjacent inserts and deletes.
-exports.normalize = (op) ->
- newOp = []
- append = makeAppend newOp
- append component for component in op
- newOp
-
-# Apply the op to the string. Returns the new string.
-exports.apply = (str, op) ->
- p "Applying #{i op} to '#{str}'"
- throw new Error('Snapshot should be a string') unless typeof(str) == 'string'
- checkOp op
-
- pos = 0
- newDoc = []
-
- for component in op
- if typeof(component) == 'number'
- throw new Error('The op is too long for this document') if component > str.length
- newDoc.push str[...component]
- str = str[component..]
- else if component.i?
- newDoc.push component.i
- else
- throw new Error("The deleted text '#{component.d}' doesn't match the next characters in the document '#{str[...component.d.length]}'") unless component.d == str[...component.d.length]
- str = str[component.d.length..]
-
- throw new Error("The applied op doesn't traverse the entire document") unless '' == str
-
- newDoc.join ''
-
-# transform op1 by op2. Return transformed version of op1.
-# op1 and op2 are unchanged by transform.
-exports.transform = (op, otherOp, side) ->
- throw new Error "side (#{side} must be 'left' or 'right'" unless side == 'left' or side == 'right'
-
- checkOp op
- checkOp otherOp
- newOp = []
-
- append = makeAppend newOp
- [take, peek] = makeTake op
-
- for component in otherOp
- if typeof(component) == 'number' # Skip
- length = component
- while length > 0
- chunk = take(length, 'i')
- throw new Error('The op traverses more elements than the document has') unless chunk != null
-
- append chunk
- length -= componentLength chunk unless typeof(chunk) == 'object' && chunk.i?
- else if component.i? # Insert
- if side == 'left'
- # The left insert should go first.
- o = peek()
- append take() if o?.i
-
- # Otherwise, skip the inserted text.
- append(component.i.length)
- else # Delete.
- #assert.ok component.d
- length = component.d.length
- while length > 0
- chunk = take(length, 'i')
- throw new Error('The op traverses more elements than the document has') unless chunk != null
-
- if typeof(chunk) == 'number'
- length -= chunk
- else if chunk.i?
- append(chunk)
- else
- #assert.ok chunk.d
- # The delete is unnecessary now.
- length -= chunk.d.length
-
- # Append extras from op1
- while (component = take())
- throw new Error "Remaining fragments in the op: #{i component}" unless component?.i?
- append component
-
- newOp
-
-
-# Compose 2 ops into 1 op.
-exports.compose = (op1, op2) ->
- p "COMPOSE #{i op1} + #{i op2}"
- checkOp op1
- checkOp op2
-
- result = []
-
- append = makeAppend result
- [take, _] = makeTake op1
-
- for component in op2
- if typeof(component) == 'number' # Skip
- length = component
- while length > 0
- chunk = take(length, 'd')
- throw new Error('The op traverses more elements than the document has') unless chunk != null
-
- append chunk
- length -= componentLength chunk unless typeof(chunk) == 'object' && chunk.d?
-
- else if component.i? # Insert
- append {i:component.i}
-
- else # Delete
- offset = 0
- while offset < component.d.length
- chunk = take(component.d.length - offset, 'd')
- throw new Error('The op traverses more elements than the document has') unless chunk != null
-
- # If its delete, append it. If its skip, drop it and decrease length. If its insert, check the strings match, drop it and decrease length.
- if typeof(chunk) == 'number'
- append {d:component.d[offset...(offset + chunk)]}
- offset += chunk
- else if chunk.i?
- throw new Error("The deleted text doesn't match the inserted text") unless component.d[offset...(offset + chunk.i.length)] == chunk.i
- offset += chunk.i.length
- # The ops cancel each other out.
- else
- # Delete
- append chunk
-
- # Append extras from op1
- while (component = take())
- throw new Error "Trailing stuff in op1 #{i component}" unless component?.d?
- append component
-
- result
-
-
-invertComponent = (c) ->
- if typeof(c) == 'number'
- c
- else if c.i?
- {d:c.i}
- else
- {i:c.d}
-
-# Invert an op
-exports.invert = (op) ->
- result = []
- append = makeAppend result
-
- append(invertComponent component) for component in op
-
- result
-
-if window?
- window.ot ||= {}
- window.ot.types ||= {}
- window.ot.types.text = exports
-
diff --git a/services/web/public/coffee/editor/sharejs/types/text-tp2-api.coffee b/services/web/public/coffee/editor/sharejs/types/text-tp2-api.coffee
deleted file mode 100644
index d661b5ae37..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/text-tp2-api.coffee
+++ /dev/null
@@ -1,89 +0,0 @@
-# Text document API for text-tp2
-
-if WEB?
- type = exports.types['text-tp2']
-else
- type = require './text-tp2'
-
-{_takeDoc:takeDoc, _append:append} = type
-
-appendSkipChars = (op, doc, pos, maxlength) ->
- while (maxlength == undefined || maxlength > 0) and pos.index < doc.data.length
- part = takeDoc doc, pos, maxlength, true
- maxlength -= part.length if maxlength != undefined and typeof part is 'string'
- append op, (part.length || part)
-
-type['api'] =
- 'provides': {'text':true}
-
- # The number of characters in the string
- 'getLength': -> @snapshot.charLength
-
- # Flatten a document into a string
- 'getText': ->
- strings = (elem for elem in @snapshot.data when typeof elem is 'string')
- strings.join ''
-
- 'insert': (pos, text, callback) ->
- pos = 0 if pos == undefined
-
- op = []
- docPos = {index:0, offset:0}
-
- appendSkipChars op, @snapshot, docPos, pos
- append op, {'i':text}
- appendSkipChars op, @snapshot, docPos
-
- @submitOp op, callback
- op
-
- 'del': (pos, length, callback) ->
- op = []
- docPos = {index:0, offset:0}
-
- appendSkipChars op, @snapshot, docPos, pos
-
- while length > 0
- part = takeDoc @snapshot, docPos, length, true
- if typeof part is 'string'
- append op, {'d':part.length}
- length -= part.length
- else
- append op, part
-
- appendSkipChars op, @snapshot, docPos
-
- @submitOp op, callback
- op
-
- '_register': ->
- # Interpret recieved ops + generate more detailed events for them
- @on 'remoteop', (op, snapshot) ->
- textPos = 0
- docPos = {index:0, offset:0}
-
- for component in op
- if typeof component is 'number'
- # Skip
- remainder = component
- while remainder > 0
- part = takeDoc snapshot, docPos, remainder
- if typeof part is 'string'
- textPos += part.length
- remainder -= part.length || part
- else if component.i != undefined
- # Insert
- if typeof component.i is 'string'
- @emit 'insert', textPos, component.i
- textPos += component.i.length
- else
- # Delete
- remainder = component.d
- while remainder > 0
- part = takeDoc snapshot, docPos, remainder
- if typeof part is 'string'
- @emit 'delete', textPos, part
- remainder -= part.length || part
-
- return
-
diff --git a/services/web/public/coffee/editor/sharejs/types/text-tp2.coffee b/services/web/public/coffee/editor/sharejs/types/text-tp2.coffee
deleted file mode 100644
index d19cbdcef4..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/text-tp2.coffee
+++ /dev/null
@@ -1,322 +0,0 @@
-# A TP2 implementation of text, following this spec:
-# http://code.google.com/p/lightwave/source/browse/trunk/experimental/ot/README
-#
-# A document is made up of a string and a set of tombstones inserted throughout
-# the string. For example, 'some ', (2 tombstones), 'string'.
-#
-# This is encoded in a document as: {s:'some string', t:[5, -2, 6]}
-#
-# Ops are lists of components which iterate over the whole document.
-# Components are either:
-# N: Skip N characters in the original document
-# {i:'str'}: Insert 'str' at the current position in the document
-# {i:N}: Insert N tombstones at the current position in the document
-# {d:N}: Delete (tombstone) N characters at the current position in the document
-#
-# Eg: [3, {i:'hi'}, 5, {d:8}]
-#
-# Snapshots are lists with characters and tombstones. Characters are stored in strings
-# and adjacent tombstones are flattened into numbers.
-#
-# Eg, the document: 'Hello .....world' ('.' denotes tombstoned (deleted) characters)
-# would be represented by a document snapshot of ['Hello ', 5, 'world']
-
-type =
- name: 'text-tp2'
- tp2: true
- create: -> {charLength:0, totalLength:0, positionCache:[], data:[]}
- serialize: (doc) ->
- throw new Error 'invalid doc snapshot' unless doc.data
- doc.data
- deserialize: (data) ->
- doc = type.create()
- doc.data = data
-
- for component in data
- if typeof component is 'string'
- doc.charLength += component.length
- doc.totalLength += component.length
- else
- doc.totalLength += component
-
- doc
-
-
-checkOp = (op) ->
- throw new Error('Op must be an array of components') unless Array.isArray(op)
- last = null
- for c in op
- if typeof(c) == 'object'
- if c.i != undefined
- throw new Error('Inserts must insert a string or a +ive number') unless (typeof(c.i) == 'string' and c.i.length > 0) or (typeof(c.i) == 'number' and c.i > 0)
- else if c.d != undefined
- throw new Error('Deletes must be a +ive number') unless typeof(c.d) == 'number' and c.d > 0
- else
- throw new Error('Operation component must define .i or .d')
- else
- throw new Error('Op components must be objects or numbers') unless typeof(c) == 'number'
- throw new Error('Skip components must be a positive number') unless c > 0
- throw new Error('Adjacent skip components should be combined') if typeof(last) == 'number'
-
- last = c
-
-# Take the next part from the specified position in a document snapshot.
-# position = {index, offset}. It will be updated.
-type._takeDoc = takeDoc = (doc, position, maxlength, tombsIndivisible) ->
- throw new Error 'Operation goes past the end of the document' if position.index >= doc.data.length
-
- part = doc.data[position.index]
- # peel off data[0]
- result = if typeof(part) == 'string'
- if maxlength != undefined
- part[position.offset...(position.offset + maxlength)]
- else
- part[position.offset...]
- else
- if maxlength == undefined or tombsIndivisible
- part - position.offset
- else
- Math.min(maxlength, part - position.offset)
-
- resultLen = result.length || result
-
- if (part.length || part) - position.offset > resultLen
- position.offset += resultLen
- else
- position.index++
- position.offset = 0
-
- result
-
-# Append a part to the end of a document
-type._appendDoc = appendDoc = (doc, p) ->
- return if p == 0 or p == ''
-
- if typeof p is 'string'
- doc.charLength += p.length
- doc.totalLength += p.length
- else
- doc.totalLength += p
-
- data = doc.data
- if data.length == 0
- data.push p
- else if typeof(data[data.length - 1]) == typeof(p)
- data[data.length - 1] += p
- else
- data.push p
- return
-
-# Apply the op to the document. The document is not modified in the process.
-type.apply = (doc, op) ->
- unless doc.totalLength != undefined and doc.charLength != undefined and doc.data.length != undefined
- throw new Error('Snapshot is invalid')
-
- checkOp op
-
- newDoc = type.create()
- position = {index:0, offset:0}
-
- for component in op
- if typeof(component) is 'number'
- remainder = component
- while remainder > 0
- part = takeDoc doc, position, remainder
-
- appendDoc newDoc, part
- remainder -= part.length || part
-
- else if component.i != undefined
- appendDoc newDoc, component.i
- else if component.d != undefined
- remainder = component.d
- while remainder > 0
- part = takeDoc doc, position, remainder
- remainder -= part.length || part
- appendDoc newDoc, component.d
-
- newDoc
-
-# Append an op component to the end of the specified op.
-# Exported for the randomOpGenerator.
-type._append = append = (op, component) ->
- if component == 0 || component.i == '' || component.i == 0 || component.d == 0
- return
- else if op.length == 0
- op.push component
- else
- last = op[op.length - 1]
- if typeof(component) == 'number' && typeof(last) == 'number'
- op[op.length - 1] += component
- else if component.i != undefined && last.i? && typeof(last.i) == typeof(component.i)
- last.i += component.i
- else if component.d != undefined && last.d?
- last.d += component.d
- else
- op.push component
-
-# Makes 2 functions for taking components from the start of an op, and for peeking
-# at the next op that could be taken.
-makeTake = (op) ->
- # The index of the next component to take
- index = 0
- # The offset into the component
- offset = 0
-
- # Take up to length maxlength from the op. If maxlength is not defined, there is no max.
- # If insertsIndivisible is true, inserts (& insert tombstones) won't be separated.
- #
- # Returns null when op is fully consumed.
- take = (maxlength, insertsIndivisible) ->
- return null if index == op.length
-
- e = op[index]
- if typeof((current = e)) == 'number' or typeof((current = e.i)) == 'number' or (current = e.d) != undefined
- if !maxlength? or current - offset <= maxlength or (insertsIndivisible and e.i != undefined)
- # Return the rest of the current element.
- c = current - offset
- ++index; offset = 0
- else
- offset += maxlength
- c = maxlength
- if e.i != undefined then {i:c} else if e.d != undefined then {d:c} else c
- else
- # Take from the inserted string
- if !maxlength? or e.i.length - offset <= maxlength or insertsIndivisible
- result = {i:e.i[offset..]}
- ++index; offset = 0
- else
- result = {i:e.i[offset...offset + maxlength]}
- offset += maxlength
- result
-
- peekType = -> op[index]
-
- [take, peekType]
-
-# Find and return the length of an op component
-componentLength = (component) ->
- if typeof(component) == 'number'
- component
- else if typeof(component.i) == 'string'
- component.i.length
- else
- # This should work because c.d and c.i must be +ive.
- component.d or component.i
-
-# Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
-# adjacent inserts and deletes.
-type.normalize = (op) ->
- newOp = []
- append newOp, component for component in op
- newOp
-
-# This is a helper method to transform and prune. goForwards is true for transform, false for prune.
-transformer = (op, otherOp, goForwards, side) ->
- checkOp op
- checkOp otherOp
- newOp = []
-
- [take, peek] = makeTake op
-
- for component in otherOp
- length = componentLength component
-
- if component.i != undefined # Insert text or tombs
- if goForwards # transform - insert skips over inserted parts
- if side == 'left'
- # The left insert should go first.
- append newOp, take() while peek()?.i != undefined
-
- # In any case, skip the inserted text.
- append newOp, length
-
- else # Prune. Remove skips for inserts.
- while length > 0
- chunk = take length, true
-
- throw new Error 'The transformed op is invalid' unless chunk != null
- throw new Error 'The transformed op deletes locally inserted characters - it cannot be purged of the insert.' if chunk.d != undefined
-
- if typeof chunk is 'number'
- length -= chunk
- else
- append newOp, chunk
-
- else # Skip or delete
- while length > 0
- chunk = take length, true
- throw new Error('The op traverses more elements than the document has') unless chunk != null
-
- append newOp, chunk
- length -= componentLength chunk unless chunk.i
-
- # Append extras from op1
- while (component = take())
- throw new Error "Remaining fragments in the op: #{component}" unless component.i != undefined
- append newOp, component
-
- newOp
-
-# transform op1 by op2. Return transformed version of op1.
-# op1 and op2 are unchanged by transform.
-# side should be 'left' or 'right', depending on if op1.id <> op2.id. 'left' == client op.
-type.transform = (op, otherOp, side) ->
- throw new Error "side (#{side}) should be 'left' or 'right'" unless side == 'left' or side == 'right'
- transformer op, otherOp, true, side
-
-# Prune is the inverse of transform.
-type.prune = (op, otherOp) -> transformer op, otherOp, false
-
-# Compose 2 ops into 1 op.
-type.compose = (op1, op2) ->
- return op2 if op1 == null or op1 == undefined
-
- checkOp op1
- checkOp op2
-
- result = []
-
- [take, _] = makeTake op1
-
- for component in op2
-
- if typeof(component) == 'number' # Skip
- # Just copy from op1.
- length = component
- while length > 0
- chunk = take length
- throw new Error('The op traverses more elements than the document has') unless chunk != null
-
- append result, chunk
- length -= componentLength chunk
-
- else if component.i != undefined # Insert
- append result, {i:component.i}
-
- else # Delete
- length = component.d
- while length > 0
- chunk = take length
- throw new Error('The op traverses more elements than the document has') unless chunk != null
-
- chunkLength = componentLength chunk
- if chunk.i != undefined
- append result, {i:chunkLength}
- else
- append result, {d:chunkLength}
-
- length -= chunkLength
-
- # Append extras from op1
- while (component = take())
- throw new Error "Remaining fragments in op1: #{component}" unless component.i != undefined
- append result, component
-
- result
-
-if WEB?
- exports.types['text-tp2'] = type
-else
- module.exports = type
-
diff --git a/services/web/public/coffee/editor/sharejs/types/text.coffee b/services/web/public/coffee/editor/sharejs/types/text.coffee
deleted file mode 100644
index c64b4dfa68..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/text.coffee
+++ /dev/null
@@ -1,209 +0,0 @@
-# A simple text implementation
-#
-# Operations are lists of components.
-# Each component either inserts or deletes at a specified position in the document.
-#
-# Components are either:
-# {i:'str', p:100}: Insert 'str' at position 100 in the document
-# {d:'str', p:100}: Delete 'str' at position 100 in the document
-#
-# Components in an operation are executed sequentially, so the position of components
-# assumes previous components have already executed.
-#
-# Eg: This op:
-# [{i:'abc', p:0}]
-# is equivalent to this op:
-# [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}]
-
-# NOTE: The global scope here is shared with other sharejs files when built with closure.
-# Be careful what ends up in your namespace.
-
-text = {}
-
-text.name = 'text'
-
-text.create = -> ''
-
-strInject = (s1, pos, s2) -> s1[...pos] + s2 + s1[pos..]
-
-checkValidComponent = (c) ->
- throw new Error 'component missing position field' if typeof c.p != 'number'
-
- i_type = typeof c.i
- d_type = typeof c.d
- throw new Error 'component needs an i or d field' unless (i_type == 'string') ^ (d_type == 'string')
-
- throw new Error 'position cannot be negative' unless c.p >= 0
-
-checkValidOp = (op) ->
- checkValidComponent(c) for c in op
- true
-
-text.apply = (snapshot, op) ->
- checkValidOp op
- for component in op
- if component.i?
- snapshot = strInject snapshot, component.p, component.i
- else
- deleted = snapshot[component.p...(component.p + component.d.length)]
- throw new Error "Delete component '#{component.d}' does not match deleted text '#{deleted}'" unless component.d == deleted
- snapshot = snapshot[...component.p] + snapshot[(component.p + component.d.length)..]
-
- snapshot
-
-
-# Exported for use by the random op generator.
-#
-# For simplicity, this version of append does not compress adjacent inserts and deletes of
-# the same text. It would be nice to change that at some stage.
-text._append = append = (newOp, c) ->
- return if c.i == '' or c.d == ''
- if newOp.length == 0
- newOp.push c
- else
- last = newOp[newOp.length - 1]
-
- # Compose the insert into the previous insert if possible
- if last.i? && c.i? and last.p <= c.p <= (last.p + last.i.length)
- newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p}
- else if last.d? && c.d? and c.p <= last.p <= (c.p + c.d.length)
- newOp[newOp.length - 1] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p}
- else
- newOp.push c
-
-text.compose = (op1, op2) ->
- checkValidOp op1
- checkValidOp op2
-
- newOp = op1.slice()
- append newOp, c for c in op2
-
- newOp
-
-# Attempt to compress the op components together 'as much as possible'.
-# This implementation preserves order and preserves create/delete pairs.
-text.compress = (op) -> text.compose [], op
-
-text.normalize = (op) ->
- newOp = []
-
- # Normalize should allow ops which are a single (unwrapped) component:
- # {i:'asdf', p:23}.
- # There's no good way to test if something is an array:
- # http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
- # so this is probably the least bad solution.
- op = [op] if op.i? or op.p?
-
- for c in op
- c.p ?= 0
- append newOp, c
-
- newOp
-
-# This helper method transforms a position by an op component.
-#
-# If c is an insert, insertAfter specifies whether the transform
-# is pushed after the insert (true) or before it (false).
-#
-# insertAfter is optional for deletes.
-transformPosition = (pos, c, insertAfter) ->
- if c.i?
- if c.p < pos || (c.p == pos && insertAfter)
- pos + c.i.length
- else
- pos
- else
- # I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length))
- # but I think its harder to read that way, and it compiles using ternary operators anyway
- # so its no slower written like this.
- if pos <= c.p
- pos
- else if pos <= c.p + c.d.length
- c.p
- else
- pos - c.d.length
-
-# Helper method to transform a cursor position as a result of an op.
-#
-# Like transformPosition above, if c is an insert, insertAfter specifies whether the cursor position
-# is pushed after an insert (true) or before it (false).
-text.transformCursor = (position, op, side) ->
- insertAfter = side == 'right'
- position = transformPosition position, c, insertAfter for c in op
- position
-
-# Transform an op component by another op component. Asymmetric.
-# The result will be appended to destination.
-#
-# exported for use in JSON type
-text._tc = transformComponent = (dest, c, otherC, side) ->
- checkValidOp [c]
- checkValidOp [otherC]
-
- if c.i?
- append dest, {i:c.i, p:transformPosition(c.p, otherC, side == 'right')}
-
- else # Delete
- if otherC.i? # delete vs insert
- s = c.d
- if c.p < otherC.p
- append dest, {d:s[...otherC.p - c.p], p:c.p}
- s = s[(otherC.p - c.p)..]
- if s != ''
- append dest, {d:s, p:c.p + otherC.i.length}
-
- else # Delete vs delete
- if c.p >= otherC.p + otherC.d.length
- append dest, {d:c.d, p:c.p - otherC.d.length}
- else if c.p + c.d.length <= otherC.p
- append dest, c
- else
- # They overlap somewhere.
- newC = {d:'', p:c.p}
- if c.p < otherC.p
- newC.d = c.d[...(otherC.p - c.p)]
- if c.p + c.d.length > otherC.p + otherC.d.length
- newC.d += c.d[(otherC.p + otherC.d.length - c.p)..]
-
- # This is entirely optional - just for a check that the deleted
- # text in the two ops matches
- intersectStart = Math.max c.p, otherC.p
- intersectEnd = Math.min c.p + c.d.length, otherC.p + otherC.d.length
- cIntersect = c.d[intersectStart - c.p...intersectEnd - c.p]
- otherIntersect = otherC.d[intersectStart - otherC.p...intersectEnd - otherC.p]
- throw new Error 'Delete ops delete different text in the same region of the document' unless cIntersect == otherIntersect
-
- if newC.d != ''
- # This could be rewritten similarly to insert v delete, above.
- newC.p = transformPosition newC.p, otherC
- append dest, newC
-
- dest
-
-invertComponent = (c) ->
- if c.i?
- {d:c.i, p:c.p}
- else
- {i:c.d, p:c.p}
-
-# No need to use append for invert, because the components won't be able to
-# cancel with one another.
-text.invert = (op) -> (invertComponent c for c in op.slice().reverse())
-
-
-if WEB?
- exports.types ||= {}
-
- # This is kind of awful - come up with a better way to hook this helper code up.
- bootstrapTransform(text, transformComponent, checkValidOp, append)
-
- # [] is used to prevent closure from renaming types.text
- exports.types.text = text
-else
- module.exports = text
-
- # The text type really shouldn't need this - it should be possible to define
- # an efficient transform function by making a sort of transform map and passing each
- # op component through it.
- require('./helpers').bootstrapTransform(text, transformComponent, checkValidOp, append)
-
diff --git a/services/web/public/coffee/editor/sharejs/types/text.js b/services/web/public/coffee/editor/sharejs/types/text.js
deleted file mode 100644
index 17fdd0be29..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/text.js
+++ /dev/null
@@ -1,239 +0,0 @@
-// Generated by CoffeeScript 1.4.0
-(function() {
- var append, checkValidComponent, checkValidOp, invertComponent, strInject, text, transformComponent, transformPosition;
-
- text = {};
-
- text.name = 'text';
-
- text.create = function() {
- return '';
- };
-
- strInject = function(s1, pos, s2) {
- return s1.slice(0, pos) + s2 + s1.slice(pos);
- };
-
- checkValidComponent = function(c) {
- var d_type, i_type;
- if (typeof c.p !== 'number') {
- throw new Error('component missing position field');
- }
- i_type = typeof c.i;
- d_type = typeof c.d;
- if (!((i_type === 'string') ^ (d_type === 'string'))) {
- throw new Error('component needs an i or d field');
- }
- if (!(c.p >= 0)) {
- throw new Error('position cannot be negative');
- }
- };
-
- checkValidOp = function(op) {
- var c, _i, _len;
- for (_i = 0, _len = op.length; _i < _len; _i++) {
- c = op[_i];
- checkValidComponent(c);
- }
- return true;
- };
-
- text.apply = function(snapshot, op) {
- var component, deleted, _i, _len;
- checkValidOp(op);
- for (_i = 0, _len = op.length; _i < _len; _i++) {
- component = op[_i];
- if (component.i != null) {
- snapshot = strInject(snapshot, component.p, component.i);
- } else {
- deleted = snapshot.slice(component.p, component.p + component.d.length);
- if (component.d !== deleted) {
- throw new Error("Delete component '" + component.d + "' does not match deleted text '" + deleted + "'");
- }
- snapshot = snapshot.slice(0, component.p) + snapshot.slice(component.p + component.d.length);
- }
- }
- return snapshot;
- };
-
- text._append = append = function(newOp, c) {
- var last, _ref, _ref1;
- if (c.i === '' || c.d === '') {
- return;
- }
- if (newOp.length === 0) {
- return newOp.push(c);
- } else {
- last = newOp[newOp.length - 1];
- if ((last.i != null) && (c.i != null) && (last.p <= (_ref = c.p) && _ref <= (last.p + last.i.length))) {
- return newOp[newOp.length - 1] = {
- i: strInject(last.i, c.p - last.p, c.i),
- p: last.p
- };
- } else if ((last.d != null) && (c.d != null) && (c.p <= (_ref1 = last.p) && _ref1 <= (c.p + c.d.length))) {
- return newOp[newOp.length - 1] = {
- d: strInject(c.d, last.p - c.p, last.d),
- p: c.p
- };
- } else {
- return newOp.push(c);
- }
- }
- };
-
- text.compose = function(op1, op2) {
- var c, newOp, _i, _len;
- checkValidOp(op1);
- checkValidOp(op2);
- newOp = op1.slice();
- for (_i = 0, _len = op2.length; _i < _len; _i++) {
- c = op2[_i];
- append(newOp, c);
- }
- return newOp;
- };
-
- text.compress = function(op) {
- return text.compose([], op);
- };
-
- text.normalize = function(op) {
- var c, newOp, _i, _len, _ref;
- newOp = [];
- if ((op.i != null) || (op.p != null)) {
- op = [op];
- }
- for (_i = 0, _len = op.length; _i < _len; _i++) {
- c = op[_i];
- if ((_ref = c.p) == null) {
- c.p = 0;
- }
- append(newOp, c);
- }
- return newOp;
- };
-
- transformPosition = function(pos, c, insertAfter) {
- if (c.i != null) {
- if (c.p < pos || (c.p === pos && insertAfter)) {
- return pos + c.i.length;
- } else {
- return pos;
- }
- } else {
- if (pos <= c.p) {
- return pos;
- } else if (pos <= c.p + c.d.length) {
- return c.p;
- } else {
- return pos - c.d.length;
- }
- }
- };
-
- text.transformCursor = function(position, op, side) {
- var c, insertAfter, _i, _len;
- insertAfter = side === 'right';
- for (_i = 0, _len = op.length; _i < _len; _i++) {
- c = op[_i];
- position = transformPosition(position, c, insertAfter);
- }
- return position;
- };
-
- text._tc = transformComponent = function(dest, c, otherC, side) {
- var cIntersect, intersectEnd, intersectStart, newC, otherIntersect, s;
- checkValidOp([c]);
- checkValidOp([otherC]);
- if (c.i != null) {
- append(dest, {
- i: c.i,
- p: transformPosition(c.p, otherC, side === 'right')
- });
- } else {
- if (otherC.i != null) {
- s = c.d;
- if (c.p < otherC.p) {
- append(dest, {
- d: s.slice(0, otherC.p - c.p),
- p: c.p
- });
- s = s.slice(otherC.p - c.p);
- }
- if (s !== '') {
- append(dest, {
- d: s,
- p: c.p + otherC.i.length
- });
- }
- } else {
- if (c.p >= otherC.p + otherC.d.length) {
- append(dest, {
- d: c.d,
- p: c.p - otherC.d.length
- });
- } else if (c.p + c.d.length <= otherC.p) {
- append(dest, c);
- } else {
- newC = {
- d: '',
- p: c.p
- };
- if (c.p < otherC.p) {
- newC.d = c.d.slice(0, otherC.p - c.p);
- }
- if (c.p + c.d.length > otherC.p + otherC.d.length) {
- newC.d += c.d.slice(otherC.p + otherC.d.length - c.p);
- }
- intersectStart = Math.max(c.p, otherC.p);
- intersectEnd = Math.min(c.p + c.d.length, otherC.p + otherC.d.length);
- cIntersect = c.d.slice(intersectStart - c.p, intersectEnd - c.p);
- otherIntersect = otherC.d.slice(intersectStart - otherC.p, intersectEnd - otherC.p);
- if (cIntersect !== otherIntersect) {
- throw new Error('Delete ops delete different text in the same region of the document');
- }
- if (newC.d !== '') {
- newC.p = transformPosition(newC.p, otherC);
- append(dest, newC);
- }
- }
- }
- }
- return dest;
- };
-
- invertComponent = function(c) {
- if (c.i != null) {
- return {
- d: c.i,
- p: c.p
- };
- } else {
- return {
- i: c.d,
- p: c.p
- };
- }
- };
-
- text.invert = function(op) {
- var c, _i, _len, _ref, _results;
- _ref = op.slice().reverse();
- _results = [];
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- c = _ref[_i];
- _results.push(invertComponent(c));
- }
- return _results;
- };
-
- if (typeof WEB !== "undefined" && WEB !== null) {
- exports.types || (exports.types = {});
- bootstrapTransform(text, transformComponent, checkValidOp, append);
- exports.types.text = text;
- } else {
- module.exports = text;
- require('./helpers').bootstrapTransform(text, transformComponent, checkValidOp, append);
- }
-
-}).call(this);
diff --git a/services/web/public/coffee/editor/sharejs/types/web-prelude.coffee b/services/web/public/coffee/editor/sharejs/types/web-prelude.coffee
deleted file mode 100644
index 3c045532dc..0000000000
--- a/services/web/public/coffee/editor/sharejs/types/web-prelude.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-# This is included at the top of each compiled type file for the web.
-
-`/**
- @const
- @type {boolean}
-*/
-var WEB = true;
-`
-
-exports = window['sharejs']
-
diff --git a/services/web/public/coffee/event_tracking.coffee b/services/web/public/coffee/event_tracking.coffee
deleted file mode 100644
index 546ad30bbe..0000000000
--- a/services/web/public/coffee/event_tracking.coffee
+++ /dev/null
@@ -1,71 +0,0 @@
-require [
-], ()->
- #plans page
- $('a.sign_up_now').on 'click', (e)->
- ga_PlanType = $(@).attr("ga_PlanType")
- ga 'send', 'event', 'subscription-funnel', 'sign_up_now_button', ga_PlanType
-
- $('#annual-pricing').on 'click', ->
- ga 'send', 'event', 'subscription-funnel', 'plans-page', 'annual-prices'
- $('#student-pricing').on 'click', ->
- ga('send', 'event', 'subscription-funnel', 'plans-page', 'student-prices')
-
- $('#plansLink').on 'click', ->
- ga 'send', 'event', 'subscription-funnel', 'go-to-plans-page', 'from menu bar'
-
-
- #list page
- $('#newProject a').on 'click', (e)->
- ga 'send', 'event', 'project-list-page-interaction', 'new-project', $(@).text().trim()
-
- $('#projectFilter').on 'keydown', (e)->
- ga 'send', 'event', 'project-list-page-interaction', 'project-search', 'keydown'
-
- $('#projectList .project-actions li a').on 'click', (e)->
- ga 'send', 'event', 'project-list-page-interaction', 'project action', $(@).text().trim()
-
-
-
- #left menu navigation
-
- $('.tab-link.account-settings-tab').on 'click', ->
- ga 'send', 'event', 'navigation', 'left menu bar', 'user settings link'
-
- $('.tab-link.subscription-tab').on 'click', ->
- ga 'send', 'event', 'navigation', 'left menu bar', 'subscription managment link'
-
-
-
- #menu bar navigation
-
- $('.userSettingsLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'top menu bar', 'user settings link'
-
- $('.subscriptionLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'top menu bar', 'subscription managment link'
-
- $('.logoutLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'top menu bar', 'logout'
-
- $('#templatesLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'top menu bar', 'templates'
-
- $('#blogLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'top menu bar', 'blog'
-
- $('#learnLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'top menu bar', 'learn link'
-
- $('#resourcesLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'top menu bar', 'resources link'
-
- $('#aboutUsLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'top menu bar', 'about us link'
-
- # editor
-
- $('#hotkeysLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'editor', 'show hot keys link'
-
- $('#editorTourLink').on 'click', ->
- ga 'send', 'event', 'navigation', 'editor', 'editor tour link'
diff --git a/services/web/public/coffee/file-tree/DeletedDocsFolderView.coffee b/services/web/public/coffee/file-tree/DeletedDocsFolderView.coffee
deleted file mode 100644
index 48217e24c5..0000000000
--- a/services/web/public/coffee/file-tree/DeletedDocsFolderView.coffee
+++ /dev/null
@@ -1,33 +0,0 @@
-define [
- "file-tree/FolderView"
-], (FolderView) ->
- DeletedDocsFolderView = FolderView.extend
- template: $("#deletedDocsFolderTemplate").html()
-
- render: () ->
- @$el.append(Mustache.to_html @template, @model.attributes)
- @_bindToDomElements()
- @hideRenameBox()
- @hideToggle()
- @renderEntries()
- @showEntries()
- return @
-
- onClick: () ->
- e.preventDefault()
-
- onToggle: () ->
- e.preventDefault()
-
- getContextMenuEntries: () -> null
-
- hideToggle: () ->
- @$(".js-toggle").hide()
-
- makeReadOnly: () ->
-
- makeReadWrite: () ->
-
-
-
-
diff --git a/services/web/public/coffee/file-tree/DocView.coffee b/services/web/public/coffee/file-tree/DocView.coffee
deleted file mode 100644
index db360b9970..0000000000
--- a/services/web/public/coffee/file-tree/DocView.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-define [
- "file-tree/EntityView"
- "libs/mustache"
-], (EntityView) ->
- DocView = EntityView.extend
- onClick: (e) ->
- e.preventDefault()
- @options.manager.openDoc(@model)
-
diff --git a/services/web/public/coffee/file-tree/EntityView.coffee b/services/web/public/coffee/file-tree/EntityView.coffee
deleted file mode 100644
index b22f2910aa..0000000000
--- a/services/web/public/coffee/file-tree/EntityView.coffee
+++ /dev/null
@@ -1,179 +0,0 @@
-define [
- "utils/ContextMenu"
- "libs/backbone"
- "libs/mustache"
-], (ContextMenu) ->
- EntityView = Backbone.View.extend
- entityTemplate: $("#entityTemplate").html()
-
- initialize: () ->
- @ide = @options.manager.ide
- @manager = @options.manager
- @manager.registerView(@model.id, @)
- @bindToModel()
-
- events: () ->
- events = {}
- events["click ##{@model.id} > .js-clickable"] = "parentOnClick"
- events["click ##{@model.id} > .entity-label"] = "parentOnClick"
- events["click .dropdown-caret"] = "showContextMenuFromCaret"
- events["contextmenu"] = "showContextMenuFromRightClick"
- return events
-
- render: () ->
- @$el.append(Mustache.to_html @entityTemplate, @model.attributes)
- @_bindToDomElements()
- @_initializeRenameBox()
- @_initializeDrag()
- return @
-
- _bindToDomElements: () ->
- @$nameEl = @$(".name")
- @$inputEl = @$("input.js-rename")
- @$entityListItemEl = @$el.children(".entity-list-item")
- @$labelEl = @$entityListItemEl.children(".entity-label")
-
- bindToModel: () ->
- @model.on "change:name", (model) =>
- @$nameEl.text(model.get("name"))
-
- hideRenameBox: () ->
- @$nameEl.show()
- @$inputEl.hide()
-
- showRenameBox: () ->
- @$nameEl.hide()
- @$inputEl.show()
-
- setLabels: (labels) ->
- label = labels[@model.get("id")]
- if label?
- @$entityListItemEl.addClass("show-label")
- @$labelEl.text("±")
- return true
- else
- @$entityListItemEl.removeClass("show-label")
- @$labelEl.text("")
- return false
-
- select: () ->
- @selected = true
- @$entityListItemEl.addClass("selected")
-
- deselect: () ->
- @selected = false
- @$entityListItemEl.removeClass("selected")
-
- isSelected: () ->
- @selected
-
- parentOnClick: (e) ->
- doubleClickInterval = 600
- e.preventDefault()
- if @lastClick and new Date() - @lastClick < doubleClickInterval
- @onDoubleClick(e)
- else
- @lastClick = new Date()
- @onClick(e)
-
- onDoubleClick: (e) ->
- e.preventDefault()
- e.stopPropagation()
- if !@readonly
- @startRename()
-
- showContextMenuFromCaret: (e) ->
- e.stopPropagation()
- caret = @$(".dropdown-caret")
- offset = caret.offset()
- position =
- top: offset.top + caret.outerHeight()
- right: $(document.body).width() - (offset.left + caret.outerWidth())
- @toggleContextMenu(position)
-
- showContextMenuFromRightClick: (e) ->
- e.preventDefault()
- e.stopPropagation()
- position =
- left: e.pageX
- top: e.pageY
- @showContextMenu(position)
-
- toggleContextMenu: (position) ->
- if @contextMenu?
- @contextMenu.destroy()
- else
- @showContextMenu(position)
-
- showContextMenu: (position) ->
- entries = @getContextMenuEntries()
- return if !entries?
-
- @manager.trigger "contextmenu:beforeshow", @model, entries
-
- @contextMenu = new ContextMenu(position, entries)
- @contextMenu.on "destroy", () =>
- delete @contextMenu
-
- getContextMenuEntries: () ->
- return null if @readonly
- return [{
- text: "Rename"
- onClick: () =>
- @startRename()
- ga('send', 'event', 'editor-interaction', 'renameEntity', "entityView")
- }, {
- text: "Delete"
- onClick: () =>
- @manager.confirmDelete(@model)
- ga('send', 'event', 'editor-interaction', 'deleteEntity', "entityView")
- }]
-
- _initializeDrag: () ->
- @$entityListItemEl.draggable
- delay: 250
- opacity: 0.7
- helper: "clone"
- scroll: true
-
- _initializeRenameBox: () ->
- @$inputEl.click (e) -> e.stopPropagation() # Don't stop rename on click in input
- @$inputEl.keydown (event) =>
- code = event.keyCode || event.which
- if code == 13
- @_finishRename()
- @hideRenameBox()
-
- startRename: () ->
- if !@renaming
- @renaming = true
- @showRenameBox()
- name = @model.get("name")
- @$inputEl.val(name).focus()
- if @$inputEl[0].setSelectionRange?
- selectionEnd = name.lastIndexOf(".")
- if selectionEnd == -1
- selectionEnd = name.length
- @$inputEl[0].setSelectionRange(0, selectionEnd)
- setTimeout =>
- $(document.body).on "click.entity-rename", () =>
- @_finishRename()
- , 0
-
- _finishRename: () ->
- $(document.body).off "click.entity-rename"
- @renaming = false
- name = @$inputEl.val()
- @manager.renameEntity(@model, name)
- @hideRenameBox()
-
- makeReadOnly: () ->
- @$entityListItemEl.draggable("disable")
- @readonly = true
-
- makeReadWrite: () ->
- @$entityListItemEl.draggable("enable")
- delete @readonly
-
-
-
diff --git a/services/web/public/coffee/file-tree/FileTreeManager.coffee b/services/web/public/coffee/file-tree/FileTreeManager.coffee
deleted file mode 100644
index 27b7d82d2f..0000000000
--- a/services/web/public/coffee/file-tree/FileTreeManager.coffee
+++ /dev/null
@@ -1,329 +0,0 @@
-define [
- "models/Doc"
- "models/File"
- "models/Folder"
- "file-tree/FileTreeView"
- "file-tree/FolderView"
- "file-tree/DeletedDocsFolderView"
- "utils/Effects"
- "utils/Modal"
- "libs/backbone"
- "libs/jquery.storage"
-], (Doc, File, Folder, FileTreeView, FolderView, DeletedDocsFolderView, Effects, Modal) ->
- class FileTreeManager
- constructor: (@ide) ->
- _.extend(@, Backbone.Events)
- @views = {}
- @multiSelectedEntities = []
- @ide.on "afterJoinProject", (@project) =>
- @populateFileTree()
- @makeReadWriteIfAllowed()
- @project_id = @project.id
- if @ide.editor?.current_doc_id?
- @openDoc(@ide.editor.current_doc_id)
- else if location.hash.length > 1
- fileName = location.hash.slice(1)
- @openDocByPath(fileName)
- else if openDoc_id = $.localStorage("doc.open_id.#{@project_id}") and @getEntity(openDoc_id)
- @openDoc(openDoc_id)
- else if @project.get("rootDoc_id")?
- @openDoc(project.get("rootDoc_id"))
- else
- $('#settings').click()
- @view = new FileTreeView(@)
- @ide.sideBarView.addLink
- identifier: "file-tree"
- element: @view.$el
- prepend: true
- @view.render()
- @listenForUpdates()
-
- populateFileTree: () ->
- @view.bindToRootFolder(@project.get("rootFolder"))
-
- if @deletedDocsView?
- @deletedDocsView.$el.remove()
- @deletedDocsView = new DeletedDocsFolderView(model: @project.get("deletedDocs"), manager: @)
- @deletedDocsView.render()
- $("#sections").append(@deletedDocsView.$el)
- @hideDeletedDocs()
-
- listenForUpdates: () ->
- @ide.socket.on 'reciveNewDoc', (folder_id, doc) =>
- @addEntityToFolder(
- new Doc(id: doc._id, name: doc.name)
- folder_id
- )
-
- @ide.socket.on 'reciveNewFolder', (folder_id, folder) =>
- @addEntityToFolder(
- new Folder(id: folder._id, name: folder.name)
- folder_id
- )
-
- @ide.socket.on 'reciveNewFile', (folder_id, file) =>
- @addEntityToFolder(
- new File(id: file._id, name: file.name)
- folder_id
- )
-
- @ide.socket.on 'removeEntity', (entity_id) =>
- @onDeleteEntity(entity_id)
-
- @ide.socket.on 'reciveEntityRename', (entity_id, newName) =>
- @onRenameEntity(entity_id, newName)
-
- @ide.socket.on 'reciveEntityMove', (entity_id, folder_id) =>
- @onMoveEntity(entity_id, folder_id)
-
- registerView: (entity_id, view) ->
- @views[entity_id] = view
-
- addEntityToFolder: (entity, folder_id) ->
- folder = @views[folder_id].model
- children = folder.get("children")
- children.add(entity)
-
- openDoc: (doc, line) ->
- return if !doc?
- doc_id = doc.id or doc
- @trigger "open:doc", doc_id, line: line
- @selectEntity(doc_id)
- $.localStorage "doc.open_id.#{@project_id}", doc_id
-
- openDocByPath: (path, line) ->
- doc_id = @getDocIdOfPath(path)
- return null if !doc_id?
- @openDoc(doc_id, line)
-
- openFile: (file) ->
- @trigger "open:file", file
- @selectEntity(file.id)
-
- openFolder: (folder) ->
- @selectEntity(folder.id)
-
- selectEntity: (entity_id) ->
- if @views[@selected_entity_id]?
- @views[@selected_entity_id].deselect()
- @selected_entity_id = entity_id
- @ide.sideBarView.deselectAll()
- @views[entity_id]?.select()
-
- getEntity: (entity_id, options = {include_deleted: false}) ->
- model = @views[entity_id]?.model
- if !model? or (model.get("deleted") and !options.include_deleted)
- return
- else
- return model
-
- getSelectedEntity: () -> @getEntity(@selected_entity_id)
- getSelectedEntityId: () -> @getSelectedEntity()?.id
-
- getCurrentFolder: () ->
- selected_entity = @getSelectedEntity()
- if !selected_entity?
- return @project.get("rootFolder")
- else if selected_entity instanceof Folder
- return selected_entity
- else
- return selected_entity.collection.parentFolder
-
- getDocIdOfPath: (path) ->
- parts = path.split("/")
- folder = @project.get("rootFolder")
- lastPart = parts.pop()
-
- getChildWithName = (folder, name) ->
- return folder if name == "."
- foundChild = null
- for child in folder.get("children").models
- if child.get("name") == name
- foundChild = child
- return foundChild
-
- for part in parts
- folder = getChildWithName(folder, part)
- return null if !folder or !(folder instanceof Folder)
-
- doc = getChildWithName(folder, lastPart)
- return null if !doc or !(doc instanceof Doc)
- return doc.id
-
- getPathOfEntityId: (entity_id) ->
- entity = @getEntity(entity_id)
- return if !entity?
- path = entity.get("name")
- while (entity = entity.collection?.parentFolder)
- if entity.collection?
- # it's not the root folder so keep going
- path = entity.get("name") + "/" + path
- return path
-
- getRootFolderPath: () ->
- rootFilePath = @getPathOfEntityId(@project.get("rootDoc_id"))
- return rootFilePath.split("/").slice(0, -1).join("/")
-
- getNameOfEntityId: (entity_id) ->
- entity = @getEntity(entity_id)
- return if !entity?
- return entity.get("name")
-
- # RENAMING
- renameSelected: () ->
- entity_id = @getSelectedEntityId()
- return if !entity_id?
- @views[entity_id]?.startRename()
- ga('send', 'event', 'editor-interaction', 'renameEntity', "topMenu")
-
-
- renameEntity: (entity, name) ->
- name = name?.trim()
- @ide.socket.emit 'renameEntity', entity.id, entity.get("type"), name
- entity.set("name", name)
-
- onRenameEntity: (entity_id, name) ->
- @getEntity(entity_id)?.set("name", name)
-
- # MOVING
- onMoveEntity: (entity_id, folder_id) ->
- entity = @getEntity(entity_id)
- destFolder = @getEntity(folder_id)
- return if !entity? or !destFolder?
- if entity.collection == destFolder.get("children")
- # Already in parent folder
- return
- return if @_isParent(entity_id, folder_id)
-
- entity.collection.remove(entity)
- destFolder.get("children").add(entity)
-
- _isParent: (parent_folder_id, child_folder_id) ->
- childFolder = @getEntity(child_folder_id)
- return false unless childFolder? and childFolder instanceof Folder
- parentIds = childFolder.getParentFolderIds()
- if parentIds.indexOf(parent_folder_id) > -1
- return true
- else
- return false
-
- moveEntity: (entity_id, folder_id, type) ->
- return if @_isParent(entity_id, folder_id)
- @ide.socket.emit 'moveEntity', entity_id, folder_id, type
- @onMoveEntity(entity_id, folder_id)
-
- # CREATING
- showNewEntityModal: (type, defaultName, callback = (name) ->) ->
- el = $($("#newEntityModalTemplate").html())
- input = el.find("input")
- create = _.once () =>
- name = input.val()?.trim()
- if name != ""
- callback(name)
- modal = new Modal
- title: "New #{type}"
- el: el
- buttons: [{
- text: "Cancel"
- }, {
- text: "Create"
- callback: create
- class: "btn-primary"
- }]
- input.on "keydown", (e) ->
- if e.keyCode == 13 # Enter
- create()
- modal.remove()
-
- input.val(defaultName.replace("|", ""))
- if input[0].setSelectionRange?
- # value is "name.tex"
- input[0].setSelectionRange(0, defaultName.indexOf("|"))
-
- showNewDocModal: (parentFolder = @getCurrentFolder()) ->
- return if !parentFolder?
- @showNewEntityModal "Document", "name|.tex", (name) =>
- @addDocToFolder parentFolder, name
-
- showNewFolderModal: (parentFolder = @getCurrentFolder()) ->
- return if !parentFolder?
- @showNewEntityModal "Folder", "name|", (name) =>
- @addFolderToFolder parentFolder, name
-
- showUploadFileModal: (parentFolder = @getCurrentFolder()) ->
- return if !parentFolder?
- @ide.fileUploadManager.showUploadDialog parentFolder.id
-
- addDoc: (folder_id, name) ->
- @ide.socket.emit 'addDoc', folder_id, name
-
- addDocToFolder: (parentFolder, name) ->
- @addDoc parentFolder.id, name
-
- addFolder: (parent_folder_id, name) ->
- @ide.socket.emit 'addFolder', parent_folder_id, name
-
- addFolderToFolder: (parentFolder, name) ->
- return if !parentFolder?
- @addFolder parentFolder.id, name
-
- # DELETING
- confirmDelete: (entity) ->
- ga('send', 'event', 'editor-interaction', 'deleteEntity', "topMenu")
- Modal.createModal
- title: "Confirm Deletion"
- message: "Are you sure you want to delete #{entity.get("name")}?"
- buttons: [{
- text: "Cancel"
- class: "btn"
- },{
- text: "Delete"
- class: "btn btn-danger"
- callback: () => @_doDelete(entity)
- }]
-
- confirmDeleteOfSelectedEntity: () ->
- entity = @getSelectedEntity()
- return if !entity?
- @confirmDelete(entity)
-
- _doDelete: (entity) ->
- @ide.socket.emit 'deleteEntity', entity.id, entity.get("type")
- @onDeleteEntity entity.id
-
- onDeleteEntity: (entity_id) ->
- entity = @getEntity(entity_id)
- return if !entity?
- entity.set("deleted", true)
- entity.collection?.remove(entity)
- delete @views[entity_id]
-
- # Do this after the remove so that it's never in two places at once
- # and so that it doesn't get reset by deleting from @views
- if entity.get("type") == "doc"
- @project.get("deletedDocs").get("children").add entity
-
- setLabels: (labels) ->
- @view.setLabels(labels)
- @deletedDocsView.setLabels(labels)
-
- showDeletedDocs: () ->
- if @project.get("deletedDocs").get("children").length > 0
- @deletedDocsView.$el.show()
-
- hideDeletedDocs: () ->
- @deletedDocsView.$el.hide()
-
- makeReadOnly: () ->
- for id, view of @views or []
- view.makeReadOnly?()
-
- makeReadWrite: () ->
- for id, view of @views or []
- view.makeReadWrite?()
-
- makeReadWriteIfAllowed: () ->
- if @ide.isAllowedToDoIt("readAndWrite")
- @makeReadWrite()
- else
- @makeReadOnly()
diff --git a/services/web/public/coffee/file-tree/FileTreeView.coffee b/services/web/public/coffee/file-tree/FileTreeView.coffee
deleted file mode 100644
index d06dfe52d4..0000000000
--- a/services/web/public/coffee/file-tree/FileTreeView.coffee
+++ /dev/null
@@ -1,25 +0,0 @@
-define [
- "file-tree/RootFolderView"
- "libs/backbone"
-], (RootFolderView) ->
- FileTreeView = Backbone.View.extend
- initialize: (@manager) ->
-
- template: $("#fileTreeTemplate").html()
-
- render: () ->
- @$el.append($(@template))
- return @
-
- bindToRootFolder: (rootFolder) ->
- entities = @$('.js-file-tree')
- # This is hacky, we're doing nothing to clean up the old folder tree
- # from memory, just removing it from the DOM.
- entities.empty()
- @rootFolderView = new RootFolderView(model: rootFolder, manager: @manager)
- entities.append(@rootFolderView.$el)
- @rootFolderView.render()
-
- setLabels: (labels) ->
- @rootFolderView.setLabels(labels)
-
diff --git a/services/web/public/coffee/file-tree/FileView.coffee b/services/web/public/coffee/file-tree/FileView.coffee
deleted file mode 100644
index 42078691d6..0000000000
--- a/services/web/public/coffee/file-tree/FileView.coffee
+++ /dev/null
@@ -1,8 +0,0 @@
-define [
- "file-tree/EntityView"
- "libs/mustache"
-], (EntityView) ->
- FileView = EntityView.extend
- onClick: (e) ->
- e.preventDefault()
- @options.manager.openFile(@model)
diff --git a/services/web/public/coffee/file-tree/FolderView.coffee b/services/web/public/coffee/file-tree/FolderView.coffee
deleted file mode 100644
index e62140a538..0000000000
--- a/services/web/public/coffee/file-tree/FolderView.coffee
+++ /dev/null
@@ -1,165 +0,0 @@
-define [
- "models/Folder"
- "models/Doc"
- "models/File"
- "file-tree/EntityView"
- "file-tree/DocView"
- "file-tree/FileView"
- "utils/Modal"
- "utils/Effects"
- "libs/mustache"
-], (Folder, Doc, File, EntityView, DocView, FileView, Modal, Effects) ->
- FolderView = EntityView.extend
- templates:
- childList: $("#entityListTemplate").html()
-
- entityTemplate: $("#folderTemplate").html()
-
- events: () ->
- events = EntityView::events.apply(this)
- events["click ##{@model.id} > .js-toggle"] = "onToggle"
- return events
-
- render: () ->
- EntityView::render.apply(this, arguments)
- @renderEntries()
- return @
-
- renderEntries: () ->
- @$el.append(Mustache.to_html @templates.childList, @model.attributes)
- @$contents = @$(".contents")
- @$childList = @$(".entity-list")
- @$menu = @$(".js-new-entity-menu")
- @$deleteButton = @$(".js-delete-btn")
- @$toggle = @$entityListItemEl.children(".js-toggle")
- @_renderChildViews()
- @_initializeDrop()
- @hideEntries()
-
- _renderChildViews: () ->
- throw "Already rendered children" unless !@views?
- @views = []
- @model.get("children").each (child) =>
- view = @_buildViewForModel(child)
- @views.push view
- @$childList.append(view.$el)
- view.render()
- @bindToCollection()
-
- renderNewEntry: (model, index) ->
- view = @_buildViewForModel(model)
- @views.splice(index, 0, view)
- if index == 0
- @$childList.prepend(view.$el)
- else
- view.$el.insertAfter(@views[index-1].$el)
- view.render()
- Effects.fadeElementIn view.$el
-
- removeEntry: (model, index) ->
- view = @views[index]
- @views.splice(index,1)
- if model.get("deleted")
- Effects.fadeElementOut view.$el, () ->
- view.remove()
- else
- view.remove()
-
- _buildViewForModel: (model) ->
- attrs = model: model, manager: @options.manager
- if model instanceof Folder
- view = new FolderView(attrs)
- else if model instanceof Doc
- view = new DocView(attrs)
- else
- view = new FileView(attrs)
- return view
-
- _initializeDrop: () ->
- onDrop = (event, ui) =>
- if event.target == @$childList[0] or event.target == @$entityListItemEl[0]
- entity = ui.draggable
- entity_id = entity.attr("id")
- entity_type = entity.attr("entity-type")
- @manager.moveEntity entity_id, @model.id, entity_type
-
- @$entityListItemEl.droppable
- greedy: true
- hoverClass: "droppable-folder-hover"
- drop: onDrop
-
- @$childList.droppable
- greedy: true
- hoverClass: "droppable-folder-hover"
- drop: onDrop
-
- bindToCollection: () ->
- @model.get("children").on "add", (model, folderCollection, data) =>
- @renderNewEntry(model, data.index)
- @model.get("children").on "remove", (model, folderCollection, data) =>
- @removeEntry(model, data.index)
-
- onClick: (e) ->
- e.preventDefault()
- @options.manager.openFolder(@model)
-
- hideEntries: () ->
- @$contents.hide()
- @$toggle.find(".js-open").hide()
- @$toggle.find(".js-closed").show()
- @$entityListItemEl.removeClass("folder-open")
-
- showEntries: () ->
- @$contents.show()
- @$toggle.find(".js-open").show()
- @$toggle.find(".js-closed").hide()
- @$entityListItemEl.addClass("folder-open")
-
- onToggle: (e) ->
- e.preventDefault()
- if @$contents.is(":visible")
- @hideEntries()
- else
- @showEntries()
-
- getContextMenuEntries: (args...) ->
- return null if @readonly
- entries = EntityView::getContextMenuEntries.apply(this, args)
- entries.push {
- divider: true
- }
- entries.push @getFolderContextMenuEntries()...
- return entries
-
- getFolderContextMenuEntries: () ->
- return [{
- text: "New file"
- onClick: () =>
- ga('send', 'event', 'editor-interaction', 'newFile', "folderView")
- @manager.showNewDocModal(@model)
- }, {
- text: "New folder"
- onClick: () =>
- ga('send', 'event', 'editor-interaction', 'newFolder', "folderView")
- @manager.showNewFolderModal(@model)
- }, {
- text: "Upload file"
- onClick: () =>
- ga('send', 'event', 'editor-interaction', 'uploadFile', "folderView")
- @manager.showUploadFileModal(@model)
- }]
-
- setLabels: (labels) ->
- showLabel = false
- for entity in @views
- if entity.setLabels(labels)
- showLabel = true
-
- if showLabel
- @$entityListItemEl.addClass("show-label")
- @$labelEl.text("±")
- return true
- else
- @$entityListItemEl.removeClass("show-label")
- @$labelEl.text("")
- return false
diff --git a/services/web/public/coffee/file-tree/RootFolderView.coffee b/services/web/public/coffee/file-tree/RootFolderView.coffee
deleted file mode 100644
index ff5c1c20c3..0000000000
--- a/services/web/public/coffee/file-tree/RootFolderView.coffee
+++ /dev/null
@@ -1,71 +0,0 @@
-define [
- "file-tree/FolderView"
-], (FolderView) ->
- RootFolderView = FolderView.extend
- actionsTemplate: $("#fileTreeActionsTemplate").html()
-
- events: () ->
- events = FolderView::events.apply(this)
- if @ide.isAllowedToDoIt("readAndWrite")
- _.extend(events,
- "click .js-new-file" : (e) ->
- e.preventDefault()
- @manager.showNewDocModal()
- ga('send', 'event', 'editor-interaction', 'newFile', "topMenu")
- "click .js-new-folder" : (e) ->
- e.preventDefault()
- @manager.showNewFolderModal()
- ga('send', 'event', 'editor-interaction', 'newFolder', "topMenu")
- "click .js-upload-file" : (e) ->
- e.preventDefault()
- @manager.showUploadFileModal()
- ga('send', 'event', 'editor-interaction', 'uploadFile', "topMenu")
- "click .js-delete-btn" : (e) ->
- e.preventDefault()
- @manager.confirmDeleteOfSelectedEntity()
- ga('send', 'event', 'editor-interaction', 'deleteEntity', "topMenu")
- "click .js-rename-btn" : (e) ->
- e.preventDefault()
- @manager.renameSelected()
- ga('send', 'event', 'editor-interaction', 'renameEntity', "topMenu")
- )
-
- render: () ->
- @$el.append(Mustache.to_html @entityTemplate, {
- name: @manager.project.get("name")
- type: "project"
- })
- @_bindToDomElements()
- @renderActions()
- @hideRenameBox()
- @hideToggle()
- @renderEntries()
- @showEntries()
- return @
-
- renderActions: () ->
- @$actions = $(@actionsTemplate)
- @$actions.insertAfter(@$entityListItemEl)
- @$(".js-new-entity-menu > a").dropdown()
-
- onClick: () ->
- e.preventDefault()
-
- onToggle: () ->
- e.preventDefault()
-
- getContextMenuEntries: () ->
- @getFolderContextMenuEntries()
-
- hideToggle: () ->
- @$(".js-toggle").hide()
-
- makeReadOnly: () ->
- @$actions.hide()
-
- makeReadWrite: () ->
- @$actions.show()
-
-
-
-
diff --git a/services/web/public/coffee/file-view/FileView.coffee b/services/web/public/coffee/file-view/FileView.coffee
deleted file mode 100644
index 51d1e33959..0000000000
--- a/services/web/public/coffee/file-view/FileView.coffee
+++ /dev/null
@@ -1,29 +0,0 @@
-define [
- "libs/backbone"
- "libs/mustache"
-], () ->
- FileViewManager = Backbone.View.extend
- template: $("#fileViewTemplate").html()
- className: "fullEditorArea"
- id: "fileViewArea"
-
- render: () ->
- extension = @model.get("name").split(".").pop().toLowerCase()
- image = (["jpg", "jpeg", "png", "gif", "eps", "pdf"].indexOf(extension) != -1)
- html = Mustache.to_html(@template, {
- name: @model.get("name")
- downloadUrl: @model.downloadUrl()
- previewUrl: @model.previewUrl()
- image: image
- })
- @$el.html(html)
-
- setModel: (model) ->
- @model = model
- @render()
- @onResize()
-
- onResize: () ->
- @$("img").css
- "max-width": ($("#fileViewArea").width() - 40) + "px"
- "max-height": ($("#fileViewArea").height() - 140) + "px"
diff --git a/services/web/public/coffee/file-view/FileViewManager.coffee b/services/web/public/coffee/file-view/FileViewManager.coffee
deleted file mode 100644
index 4f414493b9..0000000000
--- a/services/web/public/coffee/file-view/FileViewManager.coffee
+++ /dev/null
@@ -1,32 +0,0 @@
-define [
- "file-view/FileView"
-], (FileView) ->
- class FileViewManager
- constructor: (@ide) ->
- @view = new FileView()
-
- @ide.mainAreaManager.addArea
- identifier: "file"
- element: @view.$el
-
- $(window).resize () => @view.onResize()
- @ide.layoutManager.on "resize", () => @view.onResize()
- @view.onResize()
-
- @bindToFileTreeEvents()
- @enable()
-
- bindToFileTreeEvents: () ->
- @ide.fileTreeManager.on "open:file", (file) =>
- if @enabled
- @showFile(file)
-
- showFile: (file) ->
- @ide.mainAreaManager.change('file')
- @view.setModel(file)
-
- enable: () ->
- @enabled = true
-
- disable: () ->
- @enabled = false
diff --git a/services/web/public/coffee/help/HelpManager.coffee b/services/web/public/coffee/help/HelpManager.coffee
deleted file mode 100644
index 07416f81fe..0000000000
--- a/services/web/public/coffee/help/HelpManager.coffee
+++ /dev/null
@@ -1,13 +0,0 @@
-define [], () ->
- class HelpManager
- template: $("#helpLinkTemplate").html()
-
- constructor: (@ide) ->
- @$el = $(@template)
- $("#toolbar-footer").append(@$el)
- @$el.on "click", (e) ->
- e.preventDefault()
- window.open("/learn", "_latex_help")
-
-
-
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index 7b8961901e..393b77219c 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -1,210 +1,70 @@
define [
- "ide/ConnectionManager"
- "history/HistoryManager"
- "auto-complete/AutoCompleteManager"
- "project-members/ProjectMembersManager"
- "settings/SettingsManager"
- "editor/Editor"
- "pdf/PdfManager"
- "ide/MainAreaManager"
- "ide/SideBarManager"
- "ide/TabManager"
- "ide/LayoutManager"
- "ide/FileUploadManager"
- "ide/SavingAreaManager"
- "spelling/SpellingManager"
- "search/SearchManager"
- "models/Project"
- "models/User"
- "utils/Modal"
- "file-tree/FileTreeManager"
- "messages/MessageManager"
- "help/HelpManager"
- "cursors/CursorManager"
- "keys/HotkeysManager"
- "keys/BackspaceHighjack"
- "file-view/FileViewManager"
- "tour/IdeTour"
- "analytics/AnalyticsManager"
- "track-changes/TrackChangesManager"
- "debug/DebugManager"
- "ace/ace"
- "libs/jquery.color"
- "libs/jquery-layout"
- "libs/backbone"
+ "base"
+ "ide/file-tree/FileTreeManager"
+ "ide/connection/ConnectionManager"
+ "ide/editor/EditorManager"
+ "ide/online-users/OnlineUsersManager"
+ "ide/track-changes/TrackChangesManager"
+ "ide/permissions/PermissionsManager"
+ "ide/pdf/PdfManager"
+ "ide/binary-files/BinaryFilesManager"
+ "ide/settings/index"
+ "ide/share/index"
+ "ide/chat/index"
+ "ide/directives/layout"
+ "ide/services/ide"
+ "directives/focus"
+ "directives/fineUpload"
+ "directives/onEnter"
+ "filters/formatDate"
], (
- ConnectionManager,
- HistoryManager,
- AutoCompleteManager,
- ProjectMembers,
- SettingsManager,
- Editor,
- PdfManager,
- MainAreaManager,
- SideBarManager,
- TabManager,
- LayoutManager,
- FileUploadManager,
- SavingAreaManager,
- SpellingManager,
- SearchManager,
- Project,
- User,
- Modal,
- FileTreeManager,
- MessageManager,
- HelpManager,
- CursorManager,
- HotkeysManager,
- BackspaceHighjack,
- FileViewManager,
- IdeTour,
- AnalyticsManager,
+ App
+ FileTreeManager
+ ConnectionManager
+ EditorManager
+ OnlineUsersManager
TrackChangesManager
- DebugManager
+ PermissionsManager
+ PdfManager
+ BinaryFilesManager
) ->
-
-
-
- ProjectMembersManager = ProjectMembers.ProjectMembersManager
-
- mainAreaManager = undefined
- socket = undefined
- currentDoc_id = undefined
- selectElement = undefined
- security = undefined
- _.templateSettings =
- interpolate : /\{\{(.+?)\}\}/g
-
- isAllowedToDoIt = (permissionsLevel)->
-
- if permissionsLevel == "owner" && _.include ["owner"], security.permissionsLevel
- return true
- else if permissionsLevel == "readAndWrite" && _.include ["readAndWrite", "owner"], security.permissionsLevel
- return true
- else if permissionsLevel == "readOnly" && _.include ["readOnly", "readAndWrite", "owner"], security.permissionsLevel
- return true
- else
- return false
-
- Ide = class Ide
- constructor: () ->
- @userSettings = window.userSettings
- @project_id = @userSettings.project_id
-
- @user = User.findOrBuild window.user.id, window.user
-
- ide = this
- @isAllowedToDoIt = isAllowedToDoIt
-
- ioOptions =
- reconnect: false
- "force new connection": true
- @socket = socket = io.connect null, ioOptions
-
- @messageManager = new MessageManager(@)
- @connectionManager = new ConnectionManager(@)
- @tabManager = new TabManager(@)
- @layoutManager = new LayoutManager(@)
- @sideBarView = new SideBarManager(@, $("#sections"))
- selectElement = @sideBarView.selectElement
- mainAreaManager = @mainAreaManager = new MainAreaManager(@, $("#content"))
- @fileTreeManager = new FileTreeManager(@)
- @editor = new Editor(@)
- @pdfManager = new PdfManager(@)
- if @userSettings.autoComplete
- @autoCompleteManager = new AutoCompleteManager(@)
- @spellingManager = new SpellingManager(@)
- @fileUploadManager = new FileUploadManager(@)
- @searchManager = new SearchManager(@)
- @cursorManager = new CursorManager(@)
- @fileViewManager = new FileViewManager(@)
- @analyticsManager = new AnalyticsManager(@)
- if @userSettings.oldHistory
- @historyManager = new HistoryManager(@)
+ App.controller "IdeController", ["$scope", "$timeout", "ide", ($scope, $timeout, ide) ->
+ # Don't freak out if we're already in an apply callback
+ $scope.$originalApply = $scope.$apply
+ $scope.$apply = (fn = () ->) ->
+ phase = @$root.$$phase
+ if (phase == '$apply' || phase == '$digest')
+ fn()
else
- @trackChangesManager = new TrackChangesManager(@)
+ this.$originalApply(fn);
- @setLoadingMessage("Connecting")
- firstConnect = true
- socket.on "connect", () =>
- @setLoadingMessage("Joining project")
- joinProject = () =>
- socket.emit 'joinProject', {project_id: @project_id}, (err, project, permissionsLevel, protocolVersion) =>
- @hideLoadingScreen()
- if @protocolVersion? and @protocolVersion != protocolVersion
- location.reload(true)
- @protocolVersion = protocolVersion
- Security = {}
- Security.permissionsLevel = permissionsLevel
- @security = security = Object.freeze(Security)
- @project = new Project project, parse: true
- @project.set("ide", ide)
- ide.trigger "afterJoinProject", @project
+ $scope.state = {
+ loading: true
+ load_progress: 40
+ }
+ $scope.ui = {
+ leftMenuShown: false
+ view: "editor"
+ chatOpen: false
+ }
+ $scope.user = window.user
+ $scope.settings = window.userSettings
- if firstConnect
- @pdfManager.refreshPdf(isAutoCompile:true)
- firstConnect = false
+ $scope.chat = {}
- setTimeout(joinProject, 100)
-
- showErrorModal: (title, message)->
- new Modal {
- title: title
- message: message
- buttons: [ text: "OK" ]
- }
+ window._ide = ide
- showGenericServerErrorMessage: ()->
- new Modal {
- title: "There was a problem talking to the server"
- message: "Sorry, we couldn't complete your request right now. Please wait a few moments and try again. If the problem persists, please let us know."
- buttons: [ text: "OK" ]
- }
+ ide.project_id = $scope.project_id = window.project_id
+ ide.$scope = $scope
- recentEvents: []
-
- pushEvent: (type, meta = {}) ->
- @recentEvents.push type: type, meta: meta, date: new Date()
- if @recentEvents.length > 40
- @recentEvents.shift()
-
- reportError: (error, meta = {}) ->
- meta.client_id = @socket?.socket?.sessionid
- meta.transport = @socket?.socket?.transport?.name
- meta.client_now = new Date()
- meta.recent_events = @recentEvents
- errorObj = {}
- if typeof error == "object"
- for key in Object.getOwnPropertyNames(error)
- errorObj[key] = error[key]
- else if typeof error == "string"
- errorObj.message = error
- $.ajax
- url: "/error/client"
- type: "POST"
- data: JSON.stringify
- error: errorObj
- meta: meta
- contentType: "application/json; charset=utf-8"
- headers:
- "X-Csrf-Token": window.csrfToken
-
- setLoadingMessage: (message) ->
- $("#loadingMessage").text(message)
-
- hideLoadingScreen: () ->
- $("#loadingScreen").remove()
-
- _.extend(Ide::, Backbone.Events)
- window.ide = ide = new Ide()
- ide.projectMembersManager = new ProjectMembersManager ide
- ide.settingsManager = new SettingsManager ide
- ide.helpManager = new HelpManager ide
- ide.hotkeysManager = new HotkeysManager ide
- ide.layoutManager.resizeAllSplitters()
- ide.tourManager = new IdeTour ide
- ide.debugManager = new DebugManager(ide)
-
- ide.savingAreaManager = new SavingAreaManager(ide)
+ ide.connectionManager = new ConnectionManager(ide, $scope)
+ 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.pdfManager = new PdfManager(ide, $scope)
+ ide.permissionsManager = new PermissionsManager(ide, $scope)
+ ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
+ ]
+ angular.bootstrap(document.body, ["SharelatexApp"])
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/ConnectionManager.coffee b/services/web/public/coffee/ide/ConnectionManager.coffee
deleted file mode 100644
index ae54c7946b..0000000000
--- a/services/web/public/coffee/ide/ConnectionManager.coffee
+++ /dev/null
@@ -1,87 +0,0 @@
-define [
- "utils/Modal"
- "libs/backbone"
-], (Modal) ->
- class ConnectionManager
- constructor: (@ide) ->
- @connected = false
- @socket = @ide.socket
- @socket.on "connect", () =>
- @connected = true
- @ide.pushEvent("connected")
- @hideModal()
- @cancelReconnect()
-
- @socket.on 'disconnect', () =>
- @connected = false
- @ide.pushEvent("disconnected")
- @ide.trigger "disconnect"
- setTimeout(=>
- ga('send', 'event', 'editor-interaction', 'disconnect')
- , 2000)
-
- if !@forcedDisconnect
- @showModalAndStartAutoReconnect()
-
- @socket.on 'forceDisconnect', (message) =>
- @showModal(message)
- @forcedDisconnect = true
- @socket.disconnect()
-
- @messageEl = $("#connectionLostMessage")
- $('#try-reconnect-now').on 'click', (e) =>
- e.preventDefault()
- @tryReconnect()
- @hideModal()
-
- reconnectImmediately: () ->
- @disconnect()
- @tryReconnect()
-
- disconnect: () ->
- @socket.disconnect()
-
- showModalAndStartAutoReconnect: () ->
- @hideModal()
- twoMinutes = 2 * 60 * 1000
- if @ide.editor? and @ide.editor.lastUpdated? and new Date() - @ide.editor.lastUpdated > twoMinutes
- # between 1 minute and 3 minutes
- @countdown = 60 + Math.floor(Math.random() * 120)
- else
- @countdown = 3 + Math.floor(Math.random() * 7)
-
- $("#reconnection-countdown").text(@countdown)
-
- setTimeout(=>
- if !@connected
- @showModal()
- @timeoutId = setTimeout (=> @decreaseCountdown()), 1000
- , 200)
-
- showModal: () =>
- @messageEl.show()
- $("#reconnecting").hide()
- $("#trying-reconnect").show()
-
- hideModal: () ->
- @messageEl.hide()
-
- cancelReconnect: () ->
- clearTimeout @timeoutId if @timeoutId?
-
- decreaseCountdown: () ->
- @countdown--
- $("#reconnection-countdown").text(@countdown)
-
- if @countdown <= 0
- @tryReconnect()
- else
- @timeoutId = setTimeout (=> @decreaseCountdown()), 1000
-
- tryReconnect: () ->
- @cancelReconnect()
- $("#reconnecting").show()
- $("#trying-reconnect").hide()
- @socket.socket.reconnect()
- setTimeout (=> @showModalAndStartAutoReconnect() if !@connected), 1000
-
diff --git a/services/web/public/coffee/ide/FileUploadManager.coffee b/services/web/public/coffee/ide/FileUploadManager.coffee
deleted file mode 100644
index f8f2fd16b2..0000000000
--- a/services/web/public/coffee/ide/FileUploadManager.coffee
+++ /dev/null
@@ -1,63 +0,0 @@
-define [
- "utils/Modal"
- "libs/fineuploader"
-], (Modal) ->
- class FileUploadManager
- constructor: (@ide) ->
- @ide.on "afterJoinProject", () =>
- if @ide.isAllowedToDoIt "readAndWrite"
- $('button#upload-file').click (e)=>
- @showUploadDialog()
-
- showUploadDialog: (folder_id, callback = (error, entity_ids) ->) ->
- uploaderEl = $("")
- modal = new Modal
- title: "Upload file"
- el: uploaderEl
- buttons: [{
- text: "Close"
- }]
-
- uploadCount = 0
- entity_ids = []
- new qq.FineUploader
- element: uploaderEl[0]
- disabledCancelForFormUploads: true
- maxConnections: 1
- request:
- endpoint: "/Project/#{@ide.project.id}/upload"
- params:
- folder_id: folder_id
- _csrf: csrfToken
- paramsInBody: false
- forceMultipart: true
- callbacks:
- onUpload: () -> uploadCount++
- onComplete: (error, name, response) ->
- setTimeout (() ->
- uploadCount--
- entity_ids.push response.entity_id
- if uploadCount == 0 and response? and response.success
- modal.remove()
- callback null, entity_ids
- ), 250
- text:
- waitingForResponse: "Inserting file..."
- failUpload: "Upload failed, sorry :("
- uploadButton: "Select file(s)"
- template: """
-
-
{dragZoneText}
-
-
or
-
drag file(s)
-
{dropProcessingText}
-
Hint: Press and hold the Control (Ctrl) key to select multiple files
-
-
- """
- $(".qq-uploader input").addClass("js-file-uploader")
-
-
diff --git a/services/web/public/coffee/ide/LayoutManager.coffee b/services/web/public/coffee/ide/LayoutManager.coffee
deleted file mode 100644
index e7a09e36d7..0000000000
--- a/services/web/public/coffee/ide/LayoutManager.coffee
+++ /dev/null
@@ -1,76 +0,0 @@
-define [
- "underscore",
- "libs/backbone",
- "libs/jquery-layout"
- "libs/jquery.storage"
-], () ->
- class LayoutManager
- constructor: (@ide) ->
- _.extend @, Backbone.Events
-
- template = $("#editorLayoutTemplate").html()
- el = $(template)
- @ide.tabManager.addTab {
- id: "code"
- name: "Code"
- content: el
- active: true
- contract: true
- onShown: () =>
- @resizeAllSplitters()
- }
-
- $(window).resize () =>
- @refreshHeights()
-
- @refreshHeights()
-
- @initLayout()
-
- $(window).keypress (event)->
- if (!(event.which == 115 && event.ctrlKey) && !(event.which == 19))
- return true
- event.preventDefault()
- return false
-
- @refreshHeights()
-
- initLayout: () ->
- options =
- spacing_open: 8
- spacing_closed: 16
- onresize: () =>
- @.trigger("resize")
-
- if (state = $.localStorage("layout.main"))?
- options.west =
- state.west
-
- $("#mainSplitter").layout options
-
- $(window).unload () ->
- $.localStorage("layout.main", $("#mainSplitter").layout().readState())
-
- refreshHeights: ->
- @setSplitterHeight()
- @setSectionsHeight()
- @setTopOffset()
-
- setSplitterHeight: () ->
- $("#mainSplitter").height($(window).height() - $(".navbar").outerHeight())
-
- setTopOffset: () ->
- $("#toolbar").css(top: $(".navbar").outerHeight())
- $("#tab-content").css(top: $(".navbar").outerHeight())
-
- setSectionsHeight: ()->
- $sections = $('#sections')
- $chatArea = $('#chatArea')
- availableSpace = $(window).height() - 40 - 20 - 10
- if $chatArea.is(':visible')
- availableSpace -= 200
- $sections.height(availableSpace)
-
- resizeAllSplitters : ->
- $("#mainSplitter").layout().resizeAll()
- $("#editorSplitter").layout().resizeAll()
diff --git a/services/web/public/coffee/ide/MainAreaManager.coffee b/services/web/public/coffee/ide/MainAreaManager.coffee
deleted file mode 100644
index a28ebb559c..0000000000
--- a/services/web/public/coffee/ide/MainAreaManager.coffee
+++ /dev/null
@@ -1,52 +0,0 @@
-define () ->
- class MainAreaManager
- constructor: (@ide, @el) ->
- @$iframe = $('#imageArea')
- @$loading = $('#loading')
- @$currentArea = $('#loading')
- @areas = {}
-
- addArea: (options) ->
- @areas ||= {}
- @areas[options.identifier] = options.element
- options.element.hide()
- @el.append(options.element)
-
- removeArea: (identifier) ->
- @areas ||= {}
- if @areas[identifier]?
- if @$currentArea == @areas[identifier]
- delete @$currentArea
- @areas[identifier].remove()
- delete @areas[identifier]
-
- getAreaElement: (identifier) ->
- @areas[identifier]
-
- setIframeSrc: (src)->
- $('#imageArea iframe').attr 'src', src
-
- change : (type, complete)->
- if @areas[type]?
- @$currentArea.hide() if @$currentArea?
- @areas[type].show 0, =>
- @ide.layoutManager.refreshHeights()
- if complete?
- complete()
- @$currentArea = @areas[type]
- else
- # Deprecated system
- switch type
- when 'iframe'
- if(@$iframe.attr('id')!=@$currentArea.attr('id'))
- @$iframe.show()
- @$currentArea.hide()
- @$currentArea = @$iframe
- break
- when 'loading'
- if(@$loading.attr('id')!=@$currentArea.attr('id'))
- @$currentArea.hide()
- @$loading.show()
- @$currentArea = @$loading
- break
-
diff --git a/services/web/public/coffee/ide/SavingAreaManager.coffee b/services/web/public/coffee/ide/SavingAreaManager.coffee
deleted file mode 100644
index 285995a3ef..0000000000
--- a/services/web/public/coffee/ide/SavingAreaManager.coffee
+++ /dev/null
@@ -1,32 +0,0 @@
-define [
-], () ->
- class SavingAreaManager
- $el: $('#saving-area')
-
- constructor: (@ide) ->
- @unsavedSeconds = 0
- setInterval () =>
- @pollSavedStatus()
- , 1000
-
- $(window).bind 'beforeunload', () =>
- @warnAboutUnsavedChanges()
-
- pollSavedStatus: () ->
- doc = @ide.editor.document
- return if !doc?
- saved = doc.pollSavedStatus()
- if saved
- @unsavedSeconds = 0
- else
- @unsavedSeconds += 1
-
- if @unsavedSeconds >= 4
- $("#savingProblems").text("Saving... (#{@unsavedSeconds} seconds of unsaved changes)")
- $("#savingProblems").show()
- else
- $("#savingProblems").hide()
-
- warnAboutUnsavedChanges: () ->
- if @ide.editor.hasUnsavedChanges()
- return "You have unsaved changes. If you leave now they will not be saved."
diff --git a/services/web/public/coffee/ide/SideBarManager.coffee b/services/web/public/coffee/ide/SideBarManager.coffee
deleted file mode 100644
index 17674fcdf1..0000000000
--- a/services/web/public/coffee/ide/SideBarManager.coffee
+++ /dev/null
@@ -1,38 +0,0 @@
-define () ->
- class SideBarManager
- constructor: (@ide, @el) ->
-
- addLink: (options) ->
- @elements ||= {}
- @elements[options.identifier] = options.element
-
- if options.before and @elements[options.before]
- options.element.insertBefore @elements[options.before]
- else if options.after and @elements[options.after]
- options.element.insertAfter @elements[options.after]
- else if options.prepend
- @el.prepend options.element
- else
- @el.append options.element
-
- removeLink: (identifier) ->
- @elements ||= {}
- if @elements[identifier]?
- @elements[identifier].remove()
- delete @elements[identifier]
-
- selectLink: (identifier) ->
- @selectElement(@elements[identifier].find("li"))
-
- selectElement: (selector, callback)->
- # This method is deprecated and will eventually be replaced
- # by selectLink
- if $(selector).length
- @deselectAll()
- $(selector).addClass('selected')
-
- deselectAll: () ->
- $('.selected').removeClass('selected')
-
-
-
diff --git a/services/web/public/coffee/ide/TabManager.coffee b/services/web/public/coffee/ide/TabManager.coffee
deleted file mode 100644
index e4c1b3ca70..0000000000
--- a/services/web/public/coffee/ide/TabManager.coffee
+++ /dev/null
@@ -1,109 +0,0 @@
-define [
- "libs/mustache"
-], () ->
- class TabManager
- templates:
- tab: $("#tabTemplate").html()
- content: $("#tabContentTemplate").html()
-
- constructor: () ->
- @locked_open = false
- @locked_closed = false
- @state = "closed"
- $("#toolbar").on "mouseenter", () => @onMouseOver()
- $("#toolbar").on "mouseleave", (e) => @onMouseOut(e)
- @tabs = []
-
- addTab: (options) ->
- @tabs.push options
-
- options.show ||= options.id
- tabEl = $(Mustache.to_html @templates.tab, options)
- tabEl.find("a").attr("href", "#" + options.show)
-
- if options.content?
- contentEl = $(Mustache.to_html @templates.content, options)
- contentEl.append(options.content)
- $("#tab-content").append(contentEl)
-
- if options.active
- tabEl.addClass("active")
- contentEl?.addClass("active")
-
- if options.after?
- tabEl.insertAfter($("##{options.after}-tab-li"))
- else
- $("#tabs").append(tabEl)
-
- $("body").scrollTop(0)
- tabEl.on "shown", () =>
- $("body").scrollTop(0)
-
- options.onShown() if options.onShown?
- for other_tab in @tabs
- if other_tab.id != options.id and other_tab.active and other_tab.onHidden?
- other_tab.onHidden()
- other_tab.active = false
- options.active = true
-
- if options.lock
- @lockOpen()
- else
- @unlockOpen()
-
- if options.contract
- @contract()
-
- show: (tab) ->
- $("##{tab}-tab-li > a").tab("show")
-
- lockOpen: () ->
- @locked_open = true
- $("#toolbar").css({
- width: 180
- })
-
- unlockOpen: () ->
- @locked_open = false
-
- contract: () ->
- $("#toolbar").css({
- width: 40
- })
- # cooldown so we don't immediately reopen
- original_locked_closed = @locked_closed
- @locked_closed = true
- setTimeout () =>
- @locked_closed = original_locked_closed
- , 200
-
- onMouseOver: () ->
- if !@locked_closed and @state == "closed"
- @openMenu()
-
- onMouseOut: (e) ->
- @cancelOpen()
- if !@locked_open and @state == "open"
- @closeMenu()
-
- cancelOpen: () ->
- if @openTimeout
- clearTimeout @openTimeout
- @state = "closed"
-
- openMenu: () ->
- @openTimeout = setTimeout () =>
- @state = "open"
- $("#toolbar").animate({
- width: 180
- }, "fast")
- delete @openTimeout
- , 500
-
- closeMenu: () ->
- @state = "closed"
- $("#toolbar").animate({
- width: 40
- }, "fast")
-
-
diff --git a/services/web/public/coffee/keys/BackspaceHighjack.coffee b/services/web/public/coffee/keys/BackspaceHighjack.coffee
deleted file mode 100644
index b3fcf1cce1..0000000000
--- a/services/web/public/coffee/keys/BackspaceHighjack.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-define [
-], () ->
- rx = /INPUT|SELECT|TEXTAREA/i
-
- $(document).bind "keydown keypress", (e)->
- if e.which == 8
- # 8 == backspace
- if !rx.test(e.target.tagName) || e.target.disabled || e.target.readOnly
- e.preventDefault()
\ No newline at end of file
diff --git a/services/web/public/coffee/keys/HotkeysManager.coffee b/services/web/public/coffee/keys/HotkeysManager.coffee
deleted file mode 100644
index 529eabeb6f..0000000000
--- a/services/web/public/coffee/keys/HotkeysManager.coffee
+++ /dev/null
@@ -1,30 +0,0 @@
-define [
- "utils/Modal"
- "ace/lib/useragent"
-], (Modal) ->
- useragent = require("ace/lib/useragent")
-
- class HotKeysManager
- template: $("#hotKeysLinkTemplate").html()
-
- constructor: (@ide) ->
- @$el = $(@template)
- $("#toolbar-footer").append(@$el)
- @$el.on "click", (e) =>
- e.preventDefault()
- @showHotKeys()
-
- showHotKeys: () ->
- el = $($("#hotKeysListTemplate").html())
- if useragent.isMac
- el.find(".win").hide()
- else
- el.find(".mac").hide()
-
- new Modal
- title: "Hot keys"
- el: el
- buttons: [{
- text: "Hide"
- class: "btn-primary"
- }]
diff --git a/services/web/public/coffee/messages/MessageManager.coffee b/services/web/public/coffee/messages/MessageManager.coffee
deleted file mode 100644
index b740ee971f..0000000000
--- a/services/web/public/coffee/messages/MessageManager.coffee
+++ /dev/null
@@ -1,25 +0,0 @@
-define [
- "utils/Modal"
-], (Modal) ->
- class MessageManager
- constructor: (@ide) ->
- if @ide?
- @ide.on "afterJoinProject", (@project) =>
- @checkIfProjectHasBeenDeleted()
- @ide.socket.on "projectRenamedOrDeletedByExternalSource", @notifyUsersProjectHasBeenDeletedOrRenamed
-
- checkIfProjectHasBeenDeleted: ->
- if @project.get('deletedByExternalDataSource')
- @notifyUsersProjectHasBeenDeletedOrRenamed()
-
- notifyUsersProjectHasBeenDeletedOrRenamed: ->
- Modal.createModal
- isStatic: false
- title: "Project Renamed or Deleted"
- message: "This project has either been renamed or deleted by an external data source such as Dropbox. We don't want to delete your data on ShareLaTeX, so this project still contains your history and collaborators. If the project has been renamed please look in your project list for a new project under the new name."
- buttons: [{
- text : "Ok",
- class : "btn",
- }]
- @ide.mainAreaManager.addArea identifier:"project_deleted", element:$('#projectDeleted')
- @ide.mainAreaManager.change "project_deleted"
diff --git a/services/web/public/coffee/models/Doc.coffee b/services/web/public/coffee/models/Doc.coffee
deleted file mode 100644
index a80b3b6acf..0000000000
--- a/services/web/public/coffee/models/Doc.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-define [
- "libs/backbone"
-], () ->
- Doc = Backbone.Model.extend
- initialize: () ->
- @set("type", "doc")
-
- parse: (rawAttributes) ->
- attributes =
- id: rawAttributes._id
- name: rawAttributes.name
- deleted: !!rawAttributes.deleted
diff --git a/services/web/public/coffee/models/File.coffee b/services/web/public/coffee/models/File.coffee
deleted file mode 100644
index d4fc8b006f..0000000000
--- a/services/web/public/coffee/models/File.coffee
+++ /dev/null
@@ -1,25 +0,0 @@
-define [
- "libs/backbone"
-], () ->
- File = Backbone.Model.extend
- initialize: () ->
- @set("type", "file")
-
-
- previewUrl: () ->
- extension = @get("name").split(".").pop()?.toLowerCase()
- needsConverting = (["eps", "pdf"].indexOf(extension) != -1)
- queryString = if needsConverting then "?format=png" else ""
- url = "#{@downloadUrl()}#{queryString}"
- return url
-
- downloadUrl: ->
- url = "/project/#{userSettings.project_id}/file/#{@id}"
- return url
-
-
- parse: (rawAttributes) ->
- attributes =
- id: rawAttributes._id
- name: rawAttributes.name
-
diff --git a/services/web/public/coffee/models/Folder.coffee b/services/web/public/coffee/models/Folder.coffee
deleted file mode 100644
index d9c3df13ec..0000000000
--- a/services/web/public/coffee/models/Folder.coffee
+++ /dev/null
@@ -1,36 +0,0 @@
-define [
- "models/FolderChildren"
- "models/File"
- "models/Doc"
- "libs/backbone"
-], (FolderChildren, File, Doc) ->
- Folder = Backbone.Model.extend
- initialize: () ->
- if !@get("children")?
- @set("children", new FolderChildren())
-
- @get("children").parentFolder = @
- @on "change:children", () =>
- @get("children").parentFolder = @
-
- @set("type", "folder")
-
- parse: (rawAttributes) ->
- attributes =
- id: rawAttributes._id
- name: rawAttributes.name
- children = []
- for childFolder in rawAttributes.folders or []
- children.push new Folder(childFolder, parse: true)
- for file in rawAttributes.fileRefs or []
- children.push new File(file, parse: true)
- for doc in rawAttributes.docs or []
- children.push new Doc(doc, parse: true)
- attributes.children = new FolderChildren(children)
- return attributes
-
- getParentFolderIds: () ->
- ids = @collection?.parentFolder?.getParentFolderIds() or []
- ids.push @id
- return ids
-
diff --git a/services/web/public/coffee/models/FolderChildren.coffee b/services/web/public/coffee/models/FolderChildren.coffee
deleted file mode 100644
index 082e9d2dbe..0000000000
--- a/services/web/public/coffee/models/FolderChildren.coffee
+++ /dev/null
@@ -1,29 +0,0 @@
-define [
- "models/Folder"
- "libs/backbone"
-], (Folder) ->
- FolderChildren = Backbone.Collection.extend
- comparator: (a,b) ->
- # Group folders at the top and then sort by name
- Folder = require("models/Folder") # recursive includes
-
- aName = a.get("name")
- if a instanceof Folder
- aName = "0" + aName
- else
- aName = "1" + aName
-
- bName = b.get("name")
- if b instanceof Folder
- bName = "0" + bName
- else
- bName = "1" + bName
-
- if aName < bName
- return -1
- else if aName > bName
- return 1
- else
- return 0
-
-
diff --git a/services/web/public/coffee/models/Project.coffee b/services/web/public/coffee/models/Project.coffee
deleted file mode 100644
index ab3ed089ca..0000000000
--- a/services/web/public/coffee/models/Project.coffee
+++ /dev/null
@@ -1,111 +0,0 @@
-define [
- "models/User"
- "models/ProjectMemberList"
- "models/Folder"
- "models/Doc"
- "libs/backbone"
-], (User, ProjectMemberList, Folder, Doc) ->
- Project = Backbone.Model.extend
- initialize: ->
- @on "change:ide", (project, ide) =>
- @bindToRootDocId()
- @bindToProjectName()
- @bindToPublicAccessLevel()
- @bindToCompiler()
- @bindToSpellingPreference()
- @bindToProjectDescription()
-
- parse: (rawAttributes) ->
- attributes =
- id : rawAttributes._id
- name : rawAttributes.name
- rootFolder : new Folder(rawAttributes.rootFolder[0], parse: true)
- rootDoc_id : rawAttributes.rootDoc_id
- publicAccesLevel : rawAttributes.publicAccesLevel
- features : rawAttributes.features
- compiler : rawAttributes.compiler
- spellCheckLanguage: rawAttributes.spellCheckLanguage
- dropboxEnabled : rawAttributes.dropboxEnabled
- description : rawAttributes.description
- deletedByExternalDataSource: rawAttributes.deletedByExternalDataSource
- useOt : rawAttributes.useOt
-
- attributes.members = members = new ProjectMemberList()
-
- attributes.owner = owner = User.findOrBuild rawAttributes.owner._id, rawAttributes.owner
- owner.set("privileges", "owner")
- members.add owner
-
- for rawMember in rawAttributes.members or []
- member = User.findOrBuild rawMember._id, rawMember
- members.add member
-
- for doc in rawAttributes.deletedDocs or []
- doc.deleted = true
-
- attributes.deletedDocs = new Folder({
- _id: "deleted-docs-folder"
- name: "Deleted documents"
- docs: rawAttributes.deletedDocs
- }, parse: true)
-
- return attributes
-
- bindToRootDocId: ->
- remoteChange = false
- @on "change:rootDoc_id", (project, docId) =>
- if !remoteChange
- @get("ide").socket.emit "setRootDoc", docId
- @get("ide").socket.on "rootDocUpdated", (docId) =>
- remoteChange = true
- @set("rootDoc_id", docId)
- remoteChange = false
-
- bindToProjectName: ->
- remoteChange = false
- @on "change:name", (project, name) =>
- if !remoteChange
- @get("ide").socket.emit "setProjectName", name
- @get("ide").socket.on "projectNameUpdated", (name) =>
- remoteChange = true
- @set("name", name)
- remoteChange = false
-
- bindToCompiler: ->
- remoteChange = false
- @on "change:compiler", (project, name) =>
- if !remoteChange
- @get("ide").socket.emit "setCompiler", name
- @get("ide").socket.on "compilerUpdated", (compiler) =>
- remoteChange = true
- @set("compiler", compiler)
- remoteChange = false
-
- bindToSpellingPreference: ->
- @on "change:spellCheckLanguage", (project, languageCode) =>
- @get("ide").socket.emit "setSpellCheckLanguage", languageCode
-
- bindToPublicAccessLevel: ->
- remoteChange = false
- @on "change:publicAccesLevel", (project, level) =>
- if !remoteChange
- @get("ide").socket.emit "setPublicAccessLevel", level
- @get("ide").socket.on 'publicAccessLevelUpdated', (level) =>
- remoteChange = true
- @set("publicAccesLevel", level)
- remoteChange = false
-
- bindToProjectDescription: ->
- remoteChange = false
- @on "change:description", (project, description) =>
- if !remoteChange
- @get("ide").socket.emit "updateProjectDescription", description
- @get("ide").socket.on 'projectDescriptionUpdated', (description) =>
- remoteChange = true
- @set("description", description)
- remoteChange = false
-
- getRootDocumentsList: (callback) ->
- @get("ide").socket.emit "getRootDocumentsList", (err, listOfDocs) ->
- callback(listOfDocs)
-
diff --git a/services/web/public/coffee/models/ProjectMemberList.coffee b/services/web/public/coffee/models/ProjectMemberList.coffee
deleted file mode 100644
index 3f3770256a..0000000000
--- a/services/web/public/coffee/models/ProjectMemberList.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-define [
- "models/User"
- "libs/backbone"
-], (User) ->
- ProjectMemberList = Backbone.Collection.extend
- model: User
-
- numberOfCollaborators: () ->
- collaborators = 0
- for member in @models
- if member.get("privileges") != "owner"
- collaborators += 1
- return collaborators
-
diff --git a/services/web/public/coffee/models/User.coffee b/services/web/public/coffee/models/User.coffee
deleted file mode 100644
index f83df16139..0000000000
--- a/services/web/public/coffee/models/User.coffee
+++ /dev/null
@@ -1,55 +0,0 @@
-define [
- "libs/md5"
- "libs/backbone"
-], () ->
- User = Backbone.Model.extend {
- gravatarUrl: (size = 32) ->
- email = @get("email").trim().toLowerCase()
- hash = CryptoJS.MD5(email)
- return "//www.gravatar.com/avatar/#{hash}.jpg?size=#{size}&d=mm"
-
- OWNER_HUE: 200
- hue: () ->
- if window.user.id == @get("id")
- hue = @OWNER_HUE
- else
- hash = CryptoJS.MD5(@get("id"))
- hue = parseInt(hash.toString().slice(0,8), 16) % 320
- # Avoid 20 degrees either side of the owner
- if hue > @OWNER_HUE - 20
- hue = hue + 40
- return hue
-
- name: () ->
- if window.user.id == @get("id")
- return "you"
- parts = []
- first_name = @get("first_name")
- if first_name? and first_name.length > 0
- parts.push first_name
- last_name = @get("last_name")
- if last_name? and last_name.length > 0
- parts.push last_name
- return parts.join(" ")
-
- }, {
- findOrBuild : (id, attributes) ->
- model = @find id
- if !model?
- model = @build id
- model.set model.parse attributes
- return model
-
- build: (id) ->
- model = new this(id : id)
- @loadedModel ||= {}
- @loadedModel[id] = model
- return model
-
- find: (id) ->
- @loadedModel ||= {}
- return @loadedModel[id]
-
- getAnonymousUser: () ->
- return User.findOrBuild("anonymous", { first_name: "Anonymous", email: "anon@sharelatex.com" })
- }
diff --git a/services/web/public/coffee/pdf/CompiledView.coffee b/services/web/public/coffee/pdf/CompiledView.coffee
deleted file mode 100755
index 7176e63999..0000000000
--- a/services/web/public/coffee/pdf/CompiledView.coffee
+++ /dev/null
@@ -1,233 +0,0 @@
-define [
- "pdf/PDFjsView"
- "pdf/NativePdfView"
- "libs/backbone"
- "libs/mustache"
- "libs/jquery.storage"
-], (PdfjsView, NativePdfView) ->
- LatexErrorView = Backbone.View.extend
- constructor: (@ide, @error) ->
-
- template: $('#compileLogEntryTemplate').html()
-
- events:
- "click": "onClick"
-
- render: () ->
- if @error.level == "error"
- @error.type = "error"
- @error.title = "Error in #{@error.file}"
- else if @error.level == "warning"
- @error.type = "warning"
- @error.title = "Warning in #{@error.file}"
- else
- @error.type = "info"
- @error.title = "Typesetting problem in #{@error.file}"
- if @error.line?
- @error.title += " (line #{@error.line})"
- @setElement($(Mustache.to_html(@template, @error)))
-
- onClick: (event) ->
- @ide.fileTreeManager.openDocByPath(@error.file, @error.line)
-
- PdfView = Backbone.View.extend
- templates:
- pdfPanel: $("#pdfPanelTemplate").html()
- compileSuccess: $('#compileSuccessTemplate').html()
- compileFailed: $('#compileFailedTemplate').html()
- compileError: $('#compileErrorTemplate').html()
- compileTimeout: $('#compileTimeoutTemplate').html()
- outputFileLink: $('#outputFileLinkTemplate').html()
-
- events:
- "click #recompilePdf": -> @recompilePdf()
- "click #showLog": -> @showLog()
- "click #showRawLog": -> @showRawLog()
- "click #showPdf": -> @showPdf()
- "click #downloadPdf": -> @options.manager.downloadPdf()
- "click #flatViewButton": () ->
- $.localStorage("layout.pdf", "flat")
- @options.manager.switchToFlatView(showPdf: true)
- "click #splitViewButton": ->
- $.localStorage("layout.pdf", "split")
- @options.manager.switchToSplitView()
- "click .delete-cached-files > a": -> @options.manager.deleteCachedFiles()
-
- initialize: (@options) ->
- @ide = @options.ide
- @ide.layoutManager.on "resize", => @resize()
- @ide.editor.on "resize", => @resize()
- if @ide.userSettings.pdfViewer == "native"
- PdfView = NativePdfView
- @pdfjs = false
- else
- PdfView = PdfjsView
- @pdfjs = true
- @pdfView = new PdfView(manager: @)
- @pdfView.on "dblclick", (e) => @trigger "dblclick", e
-
- render: () ->
- @setElement(@templates.pdfPanel)
- @$("#pdfAreaContent").append @pdfView.render().$el
- @showBeforeCompile()
- return this
-
- resize: () ->
- toolbarHeight = @$("#pdfToolBar").outerHeight()
- areaHeight = @$el.outerHeight()
- @$("#pdfAreaContent").height(areaHeight - toolbarHeight)
- @pdfView.onResize?()
-
- updateLog: (options) ->
- {pdfExists, logExists, compileErrors, rawLog, timedOut, systemError} = options
-
- if @errorViews?
- for errorView in @errorViews
- errorView.remove()
- @errorViews = []
-
- errorLogs = @$("#logArea").find('ul')
- errorLogs.empty()
-
- logButtonHtml = "Logs"
-
- if compileErrors?
- for error in compileErrors.errors.concat(compileErrors.warnings).concat(compileErrors.typesetting)
- errorView = new LatexErrorView(@options.manager.ide, error)
- errorView.render()
- @errorViews.push(errorView)
- # TODO: The event handlers introduced by LatexError are never freed
- errorLogs.append(errorView.el)
-
- if compileErrors.errors.length > 0
- logButtonHtml += " #{compileErrors.errors.length}"
-
- if compileErrors.warnings.length > 0
- logButtonHtml += " #{compileErrors.warnings.length}"
-
- if compileErrors.typesetting.length > 0
- logButtonHtml += " #{compileErrors.typesetting.length}"
-
- @$("#showLog").html(logButtonHtml)
-
- if timedOut
- errorLogs.prepend($(@templates.compileTimeout))
- else if systemError
- errorLogs.prepend($(@templates.compileError))
- else if !pdfExists
- errorLogs.prepend($(@templates.compileFailed))
- else if pdfExists && compileErrors.all.length == 0
- errorLogs.prepend($(@templates.compileSuccess))
-
- errorLogs.find(".js-clear-cache").on "click", () =>
- @options.manager.deleteCachedFiles()
-
- @$("#rawLogArea").find("pre").text(rawLog)
-
- setPdf: (pdfUrl) ->
- @pdfUrl = pdfUrl
- @pdfView.setPdf pdfUrl
- @$("#downloadPdf").removeAttr("disabled")
- @$("#downloadLinksButton").removeAttr("disabled")
-
- unsetPdf: () ->
- delete @pdfUrl
- @pdfView.unsetPdf()
- @$("#downloadPdf").attr("disabled", "disabled")
- @$("#downloadLinksButton").attr("disabled", "disabled")
- return @
-
- hasPdf: () -> !!@pdfUrl
-
- IGNORE_FILES: ["output.fls", "output.fdb_latexmk"]
- showOutputFileDownloadLinks: (outputFiles) ->
- @$("#downloadLinks").empty()
- for file in outputFiles
- if @IGNORE_FILES.indexOf(file.path) == -1
- if file.path.match(/^output\./)
- name = "#{file.path.replace(/^output\./, "")} file"
- else
- name = file.path
- link = $ Mustache.to_html(@templates.outputFileLink, {
- project_id: @ide.project_id
- name: name
- path: file.path
- })
- @$("#downloadLinks").append(link)
-
- afterSwitchView: ()->
- if !@pdfjs
- @pdfView.unsetPdf()
- @pdfView.hide()
- @$(".not-compiled-yet-message").show()
-
- showBeforeCompile: () ->
- @pdfView.hide()
- @$("#showPdfGroup").hide()
- @$("#showLog").attr("disabled", "disabled")
- @$("#downloadPdf").attr("disabled", "disabled")
- @$("#downloadLinksButton").attr("disabled", "disabled")
- @$(".compiling-message").hide()
- @$(".not-compiled-yet-message").show()
-
- showPdf: () ->
- @$("#logArea").hide()
- @$("#rawLogArea").hide()
- @pdfView.show()
- @$("#showPdfGroup").hide()
- @$("#showLogGroup").show()
-
- showLog: () ->
- @$("#rawLogArea").hide()
- @pdfView.hide()
- @$("#logArea").show()
- @$("#showPdfGroup").show()
- @$("#showLogGroup").hide()
-
- showRawLog: () ->
- @pdfView.hide()
- @$("#logArea").hide()
- @$("#rawLogArea").show()
- @$("#showPdfGroup").show()
- @$("#showLogGroup").hide()
-
- onCompiling: () ->
- @$(".not-compiled-yet-message").hide()
- @$(".compiling-message").show()
- @$("#recompilePdf").attr("disabled", "disabled")
-
- doneCompiling: () ->
- @$(".compiling-message").hide()
- @$("#recompilePdf").removeAttr("disabled")
- @$("#showLog").removeAttr("disabled")
-
- recompilePdf: () ->
- @options.manager.trigger "compile:pdf"
- rootDocOverride_id = null
- for line in @ide.editor.getLines()
- match = line.match /(.*)\\documentclass/
- if match and !match[1].match /%/
- rootDocOverride_id = @ide.editor.getCurrentDocId()
- @options.manager.refreshPdf {rootDocOverride_id}
-
- toggleFlatViewButton: () -> @$("#flatViewButton").button("toggle")
- toggleSplitViewButton: () -> @$("#splitViewButton").button("toggle")
-
- downloadPdf: () ->
- @options.manager.downloadPdf()
-
- delegateEvents: () ->
- Backbone.View::delegateEvents.apply(this, arguments)
- @pdfView.delegateEvents()
-
- undelegateEvents: () ->
- Backbone.View::undelegateEvents.apply(this, arguments)
- @pdfView.undelegateEvents()
-
- highlightInPdf: (args...) ->
- @pdfView.highlightInPdf?(args...)
-
- getPdfPosition: () ->
- @pdfView.getPdfPosition?()
-
-
diff --git a/services/web/public/coffee/pdf/NativePdfView.coffee b/services/web/public/coffee/pdf/NativePdfView.coffee
deleted file mode 100644
index 045d889556..0000000000
--- a/services/web/public/coffee/pdf/NativePdfView.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-define [
- "libs/backbone"
-], () ->
- NativePdfView = Backbone.View.extend
- tagName: "iframe"
-
- render: () ->
- @$el.css
- border: "none"
- width: "100%"
- height: "100%"
- return @
-
- setPdf: (url) ->
- @$el.attr "src", url
-
- unsetPdf: () ->
- @$el.removeAttr "src"
-
- hide: () -> @$el.hide()
- show: () -> @$el.show()
-
diff --git a/services/web/public/coffee/pdf/PDFjsView.coffee b/services/web/public/coffee/pdf/PDFjsView.coffee
deleted file mode 100644
index e69ede132b..0000000000
--- a/services/web/public/coffee/pdf/PDFjsView.coffee
+++ /dev/null
@@ -1,168 +0,0 @@
-define [
- "libs/pdfListView/PdfListView"
- "libs/pdfListView/TextLayerBuilder"
- "libs/pdfListView/AnnotationsLayerBuilder"
- "libs/pdfListView/HighlightsLayerBuilder"
- "text!libs/pdfListView/TextLayer.css"
- "text!libs/pdfListView/AnnotationsLayer.css"
- "text!libs/pdfListView/HighlightsLayer.css"
- "libs/backbone"
- "libs/jquery.storage"
-], (PDFListView, TextLayerBuilder, AnnotationsLayerBuilder, HighlightsLayerBuilder, textLayerCss, annotationsLayerCss, highlightsLayerCss) ->
- if PDFJS?
- PDFJS.workerSrc = "#{window.sharelatex.pdfJsWorkerPath}"
-
- style = $("")
- style.text(textLayerCss + "\n" + annotationsLayerCss + "\n" + highlightsLayerCss)
- $("body").append(style)
-
- PDFjsView = Backbone.View.extend
- template: $("#pdfjsViewerTemplate").html()
-
- events:
- "mousemove" : "onMousemove"
- "mouseout" : "onMouseout"
- "click .js-fit-height" : "fitToHeight"
- "click .js-fit-width" : "fitToWidth"
- "click .js-zoom-out" : "zoomOut"
- "click .js-zoom-in" : "zoomIn"
-
- initialize: () ->
- @ide = @options.manager.ide
-
- render: () ->
- @setElement $(@template)
- @pdfListView = new PDFListView @$(".pdfjs-list-view")[0],
- textLayerBuilder: TextLayerBuilder
- annotationsLayerBuilder: AnnotationsLayerBuilder
- highlightsLayerBuilder: HighlightsLayerBuilder
- ondblclick: (e) =>
- @trigger "dblclick", e
- #logLevel: PDFListView.Logger.DEBUG
- @pdfListView.listView.pageWidthOffset = 20
- @pdfListView.listView.pageHeightOffset = 20
- @toolbar = @$(".btn-group")
- @toolbar.hide()
- @progress_bar = @$(".progress")
- @hideProgressBar()
- return @
-
- show: () ->
- @$el.show()
- @pdfListView.onResize()
-
- hide: () -> @$el.hide()
-
- setPdf: (url) ->
- @setProgressBarTo(0)
- onProgress = (progress) =>
- @setProgressBarTo(progress.loaded/progress.total)
- @pdfListView
- .loadPdf(url, onProgress)
- .then () =>
- @hideProgressBar()
- if !@setInitialPosition
- @_initInitialPosition()
- @setInitialPosition = true
- @flashToolbar()
-
- unsetPdf: () -> @hide()
-
- _initInitialPosition: () ->
- if (scale = $.localStorage("pdf.scale"))?
- @pdfListView.setScaleMode(scale.scaleMode, scale.scale)
- else
- @pdfListView.setToFitWidth()
-
- if (position = $.localStorage("pdf.position.#{@ide.project.get("id")}"))
- @pdfListView.setPdfPosition(position)
-
- $(window).unload () =>
- $.localStorage "pdf.scale", {
- scaleMode: @pdfListView.getScaleMode()
- scale: @pdfListView.getScale()
- }
- $.localStorage "pdf.position.#{@ide.project.get("id")}", @pdfListView.getPdfPosition()
-
- setProgressBarTo: (value) ->
- @progress_bar.show()
- @progress_bar.find(".bar").css width: "#{value * 100}%"
-
- hideProgressBar: () ->
- @progress_bar.hide()
-
- fitToHeight: () ->
- @pdfListView.setToFitHeight()
-
- fitToWidth: () ->
- @pdfListView.setToFitWidth()
-
- zoomIn: () ->
- scale = @pdfListView.getScale()
- @pdfListView.setScale(scale * 1.2)
-
- zoomOut: () ->
- scale = @pdfListView.getScale()
- @pdfListView.setScale(scale / 1.2)
-
- flashToolbar: () ->
- @toolbar.show()
- setTimeout =>
- @toolbar.fadeOut "slow"
- , 1000
-
- onMousemove: (e) ->
- viewerOffset = @$el.offset()
- offsetY = e.pageY - viewerOffset.top
- offsetX = e.pageX - viewerOffset.left
- if offsetY < 70 and offsetX < 250
- @toolbar.show()
- else
- @toolbar.fadeOut "slow"
-
- onMouseout: (e) ->
- @toolbar.hide()
-
- onResize: () ->
- @pdfListView.onResize()
-
- highlightInPdf: (areas) ->
- highlights = for area in (areas or [])
- {
- page: area.page - 1
- highlight:
- left: area.h
- top: area.v
- height: area.height
- width: area.width
- }
-
- if highlights.length > 0
- first = highlights[0]
- @pdfListView.setPdfPosition({
- page: first.page
- offset:
- left: first.highlight.left
- top: first.highlight.top - 80
- }, true)
-
- @pdfListView.clearHighlights()
- @pdfListView.setHighlights(highlights, true)
-
- setTimeout () =>
- @$(".pdfjs-list-view .plv-highlights-layer > div").fadeOut "slow", () =>
- @pdfListView.clearHighlights()
- , 1000
-
- getPdfPosition: () ->
- position = @pdfListView.getPdfPosition(true)
- return if !position?
- return {
- page: position.page
- x: position.offset.left
- y: position.offset.top
- }
-
-
-
-
diff --git a/services/web/public/coffee/pdf/PdfManager.coffee b/services/web/public/coffee/pdf/PdfManager.coffee
deleted file mode 100644
index 5c2738444c..0000000000
--- a/services/web/public/coffee/pdf/PdfManager.coffee
+++ /dev/null
@@ -1,302 +0,0 @@
-define [
- "utils/Modal"
- "pdf/CompiledView"
- "pdf/SyncButtonsView"
- "libs/latex-log-parser"
- "libs/jquery.storage"
- "underscore"
- "libs/backbone"
-], (Modal, CompiledView, SyncButtonsView, LogParser) ->
- class PdfManager
- templates:
- pdfLink: $("#pdfSideBarLinkTemplate").html()
-
- constructor: (@ide) ->
- _.extend @, Backbone.Events
- @createSyncButtons()
- @createPdfPanel()
- @ide.editor.aceEditor.commands.addCommand
- name: "compile",
- bindKey: win: "Ctrl-Enter", mac: "Command-Enter"
- exec: (editor) =>
- @refreshPdf()
- readOnly: true
-
- @ide.editor.aceEditor.commands.removeCommand "replace"
-
- createPdfPanel: () ->
- @view = new CompiledView manager: @, ide: @ide
- @view.on "dblclick", (e) => @syncToCode(e)
- @view.render()
- if $.localStorage("layout.pdf") == "flat"
- @switchToFlatView()
- else if $.localStorage("layout.pdf") == "split"
- @switchToSplitView()
- else if $(window).width() < 1024
- @switchToFlatView()
- else
- @switchToSplitView()
-
- createSyncButtons: () ->
- unless @ide.userSettings.pdfViewer == "native"
- @syncButtonsView = new SyncButtonsView(ide: @ide)
- @syncButtonsView.on "click:sync-code-to-pdf", () =>
- @syncToPdf()
- @syncButtonsView.on "click:sync-pdf-to-code", () =>
- @syncToCode()
- @syncButtonsView.hide()
-
- switchToFlatView: (options = {showPdf: false}) ->
- @teardownSplitView()
- @setupFlatView()
- @view.toggleFlatViewButton()
- if options.showPdf
- @ide.sideBarView.selectLink "pdf"
- @ide.mainAreaManager.change "pdf"
- @view.resize()
-
- switchToSplitView: () ->
- @teardownFlatView()
- @setupSplitView()
- @view.toggleSplitViewButton()
- @view.resize()
- @ide.editor.setIdeToEditorPanel()
-
- setupFlatView: () ->
- @teardownFlatView()
- @ide.editor.switchToFlatView()
- pdfLink = $(@templates.pdfLink)
- @ide.sideBarView.addLink
- identifier : "pdf"
- before : "history"
- element : pdfLink
- pdfLink.on "click", (e) => @showPdfPanel()
-
- @ide.mainAreaManager.addArea
- identifier: "pdf"
- element: @view.$el
- @view.resize()
-
- @view.undelegateEvents()
- @view.delegateEvents()
-
-
- teardownFlatView: () ->
- @ide.sideBarView.removeLink("pdf")
- @ide.mainAreaManager.removeArea("pdf")
- @view.afterSwitchView()
-
- setupSplitView: () ->
- @ide.editor.switchToSplitView()
- @ide.editor.rightPanel.append(
- @view.$el
- )
- @view.$el.show()
- @view.resize()
-
- @view.undelegateEvents()
- @view.delegateEvents()
-
- @ide.editor.$splitter.append(
- @syncButtonsView?.$el
- )
-
- setTimeout(@ide.layoutManager.resizeAllSplitters, 100)
-
- teardownSplitView: () ->
- @view.afterSwitchView()
-
- showPdfPanel: () ->
- @ide.sideBarView.selectLink 'pdf'
- @ide.mainAreaManager.change 'pdf'
- @view.resize()
-
- if !@view.hasPdf()
- @refreshPdf()
-
- showRawLogPanel: () ->
- @ide.mainAreaManager.change 'rawLog'
-
- refreshPdf: (opts) ->
- if @ide.project?
- @_refreshPdfWhenProjectIsLoaded(opts)
- else
- @ide.on "afterJoinProject", () =>
- @_refreshPdfWhenProjectIsLoaded(opts)
-
- _refreshPdfWhenProjectIsLoaded: (opts = {}) ->
-
- if !@ide.project.get("rootDoc_id")?
- new Modal
- title: "No root document selected"
- message: "First you need to choose a root document via the settings menu. This tells ShareLaTeX which file to run LaTeX on."
- buttons: [{
- text: "OK",
- class: "btn-primary"
- }]
- else if !@compiling
- @view.onCompiling()
- @syncButtonsView?.hide()
- @compiling = true
- @_doCompile opts, (error, status, outputFiles) =>
- @compiling = false
- @view.doneCompiling()
- @syncButtonsView?.show()
-
- if error?
- @view.updateLog(systemError: true)
- @view.unsetPdf()
- @view.showLog()
- else if status == "timedout"
- @view.updateLog(timedOut: true)
- @view.unsetPdf()
- @view.showLog()
- else if status == "autocompile-backoff"
- @view.showBeforeCompile()
- else
- pdfExists = (status == "success")
- @fetchLogAndUpdateView(pdfExists)
-
- if pdfExists
- @view.setPdf("/project/#{@ide.project_id}/output/output.pdf?cache_bust=#{Date.now()}")
- @view.showPdf()
- else
- @view.unsetPdf()
- @view.showLog()
- @syncButtonsView?.hide()
-
- if outputFiles?
- @view.showOutputFileDownloadLinks(outputFiles)
-
- _doCompile: (opts, callback = (error, status, outputFiles) ->) ->
- url = "/project/#{@ide.project_id}/compile"
- if opts.isAutoCompile
- url += "?auto_compile=true"
- $.ajax(
- url: url
- type: "POST"
- headers:
- "X-CSRF-Token": window.csrfToken
- contentType: "application/json; charset=utf-8"
- dataType: 'json'
- data: JSON.stringify settingsOverride:
- rootDoc_id: opts.rootDocOverride_id ? null
- success: (body, status, response) ->
- callback null, body.status, body.outputFiles
- error: (error) ->
- callback error
- )
-
- fetchLogAndUpdateView: (pdfExists) ->
- $.ajax(
- url: "/project/#{@ide.project_id}/output/output.log"
- success: (body, status, response) =>
- @parseLogAndUpdateView(pdfExists, body)
- error: () =>
- @view.updateLog(pdfExists: pdfExists, logExists: false)
- )
-
- parseLogAndUpdateView: (pdfExists, log) ->
- errors = LogParser.parse(log, ignoreDuplicates: true)
- lastCompileErrors = {}
- for error in errors.all
- error.file = @_normalizeFilePath(error.file)
-
- doc_id = @ide.fileTreeManager.getDocIdOfPath(error.file)
- if doc_id?
- lastCompileErrors[doc_id] ||= []
- lastCompileErrors[doc_id].push
- row: error.line - 1
- type: if error.level == "error" then "error" else "warning"
- text: error.message
- @ide.editor.compilationErrors = lastCompileErrors
- @ide.editor.refreshCompilationErrors()
-
- @view.updateLog(pdfExists: pdfExists, logExists: true, compileErrors: errors, rawLog: log)
-
- _normalizeFilePath: (path) ->
- path = path.replace(/^compiles\/[0-9a-f]{32}\/(\.\/)?/, "")
- path = path.replace(/^\/compile\//, "")
-
- rootDoc_id = @ide.project.get("rootDoc_id")
- if rootDoc_id?
- rootDocPath = @ide.fileTreeManager.getPathOfEntityId(rootDoc_id)
- if rootDocPath?
- rootDocDir = rootDocPath.split("/").slice(0,-1).map( (part) -> part + "/" ).join("")
- path = path.replace(/^\.\//, rootDocDir)
-
- return path
-
- downloadPdf: () ->
- @ide.mainAreaManager.setIframeSrc "/project/#{@ide.project_id}/output/output.pdf?popupDownload=true"
-
- deleteCachedFiles: () ->
- modal = new Modal
- title: "Clear cache?"
- message: "This will clear all hidden LaTeX files like .aux, .bbl, etc, from our compile server. You generally don't need to do this unless you're having trouble with references. Your project files will not be deleted or changed."
- buttons: [{
- text: "Cancel"
- }, {
- text: "Clear from cache",
- class: "btn-primary",
- close: false
- callback: ($button) =>
- $button.text("Clearing...")
- $button.prop("disabled", true)
- $.ajax({
- url: "/project/#{@ide.project_id}/output"
- type: "DELETE"
- headers:
- "X-CSRF-Token": window.csrfToken
- complete: () -> modal.remove()
- })
-
- }]
-
- syncToCode: (e) ->
- if !e?
- e = @view.getPdfPosition()
- return if !e?
- # It's not clear exactly where we should sync to if it was directly
- # clicked on, but a little bit down from the very top seems best.
- e.y = e.y + 80
-
- $.ajax {
- url: "/project/#{@ide.project_id}/sync/pdf"
- data:
- page: e.page + 1
- h: e.x.toFixed(2)
- v: e.y.toFixed(2)
- type: "GET"
- success: (response) =>
- data = JSON.parse(response)
- if data.code and data.code.length > 0
- file = data.code[0].file
- line = data.code[0].line
- @ide.fileTreeManager.openDocByPath(file, line)
- }
-
- syncToPdf: () ->
- entity_id = @ide.editor.getCurrentDocId()
- file = @ide.fileTreeManager.getPathOfEntityId(entity_id)
-
- # If the root file is folder/main.tex, then synctex sees the
- # path as folder/./main.tex
- rootFolderPath = @ide.fileTreeManager.getRootFolderPath()
- if rootFolderPath != ""
- file = file.replace(RegExp("^#{rootFolderPath}"), "#{rootFolderPath}/.")
-
- line = @ide.editor.getCurrentLine()
- column = @ide.editor.getCurrentColumn()
-
- $.ajax {
- url: "/project/#{@ide.project_id}/sync/code"
- data:
- file: file
- line: line + 1
- column: column
- type: "GET"
- success: (response) =>
- data = JSON.parse(response)
- @view.highlightInPdf(data.pdf or [])
- }
diff --git a/services/web/public/coffee/pdf/SyncButtonsView.coffee b/services/web/public/coffee/pdf/SyncButtonsView.coffee
deleted file mode 100644
index 0f8de8690f..0000000000
--- a/services/web/public/coffee/pdf/SyncButtonsView.coffee
+++ /dev/null
@@ -1,67 +0,0 @@
-define [
- "libs/backbone"
- "libs/mustache"
-], () ->
- SyncButtonsView = Backbone.View.extend
- template: $("#syncButtonsTemplate").html()
-
- events:
- "click .sync-code-to-pdf": () ->
- ga('send', 'event', 'editor-interaction', 'sync-code-to-pdf')
- @trigger "click:sync-code-to-pdf"
- "click .sync-pdf-to-code": () ->
- ga('send', 'event', 'editor-interaction', 'sync-pdf-to-code')
- @trigger "click:sync-pdf-to-code"
-
- initialize: (options) ->
- @render()
- @ide = options.ide
- @ide.editor.on "resize", => @repositionLeft()
-
- render: () ->
- @setElement($(@template))
-
- ### These keep screwing up in the UI :(
- @$(".sync-code-to-pdf").tooltip({
- title: "Go to code location in the output"
- animate: false
- placement: "top"
- trigger: "hover"
- delay:
- show: 800
- hide: 0
- })
- @$(".sync-pdf-to-code").tooltip({
- html: true
- title: "Go to output location in the code
(Or double click the output)"
- animate: false
- placement: "bottom"
- trigger: "hover"
- delay:
- show: 800
- hide: 0
- })
- ###
- return @
-
- hide: () ->
- @hidden = true
- @$el.hide()
-
- show: () ->
- @hidden = false
- @showIfNotHiddenAndOpen()
-
- showIfNotHiddenAndOpen: () ->
- state = @ide.editor.$splitter.layout().readState()
- if !@hidden and !state.east?.initClosed
- @$el.show()
- else
- @$el.hide()
-
- repositionLeft: () ->
- state = @ide.editor.$splitter.layout().readState()
- if state.east?
- @$el.css({right: state.east.size - 8})
- @showIfNotHiddenAndOpen();
-
diff --git a/services/web/public/coffee/project-members/ProjectMembersManager.coffee b/services/web/public/coffee/project-members/ProjectMembersManager.coffee
deleted file mode 100644
index a93b8c09e9..0000000000
--- a/services/web/public/coffee/project-members/ProjectMembersManager.coffee
+++ /dev/null
@@ -1,366 +0,0 @@
-define [
- "models/User"
- "models/ProjectMemberList"
- "account/AccountManager"
- "utils/Modal"
- "moment"
- "libs/backbone"
- "libs/mustache"
-], (User, ProjectMemberList, AccountManager, Modal, moment) ->
- INFINITE_COLLABORATORS = -1
-
- class ProjectMembersManager
- templates:
- userPanel: $("#userPanelTemplate").html()
-
- constructor: (@ide, options) ->
- options || = {}
- setupArea = _.once =>
- @ide.tabManager.addTab
- id : "collaborators"
- name: "Share"
- content : $(@templates.userPanel)
- lock: true
- onShown: () =>
- @publishProjectView?.refreshPublishStatus()
-
- setupPublish = _.once =>
- if @ide.security? and @ide.security.permissionsLevel == "owner"
- @publishProjectView = new PublishProjectView
- ide: @ide
- el: $("#publishProject")
- @publishProjectView.render()
-
- setupArea()
- if @ide?
- @ide.on "afterJoinProject", (project) =>
- @project = project
- if !@view?
- @members = @project.get("members")
- @view = new ProjectMemberListView
- ide: @ide
- collection: @members
- el: $('#projectMembersList')
- manager: this
-
- allowedCollaborators = @project.get("features").collaborators
- allowedMembers = allowedCollaborators + 1 # include owner
- if @members.length > allowedMembers and allowedCollaborators != INFINITE_COLLABORATORS
- AccountManager.showUpgradeDialog @ide,
- message: "This project has too many collaborators for your plan. Please upgrade your account or remove some collaborators"
-
- if @ide.security? and @ide.security.permissionsLevel == "owner"
- @view.options.showAdminControls = true
- else
- @view.options.showAdminControls = false
- @view.render()
-
- if @ide.project.get("owner") == @ide.user and @ide.user.get("id") != "openUser"
- setupPublish()
-
- if @ide.project.get("owner") == @ide.user or @ide.project.get("publicAccesLevel") != "private"
- if !@socialView?
- @socialView = new SocialSharingView
- ide: @ide
- el: $("#socialSharing")
- @socialView.render()
- else
- $("#socialSharing").hide()
-
- if @ide.socket?
- @ide.socket.on "userRemovedFromProject", (userId) =>
- @afterMemberRemoved(userId)
- @ide.socket.on "userAddedToProject", (user, privileges) =>
- @afterMemberAdded(user, privileges)
-
- removeMember: (member) ->
- @ide.socket.emit "removeUserFromProject", member.id
-
- addMember: (email, privileges) ->
- console.log "Adding member", email
- @ide.socket.emit "addUserToProject", email, privileges, (error, added) =>
- if error?
- @ide.showGenericServerErrorMessage()
- return
- if !added
- console.log "got response", error, added
- ga('send', 'event', 'subscription-funnel', 'askToUpgrade', "projectMemebrs")
- AccountManager.askToUpgrade @ide,
- why: "to add additional collaborators"
- onUpgrade: () =>
- ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "projectMemebrs")
-
- afterMemberRemoved: (memberId) ->
- for member in @members.models
- if member.id == memberId
- toRemove = member
- @members.remove(toRemove) if toRemove?
-
- afterMemberAdded: (member, privileges) ->
- @members.add new User
- email : member.email
- id : member._id
- privileges : privileges
-
- ProjectMemberListView = Backbone.View.extend
- template : $("#projectMemberListTemplate").html()
-
- events:
- "click .addUser" : "addMember"
-
- initialize: ->
- @itemViews = {}
- @options.showAdminControls ||= false
- @collection.on "add", (model) =>
- @addItem model
- @collection.on "reset", (collection) =>
- @addItem model for model in collection.models
- @collection.on "remove", (model) =>
- @removeItem model
-
- render: ->
- $(@el).html Mustache.to_html(@template, showAdminControls: @options.showAdminControls)
- @addItem model for model in @collection.models
- return this
-
- addItem: (model) ->
- view = new ProjectMemberListItemView
- model : model
- showRemove : @options.showAdminControls and model.get("privileges") != "owner"
- manager: @options.manager
- @itemViews[model.id] = view
- @$("tbody").append view.$el
-
- removeItem: (model) ->
- if @itemViews[model.id]?
- @itemViews[model.id].remove()
- delete @itemViews[model.id]
-
- addMember: (e) ->
- e.preventDefault()
- email = @$(".addUserForm .email").val()
- privileges= @$(".addUserForm .privileges").val()
- if email.indexOf('@') == -1
- @options.ide.showErrorModal("Invalid Email", "#{email} is not a valid email address")
- else
- @options.manager.addMember email, privileges
- @$(".addUserForm .email").val("")
-
-
- ProjectMemberListItemView = Backbone.View.extend
- template : $("#projectMemberListItemTemplate").html()
-
- events:
- "click .removeUser": "removeMember"
-
- initialize: -> @render()
-
- render: ->
- @setElement $(Mustache.to_html(@template, @modelView()))
- return this
-
- modelView: ->
- modelView = @model.toJSON()
- modelView.privileges = {
- readOnly : "Read Only"
- readAndWrite : "Read and Write"
- owner : "Owner"
- }[modelView.privileges]
- modelView.showRemove = @options.showRemove
- return modelView
-
- removeMember: (e) ->
- e.preventDefault()
- @options.manager.removeMember(@model)
-
- PublishProjectView = Backbone.View.extend
- template: $("#publishProjectTemplate").html()
-
- events:
- "click #publishProjectAsTemplate": "publishProjectAsTemplate"
- "click #republishProjectAsTemplate": "publishProjectAsTemplate"
- "click #unPublishProjectAsTemplate": "unPublishProjectAsTemplate"
- "blur #projectDescription" : "updateDescription"
-
- initialize: () ->
- @ide = @options.ide
- @model = @ide.project
- @render()
-
- render: ->
- viewModel =
- description: @model.get("description")
- canonicalUrl: @model.get("template.canonicalUrl")
- isPublished: @model.get("template.isPublished")
- publishedDate: moment(@model.get("template.publishedDate")).format("Do MMM YYYY, h:mm a")
-
- @$el.html $(Mustache.to_html(@template, viewModel))
- @publishedArea = $('.show-when-published')
- @unpublishedArea = $('.show-when-unpublished')
- $('#problemWithPublishingArea').hide()
- $('#publishWorkingArea').hide()
-
- refreshView: () ->
- if @model.get("template.isPublished")
- @$("a#templateLink").attr("href", @model.get("template.canonicalUrl"))
- @publishedArea.show()
- else
- @unpublishedArea.show()
-
- refreshPublishStatus: ->
- @showWorking()
- @ide.socket.emit "getPublishedDetails", @ide.user.get("id"), (err, details)=>
- @hideWorking()
- if err?
- return @showError()
-
- @model.set("template.isPublished", details.exists)
- if details.exists
- @model.set("template.canonicalUrl", details.canonicalUrl)
- @model.set("template.publishedDate", details.publishedDate)
-
- @refreshView()
-
- showError: ->
- @publishedArea.hide()
- @unpublishedArea.hide()
- $('#problemWithPublishingArea').show()
-
- showWorking: ->
- @publishedArea.hide()
- @unpublishedArea.hide()
- $('#publishWorkingArea').show()
-
- hideWorking: ->
- $('#publishWorkingArea').hide()
-
- publishProjectAsTemplate: ->
- @showWorking()
- @unpublishedArea.hide()
- @publishedArea.hide()
- @ide.socket.emit "publishProjectAsTemplate", @ide.user.get("id"), (err)=>
- @hideWorking()
- if err?
- @showError()
- else
- @refreshPublishStatus()
-
- unPublishProjectAsTemplate: ->
- @showWorking()
- @publishedArea.hide()
- @ide.socket.emit "unPublishProjectAsTemplate", @ide.user.get("id"), (err)=>
- @hideWorking()
- if err?
- @showError()
- else
- @refreshPublishStatus()
-
- updateDescription: ->
- newDescription = $('#projectDescription').val()
- @model.set("description", newDescription)
-
- SocialSharingView = Backbone.View.extend
- template: $("#socialSharingTemplate").html()
-
- events:
- "click .btn-facebook": "postToFacebook"
- "click .btn-twitter": "postToTwitter"
- "click .btn-google-plus": "postToGoogle"
- "click .btn-url": "shareUrl"
-
- initialize: () ->
- @ide = @options.ide
-
- url: (medium) ->
- "#{window.sharelatex.siteUrl}/project/#{@ide.project.get("id")}" +
- "?r=#{@ide.user.get("referal_id")}&rs=ps&rm=#{medium}" # Referal source = public share
-
- render: ->
- $(@el).html $(@template)
-
- postToFacebook: () ->
- @ensurePublic (error, success) =>
- if success
-
- url = "https://www.facebook.com/dialog/feed?link=#{encodeURIComponent(@url("fb"))}&" +
- "app_id=148710621956179&" +
- "picture=#{window.sharelatex.siteUrl}/brand/logo/logo-128.png&" +
- "name=#{@ide.project.get("name")}&" +
- "caption=My LaTeX project (#{@ide.project.get("name")}) is available online on ShareLaTeX&" +
- "redirect_uri=#{window.sharelatex.siteUrl}&" +
- "display=popup"
- ga('send', 'event', 'editor-interaction', 'project-shared', "facebook")
- window.open(
- url
- ""
- 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600'
- )
-
- postToTwitter: () ->
- @ensurePublic (error, success) =>
- if success
- ga('send', 'event', 'editor-interaction', 'project-shared', "twitter")
- window.open(
- "https://www.twitter.com/share/?text=Check out my online LaTeX Project: #{@ide.project.get("name")}&url=#{encodeURIComponent(@url("t"))}"
- ""
- 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600'
- )
-
- postToGoogle: () ->
- @ensurePublic (error, success) =>
- if success
- ga('send', 'event', 'editor-interaction', 'project-shared', "google-plus")
- window.open(
- "https://plus.google.com/share?url=#{encodeURIComponent(@url("gp"))}"
- ""
- 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600'
- )
-
- shareUrl: () ->
- @ensurePublic (error, success) =>
- if success
- ga('send', 'event', 'editor-interaction', 'project-shared', "url")
- Modal.createModal
- el: $(
- "You can share you project with your friends and colleagues via this URL:
" +
- "#{@url("d")}
"
- )
- title: "Share your project"
- buttons: [
- text: "OK"
- class: "btn btn-primary"
- ]
-
- ensurePublic: (callback = (error, success) ->) ->
- accessLevel = @ide.project.get("publicAccesLevel")
- if accessLevel == "private"
- Modal.createModal
- title: "Make project public?"
- message: "Your project needs to be public before you can share it. This means anyone with the URL will be able to access it. Would you like to make your project public?"
- buttons: [{
- text: "Cancel"
- class: "btn"
- callback: () =>
- callback null, false
- }, {
- text: "Yes, make publicly readable"
- class: "btn btn-primary"
- callback: () =>
- @ide.project.set("publicAccesLevel", "readOnly")
- callback null, true
- }, {
- text: "Yes, make publicly editable"
- class: "btn btn-primary"
- callback: () =>
- @ide.project.set("publicAccesLevel", "readAndWrite")
- callback null, true
- }]
- else
- callback null, true
-
- return {
- ProjectMembersManager : ProjectMembersManager
- ProjectMemberList : ProjectMemberList
- ProjectMemberListView : ProjectMemberListView
- }
-
diff --git a/services/web/public/coffee/search/SearchManager.coffee b/services/web/public/coffee/search/SearchManager.coffee
deleted file mode 100644
index fc44cfaf26..0000000000
--- a/services/web/public/coffee/search/SearchManager.coffee
+++ /dev/null
@@ -1,18 +0,0 @@
-define [
- 'search/searchbox'
-], (searchbox) ->
- class SearchManager
- constructor: (@ide) ->
- @ide.editor.aceEditor.commands.addCommand
- name: "find",
- bindKey: win: "Ctrl-F", mac: "Command-F"
- exec: (editor) ->
- searchbox.Search(editor)
- readOnly: true
-
- @ide.editor.on "showCommandLine", (editor, arg) =>
- if arg == "/"
- searchbox.Search(editor)
-
- @ide.editor.aceEditor.commands.removeCommand "replace"
-
diff --git a/services/web/public/coffee/settings/DropboxSettingsManager.coffee b/services/web/public/coffee/settings/DropboxSettingsManager.coffee
deleted file mode 100644
index b4fd801bd3..0000000000
--- a/services/web/public/coffee/settings/DropboxSettingsManager.coffee
+++ /dev/null
@@ -1,55 +0,0 @@
-define [
- "utils/Modal"
- "account/AccountManager"
- "underscore"
-], (Modal, accountManager, _) ->
- _.templateSettings = interpolate : /\{\{(.+?)\}\}/g
- POLLING_INTERVAL = 15
- ONE_MIN_MILI = 1000*60
- USER_LINKED_TEMPLATE = _.template($('#userLinkedToDropboxTemplate').html())
-
- class DropboxSettingsManger
- constructor: (@ide)->
-
- setup = _.once =>
- @tab = $("#dropboxProjectSettings")
- tabLink = $('#manageDropboxSettiingsTabLink').on 'click', (event)=>
- @tab.empty()
- if !@ide.isAllowedToDoIt "owner"
- else if !@project.get('features').dropbox
- ga('send', 'event', 'subscription-funnel', 'askToUpgrade', "dropbox")
- accountManager.askToUpgrade @ide,
- onUpgrade: =>
- @checkIfUserIsLinkedToDropbox()
- ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "dropbox")
- else
- @checkIfUserIsLinkedToDropbox()
-
- @ide.on "afterJoinProject", (project) =>
- @project = project
- setup()
-
- checkIfUserIsLinkedToDropbox : =>
- @ide.socket.emit "getUserDropboxLinkStatus", @project.get("owner").id, (err, status)=>
- if status.registered
- @buildLinkedView()
- else
- @buildNonLinkedView()
-
- buildLinkedView: ->
- self = @
- run = ->
- @ide.socket.emit "getLastTimePollHappned", (err, lastTimePollHappened)=>
- milisecondsSinceLastPoll = new Date().getTime() - lastTimePollHappened
- roundedMinsSinceLastPoll = Math.round(milisecondsSinceLastPoll / ONE_MIN_MILI)
-
- minsTillNextPoll = POLLING_INTERVAL - roundedMinsSinceLastPoll
- percentageLeftTillNextPoll = 100 - ((roundedMinsSinceLastPoll / POLLING_INTERVAL) * 100)
-
- html = $(USER_LINKED_TEMPLATE(minsTillNextPoll:minsTillNextPoll, percentageLeftTillNextPoll:percentageLeftTillNextPoll))
- self.tab.empty()
- self.tab.append html
- setTimeout run, ONE_MIN_MILI
- run()
- buildNonLinkedView: ->
- @tab.append $('#userNotLinkedToDropboxTemplate').html()
diff --git a/services/web/public/coffee/settings/SettingsManager.coffee b/services/web/public/coffee/settings/SettingsManager.coffee
deleted file mode 100644
index 47791833de..0000000000
--- a/services/web/public/coffee/settings/SettingsManager.coffee
+++ /dev/null
@@ -1,219 +0,0 @@
-define [
- "utils/Modal"
- "settings/DropboxSettingsManager"
-], (Modal, DropboxSettingsManager) ->
- class SettingsManager
- templates:
- settingsPanel: $("#settingsPanelTemplate").html()
-
- constructor: (@ide, options) ->
- options || = {}
- setup = _.once =>
- @bindToProjectName()
- @bindToPublicAccessLevel()
- @bindToCompiler()
- @bindToRootDocument()
- @bindToSpellingPreference()
-
- new DropboxSettingsManager @ide
-
- @setFontSize()
-
- if @ide?
- @ide.on "afterJoinProject", (project) =>
- @project = project
- setup()
-
- settingsPanel = $(@templates.settingsPanel)
- @ide.tabManager.addTab
- id: "settings"
- name: "Settings"
- content: settingsPanel
- lock: true
-
- $('#DownloadZip').click (event)=>
- event.preventDefault()
- @ide.mainAreaManager.setIframeSrc "/project/#{@ide.project_id}/download/zip"
-
- $("#deleteProject").click (event)=>
- event.preventDefault()
- self = @
- deleteProject = =>
- $.ajax
- url: "/Project/#{@ide.project_id}",
- type: 'DELETE'
- data:
- _csrf: window.csrfToken
- success: ->
- window.location = '/'
- modalOptions =
- templateId:'deleteEntityModal'
- isStatic: false
- title:'Delete Project'
- message: "Are you sure you want to delete this project?"
- buttons: [{
- text : "Cancel",
- class : "btn",
- }, {
- text : "Delete Forever",
- class : "btn-danger confirm",
- callback : deleteProject
- }]
- Modal.createModal modalOptions
-
- $('.cloneProject').click (event)=>
- event.stopPropagation()
- event.preventDefault()
- goToRegPage = ->
- window.location = "/register"
-
- modalOptions =
- isStatic: false
- title:'Registration Required'
- message: "You need to register to clone a project"
- buttons: [{
- text : "Cancel",
- class : "btn",
- }, {
- text : "Register Now",
- class : "btn-success confirm",
- callback : goToRegPage
- }]
-
- user = @project.get("ide").user
- if user.id == "openUser"
- return Modal.createModal modalOptions
-
- $modal = $('#cloneProjectModal')
- $confirm = $('#confirmCloneProject')
- $modal.modal({backdrop:true, show:true, keyboard:true})
- $modal.find('input').val('').focus()
- self = @
- $confirm.click (e)->
- $confirm.attr("disabled", true)
- $confirm.text("Cloning...")
- projectName = $modal.find('input').val()
- $.ajax
-
- url: "/project/#{self.ide.project_id}/clone"
- type:'POST'
- data:
- projectName: projectName
- _csrf: window.csrfToken
- success: (data)->
- if data.redir?
- window.location = data.redir
- else if data.project_id?
- window.location = '/project/'+data.project_id
- $modal.on 'hide', ->
- $confirm.off 'click'
- $modal.find('.cancel').click (e)->
- $modal.modal('hide')
-
- setFontSize: () ->
- @fontSizeCss = $("")
- @fontSizeCss.text """
- .ace_editor, .ace_content {
- font-size: #{window.userSettings.fontSize}px;
- }
- """
- $(document.body).append(@fontSizeCss)
-
- bindToProjectName: () ->
- @project.on "change:name", (project, newName) ->
- $element = $('.projectName')
- $element.text(newName)
- window.document.title = newName
- $("input.projectName").val(newName)
-
- $("input.projectName").on "change", (e)=>
- # Check if event was triggered by the user, re:
- # http://stackoverflow.com/questions/6692031/check-if-event-is-triggered-by-a-human
- if e.originalEvent?
- if @ide.isAllowedToDoIt "readAndWrite"
- newName = e.target.value?.trim()
- $("input.projectName").val(newName)
- @project.set("name", newName)
-
-
- bindToCompiler: ->
- $('select#compilers').val(@project.get("compiler"))
-
- $('select#compilers').change (e)=>
- if @ide.isAllowedToDoIt "readAndWrite"
- @project.set("compiler", e.target.value)
-
-
-
- bindToSpellingPreference: ->
-
- $('select#spellCheckLanguageSelection').on "change", (e)=>
- languageCode = e.target.value
- @project.set("spellCheckLanguage", languageCode)
-
-
- bindToRootDocument: () ->
- $('#rootDocList').change (event)=>
- docId = $(event.target).val()
- @project.set("rootDoc_id", docId)
-
- # Repopulate the root document list when the settings page is shown. Updating
- # it in real time is just a little too complicated
- do refreshDocList = =>
- $docList = $('select#rootDocList')
- $docList.empty()
- @project.getRootDocumentsList (listOfDocs) =>
- template = _.template($('#rootDocListEntity').html())
- _.each listOfDocs, (doc)=>
- option = $(template(name:doc.path))
- option.attr('value', doc._id)
- $docList.append(option)
- hasRootDoc = _.find listOfDocs, (doc)=>
- if doc._id == @project.get("rootDoc_id")
- return true
- if hasRootDoc
- $docList.val(@project.get("rootDoc_id"))
- else
- option = $(template(name:"No Root Document Selected!"))
- option.attr('value', 'blank')
- $docList.append(option)
- $docList.val('blank')
-
- $('#settings-tab-li').on "click", refreshDocList
-
- bindToPublicAccessLevel: () ->
- $('select#publicAccessLevel').val(@project.get("publicAccesLevel"))
-
- @project.on "change:publicAccesLevel", (project, level) ->
- $('select#publicAccessLevel').val(level)
-
- $("select#publicAccessLevel").on "change", (event)=>
- newSetting = event.target.value
- cancelChange = =>
- @project.set("publicAccesLevel", "private")
- doChange = () =>
- @project.set("publicAccesLevel", newSetting)
- modalOptions =
- buttons: [{
- text : "Cancel",
- class : "btn",
- callback : cancelChange
- },{
- text : "OK",
- class : "btn-danger confirm",
- callback : doChange
- }]
-
- if newSetting == 'readOnly'
- modalOptions.title = 'Make Project Public - Read Only'
- modalOptions.message = 'Are you sure you want make this project public to the world? Google and search engines will be able to see it. Public users will not be able to edit the project'
- else if newSetting == 'readAndWrite'
- modalOptions.title = 'Make Project Public - Read and Write'
- modalOptions.message = 'Are you sure you want make this project public to the world? Google and search engines will be able to see it. Public users will be able to write and modify the project'
- if newSetting == 'private'
- modalOptions.title = 'Make Project Private'
- modalOptions.message = 'Are you sure you want make this project private? Only registered users who are given permission below will be able to view the project'
-
- Modal.createModal modalOptions
-
-
diff --git a/services/web/public/coffee/spelling/HighlightedWordManager.coffee b/services/web/public/coffee/spelling/HighlightedWordManager.coffee
deleted file mode 100644
index 1ae058960b..0000000000
--- a/services/web/public/coffee/spelling/HighlightedWordManager.coffee
+++ /dev/null
@@ -1,146 +0,0 @@
-define () ->
- class Highlight
- constructor: (options) ->
- @row = options.row
- @column = options.column
- @word = options.word
- @suggestions = options.suggestions
-
- class HighlightedWordManager
- constructor: (@ide) ->
- @highlights = rows: []
-
- addHighlight: (highlight) ->
- unless highlight instanceof Highlight
- highlight = new Highlight(highlight)
- highlight.markerId = @ide.editor.addMarker {
- row: highlight.row
- column: highlight.column
- length: highlight.word.length
- }, "sharelatex-spelling-highlight"
- @highlights.rows[highlight.row] ||= []
- @highlights.rows[highlight.row].push highlight
-
- removeHighlight: (highlight) ->
- @ide.editor.removeMarker(highlight.markerId)
- for h, i in @highlights.rows[highlight.row]
- if h == highlight
- @highlights.rows[highlight.row].splice(i, 1)
-
- removeWord: (word) ->
- toRemove = []
- for row in @highlights.rows
- for highlight in (row || [])
- if highlight.word == word
- toRemove.push(highlight)
- for highlight in toRemove
- @removeHighlight highlight
-
- moveHighlight: (highlight, position) ->
- @removeHighlight highlight
- highlight.row = position.row
- highlight.column = position.column
- @addHighlight highlight
-
- clearRows: (from, to) ->
- from ||= 0
- to ||= @highlights.rows.length - 1
- for row in @highlights.rows.slice(from, to + 1)
- for highlight in (row || []).slice(0)
- @removeHighlight highlight
-
- insertRows: (offset, number) ->
- # rows are inserted after offset. i.e. offset row is not modified
- affectedHighlights = []
- for row in @highlights.rows.slice(offset)
- affectedHighlights.push(highlight) for highlight in (row || [])
- for highlight in affectedHighlights
- @moveHighlight highlight,
- row: highlight.row + number
- column: highlight.column
-
- removeRows: (offset, number) ->
- # offset is the first row to delete
- affectedHighlights = []
- for row in @highlights.rows.slice(offset)
- affectedHighlights.push(highlight) for highlight in (row || [])
- for highlight in affectedHighlights
- if highlight.row >= offset + number
- @moveHighlight highlight,
- row: highlight.row - number
- column: highlight.column
- else
- @removeHighlight highlight
-
- findHighlightWithinRange: (range) ->
- rows = @highlights.rows.slice(range.start.row, range.end.row + 1)
- for row in rows
- for highlight in (row || [])
- if @_doesHighlightOverlapRange(highlight, range.start, range.end)
- return highlight
- return null
-
- applyChange: (change) ->
- start = change.range.start
- end = change.range.end
- if change.action == "insertText"
- if start.row != end.row
- rowsAdded = end.row - start.row
- @insertRows start.row + 1, rowsAdded
- # make a copy since we're going to modify in place
- oldHighlights = (@highlights.rows[start.row] || []).slice(0)
- for highlight in oldHighlights
- if highlight.column > start.column
- # insertion was fully before this highlight
- @moveHighlight highlight,
- row: end.row
- column: highlight.column + (end.column - start.column)
- else if highlight.column + highlight.word.length >= start.column
- # insertion was inside this highlight
- @removeHighlight highlight
-
- else if change.action == "insertLines"
- @insertRows start.row, change.lines.length
-
- else if change.action == "removeText"
- if start.row == end.row
- oldHighlights = (@highlights.rows[start.row] || []).slice(0)
- else
- rowsRemoved = end.row - start.row
- oldHighlights =
- (@highlights.rows[start.row] || []).concat(
- (@highlights.rows[end.row] || [])
- )
- @removeRows start.row + 1, rowsRemoved
-
- for highlight in oldHighlights
- if @_doesHighlightOverlapRange highlight, start, end
- @removeHighlight highlight
- else if @_isHighlightAfterRange highlight, start, end
- @moveHighlight highlight,
- row: start.row
- column: highlight.column - (end.column - start.column)
-
- else if change.action == "removeLines"
- @removeRows start.row, change.lines.length
-
- _doesHighlightOverlapRange: (highlight, start, end) ->
- highlightIsAllBeforeRange =
- highlight.row < start.row or
- (highlight.row == start.row and highlight.column + highlight.word.length <= start.column)
- highlightIsAllAfterRange =
- highlight.row > end.row or
- (highlight.row == end.row and highlight.column >= end.column)
- !(highlightIsAllBeforeRange or highlightIsAllAfterRange)
-
- _isHighlightAfterRange: (highlight, start, end) ->
- return true if highlight.row > end.row
- return false if highlight.row < end.row
- highlight.column >= end.column
-
-
-
-
-
-
-
diff --git a/services/web/public/coffee/spelling/SpellingManager.coffee b/services/web/public/coffee/spelling/SpellingManager.coffee
deleted file mode 100644
index d21b408ceb..0000000000
--- a/services/web/public/coffee/spelling/SpellingManager.coffee
+++ /dev/null
@@ -1,159 +0,0 @@
-define [
- "spelling/HighlightedWordManager"
- "spelling/SpellingMenuView"
-], (HighlightedWordManager, SpellingMenuView) ->
- class SpellingManager
- constructor: (@ide) ->
- setup = _.once =>
- @updatedLines = []
- @highlightedWordManager = new HighlightedWordManager(@ide)
- @menu = new SpellingMenuView(ide: @ide)
- @menu.on "click:suggestion", (suggestion, highlight) =>
- @menu.hide()
- @replaceWord(highlight, suggestion)
- @menu.on "click:learn", (highlight) =>
- @menu.hide()
- @learnWord highlight
- @ide.editor.on "change:doc", () => @changeOpenDoc()
- @ide.editor.on "update:doc", (e) => @onDocUpdated(e)
- @ide.editor.on "mousemove", (e) => @onMouseMove(e)
-
- @ide.on "afterJoinProject", (project) =>
- @project = project
- @language = @ide.project.get("spellCheckLanguage") || window.userSettings.spellCheckLanguage
- if @language? and @language != ""
- setup()
-
- @project.on "change:spellCheckLanguage", =>
- @changeOpenDoc()
-
- changeOpenDoc: () ->
- @highlightedWordManager.clearRows()
- @runSpellCheck()
-
- onDocUpdated: (e) ->
- @highlightedWordManager.applyChange(e.data)
- @markLinesAsUpdated(e.data)
- @runSpellCheckSoon()
-
- onMouseMove: (e) ->
- @showMenuIfOverMisspelling(e.position)
-
- showMenuIfOverMisspelling: (position) ->
- highlight = @highlightedWordManager.findHighlightWithinRange
- start: position
- end: position
- if highlight
- @menu.showForHighlight(highlight)
- else
- @menu.hideIfAppropriate(position)
-
- replaceWord: (highlight, suggestion) ->
- @ide.editor.replaceText {
- start:
- row: highlight.row
- column: highlight.column
- end:
- row: highlight.row
- column: highlight.column + highlight.word.length
- }, suggestion
-
- learnWord: (highlight) ->
- @apiRequest "/learn", word: highlight.word
- @highlightedWordManager.removeWord highlight.word
-
- getHighlightedWordAtCursor: () ->
- cursor = @ide.editor.getCursorPosition()
- highlight = @highlightedWordManager.findHighlightWithinRange
- start: cursor
- end: cursor
- return highlight
-
- runSpellCheckSoon: () ->
- run = () =>
- delete @timeoutId
- @runSpellCheck(@updatedLines)
- @updatedLines = []
- if @timeoutId?
- clearTimeout @timeoutId
- @timeoutId = setTimeout run, 1000
-
- markLinesAsUpdated: (change) ->
- start = change.range.start
- end = change.range.end
-
- insertLines = () =>
- lines = end.row - start.row
- while lines--
- @updatedLines.splice(start.row, 0, true)
-
- removeLines = () =>
- lines = end.row - start.row
- while lines--
- @updatedLines.splice(start.row + 1, 1)
-
- if change.action == "insertText"
- @updatedLines[start.row] = true
- insertLines()
- else if change.action == "removeText"
- @updatedLines[start.row] = true
- removeLines()
- else if change.action == "insertLines"
- insertLines()
- else if change.action == "removeLines"
- removeLines()
-
- runSpellCheck: (linesToProcess) ->
- {words, positions} = @getWords(linesToProcess)
- language = @ide.project.get("spellCheckLanguage")
- @apiRequest "/check", {language: language, words: words}, (error, result) =>
- if error? or !result? or !result.misspellings?
- return null
-
- if linesToProcess?
- for shouldProcess, row in linesToProcess
- @highlightedWordManager.clearRows(row, row) if shouldProcess
- else
- @highlightedWordManager.clearRows()
-
- for misspelling in result.misspellings
- word = words[misspelling.index]
- position = positions[misspelling.index]
- @highlightedWordManager.addHighlight
- column: position.column
- row: position.row
- word: word
- suggestions: misspelling.suggestions
-
- getWords: (linesToProcess) ->
- lines = @ide.editor.getLines()
- words = []
- positions = []
- for line, row in lines
- if !linesToProcess? or linesToProcess[row]
- wordRegex = /\\?['a-zA-Z\u00C0-\u00FF]+/g
- while (result = wordRegex.exec(line))
- word = result[0]
- if word[0] == "'"
- word = word.slice(1)
- if word[word.length - 1] == "'"
- word = word.slice(0,-1)
- positions.push row: row, column: result.index
- words.push(word)
- return words: words, positions: positions
-
- apiRequest: (endpoint, data, callback = (error, result) ->)->
- data.token = @ide.user.get("id")
- data._csrf = csrfToken
- options =
- url: "/spelling" + endpoint
- type: "POST"
- dataType: "json"
- headers:
- "Content-Type": "application/json"
- data: JSON.stringify data
- success: (data, status, xhr) ->
- callback null, data
- error: (xhr, status, error) ->
- callback error
- $.ajax options
diff --git a/services/web/public/coffee/spelling/SpellingMenuView.coffee b/services/web/public/coffee/spelling/SpellingMenuView.coffee
deleted file mode 100644
index 3ba9fd1d96..0000000000
--- a/services/web/public/coffee/spelling/SpellingMenuView.coffee
+++ /dev/null
@@ -1,82 +0,0 @@
-define [
- "libs/backbone"
- "libs/mustache"
-], () ->
- SUGGESTIONS_TO_SHOW = 5
-
- SpellingMenuView = Backbone.View.extend
- templates:
- menu: $("#spellingMenuTemplate").html()
- entry: $("#spellingMenuEntryTemplate").html()
-
- events:
- "click a#learnWord": ->
- @trigger "click:learn", @_currentHighlight
- @hide()
-
- initialize: (options) ->
- @ide = options.ide
- @ide.editor.getContainerElement().append @render().el
- @ide.editor.on "click", () => @hide()
- @ide.editor.on "scroll", () => @hide()
- @ide.editor.on "update:doc", () => @hide()
- @ide.editor.on "change:doc", () => @hide()
-
- render: () ->
- @setElement($(@templates.menu))
- @$el.css "z-index" : 10000
- @$(".dropdown-toggle").dropdown()
- @hide()
- return @
-
- showForHighlight: (highlight) ->
- if @_currentHighlight? and highlight != @_currentHighlight
- @_close()
-
- if !@_currentHighlight?
- @_currentHighlight = highlight
- @_setSuggestions(highlight)
- position = @ide.editor.textToEditorCoordinates(
- highlight.row
- highlight.column + highlight.word.length
- )
- @_position(position.x, position.y)
- @_show()
-
- hideIfAppropriate: (cursorPosition) ->
- if @_currentHighlight?
- if !@_cursorCloseToHighlight(cursorPosition, @_currentHighlight) and !@_isOpen()
- @hide()
-
- hide: () ->
- delete @_currentHighlight
- @_close()
- @$el.hide()
-
- _setSuggestions: (highlight) ->
- @$(".spelling-suggestion").remove()
- divider = @$(".divider")
- for suggestion in highlight.suggestions.slice(0, SUGGESTIONS_TO_SHOW)
- do (suggestion) =>
- entry = $(Mustache.to_html(@templates.entry, word: suggestion))
- divider.before(entry)
- entry.on "click", () =>
- @trigger "click:suggestion", suggestion, highlight
-
- _show: () -> @$el.show()
-
- _isOpen: () ->
- @$(".dropdown-menu").is(":visible")
-
- _close: () ->
- if @_isOpen()
- @$el.dropdown("toggle")
-
- _cursorCloseToHighlight: (position, highlight) ->
- position.row == highlight.row and
- position.column >= highlight.column and
- position.column <= highlight.column + highlight.word.length + 1
-
- _position: (x,y) -> @$el.css left: x, top: y
-
-
diff --git a/services/web/public/coffee/tests/unit/UndoManagerTests.coffee b/services/web/public/coffee/tests/unit/UndoManagerTests.coffee
deleted file mode 100644
index 6181c2ef6e..0000000000
--- a/services/web/public/coffee/tests/unit/UndoManagerTests.coffee
+++ /dev/null
@@ -1,455 +0,0 @@
-define [
- "undo/UndoManager"
-], (UndoManager) ->
- describe "UndoManager", ->
- beforeEach ->
- @undoManager = new UndoManager()
-
- describe "Regression Tests", ->
- describe "block commenting", ->
- it "should convert correctly", ->
- lines = ["a", "a", "a", "a"]
- simpleDeltas = [
- deltas: [
- { insert: "%", position: 0 }
- { insert: "%", position: 3 }
- { insert: "%", position: 6 }
- { insert: "%", position: 9 }
- ]
- group: "doc"
- ]
- aceDeltas = @undoManager._simpleDeltaSetsToAceDeltaSets(simpleDeltas, lines)
- expectedAceDeltas = [
- deltas: [{
- action: "insertText"
- text: "%"
- range:
- start: column: 0, row: 0
- end: column: 1, row: 0
- }, {
- action: "insertText"
- text: "%"
- range:
- start: column: 0, row: 1
- end: column: 1, row: 1
- }, {
- action: "insertText"
- text: "%"
- range:
- start: column: 0, row: 2
- end: column: 1, row: 2
- }, {
- action: "insertText"
- text: "%"
- range:
- start: column: 0, row: 3
- end: column: 1, row: 3
- }]
- group: "doc"
- ]
- console.log aceDeltas, expectedAceDeltas
- aceDeltas.should.deep.equal expectedAceDeltas
-
- describe "_shiftLocalChangeToTopOfUndoStack", ->
- describe "with no local undos", ->
- beforeEach ->
- @undoManager.undoStack = [
- { deltaSets: [], remote: true }
- { deltaSets: [], remote: true }
- ]
- @return = @undoManager._shiftLocalChangeToTopOfUndoStack()
-
- it "should return false", ->
- @return.should.equal false
-
- describe "with a local undo that can be shifted to the top", ->
- beforeEach ->
- @undoManager.undoStack = [
- { deltaSets: [deltas: [{ position: 0, insert: "banana"}], group: "doc"], remote: true }
- { deltaSets: [deltas: [{ position: 5, insert: "baz" }], group: "doc"], remote: false }
- { deltaSets: [deltas: [{ position: 20, insert: "bar" }], group: "doc"], remote: true }
- { deltaSets: [deltas: [{ position: 10, insert: "foo" }], group: "doc"], remote: true }
- ]
- @return = @undoManager._shiftLocalChangeToTopOfUndoStack()
-
- it "should bring the local change to the top of the stack", ->
- @expected = [
- { deltaSets: [deltas: [{ position: 0, insert: "banana"}], group: "doc"], remote: true }
- { deltaSets: [deltas: [{ position: 17, insert: "bar" }], group: "doc"], remote: true }
- { deltaSets: [deltas: [{ position: 7, insert: "foo" }], group: "doc"], remote: true }
- { deltaSets: [deltas: [{ position: 5, insert: "baz" }], group: "doc"], remote: false }
- ]
- @undoManager.undoStack.should.deep.equal @expected
-
- it "should return true", ->
- @return.should.equal true
-
- describe "with a local undo that cannot be brought all the way to the top", ->
- beforeEach ->
- @undoManager.undoStack = [
- { deltaSets: [deltas: [{ position: 0, insert: "banana"}], group: "doc"], remote: true }
- { deltaSets: [deltas: [{ position: 5, insert: "baz" }], group: "doc"], remote: false }
- { deltaSets: [deltas: [{ position: 20, insert: "bar" }], group: "doc"], remote: true }
- { deltaSets: [deltas: [{ position: 6, insert: "foo" }], group: "doc"], remote: true }
- ]
- @return = @undoManager._shiftLocalChangeToTopOfUndoStack()
-
- it "should bring the change as far up the stack as possible", ->
- @expected = [
- { deltaSets: [deltas: [{ position: 0, insert: "banana"}], group: "doc"], remote: true }
- { deltaSets: [deltas: [{ position: 17, insert: "bar" }], group: "doc"], remote: true }
- { deltaSets: [deltas: [{ position: 5, insert: "baz" }], group: "doc"], remote: false }
- { deltaSets: [deltas: [{ position: 6, insert: "foo" }], group: "doc"], remote: true }
- ]
- @undoManager.undoStack.should.deep.equal @expected
-
- it "should return true", ->
- @return.should.equal true
-
- describe "_aceDeltaToSimpleDelta", ->
- beforeEach ->
- @docLines = [
- "one",
- "two",
- "three",
- "four"
- ]
-
- describe "insertText", ->
- it "should convert correctly", ->
- @aceDelta =
- action: "insertText"
- range:
- start:
- row: 2
- column: 2
- end:
- row: 2
- column: 5
- text: "foo"
- @simpleDelta = @undoManager._aceDeltaToSimpleDelta(@aceDelta, @docLines)
- @simpleDelta.should.deep.equal
- insert: "foo"
- position: 10
-
- describe "insertLines", ->
- it "should convert correctly", ->
- @aceDelta =
- action: "insertLines"
- lines: [
- "two and half"
- "two and three quarters"
- ]
- range:
- start:
- row: 2
- column: 0
- end:
- row: 4
- column: 0
- @simpleDelta = @undoManager._aceDeltaToSimpleDelta(@aceDelta, @docLines)
- @simpleDelta.should.deep.equal
- insert: "two and half\ntwo and three quarters\n"
- position: 8
-
- describe "removeText", ->
- it "should convert correctly", ->
- @aceDelta =
- action: "removeLines"
- range:
- start:
- row: 1
- column: 0
- end:
- row: 3
- column: 0
- lines: [
- "two",
- "three"
- ]
- @simpleDelta = @undoManager._aceDeltaToSimpleDelta(@aceDelta, @docLines)
- @simpleDelta.should.deep.equal
- remove: "two\nthree\n"
- position: 4
-
- describe "_simpleDeltaToAceDelta", ->
- describe "insert", ->
- beforeEach ->
- @docLines = [
- "one",
- "two"
- "three"
- ]
- describe "with leading and trailing partial lines", ->
- it "should return insertText and insertLines ace updates", ->
- @simpleDelta =
- position: 7
- insert: "after\ntwo and a half\nbefore"
- @aceDeltas = @undoManager._simpleDeltaToAceDeltas(@simpleDelta, @docLines)
- @expected = [{
- text: "after"
- range:
- start: row: 1, column: 3
- end: row:1, column: 8
- action: "insertText"
- }, {
- text: "\n"
- range:
- start: row: 1, column: 8
- end: row:2, column: 0
- action: "insertText"
- }, {
- lines: [ "two and a half" ]
- range:
- start: row: 2, column:0
- end: row:3, column: 0
- action: "insertLines"
- }, {
- text: "before"
- range:
- start: row: 3, column:0
- end: row: 3, column: 6
- action: "insertText"
- }]
- @aceDeltas.should.deep.equal @expected
-
- describe "remove", ->
- beforeEach ->
- @docLines = [
- "one",
- "two"
- "three"
- ]
- describe "with leading and trailing partial lines", ->
- it "should return insertText and insertLines ace updates", ->
- @simpleDelta =
- position: 2
- remove: "e\ntwo\nth"
- @aceDeltas = @undoManager._simpleDeltaToAceDeltas(@simpleDelta, @docLines)
- @expected = [{
- text: "th"
- range:
- start: row: 2, column:0
- end: row: 2, column: 2
- action: "removeText"
- }, {
- lines: [ "two" ]
- range:
- start: row: 1, column:0
- end: row:2, column: 0
- action: "removeLines"
- }, {
- text: "\n"
- range:
- start: row: 0, column: 3
- end: row:1, column: 0
- action: "removeText"
- }, {
- text: "e"
- range:
- start: row: 0, column: 2
- end: row: 0, column: 3
- action: "removeText"
- }]
- @aceDeltas.should.deep.equal @expected
-
- describe "_concatSimpleDeltas", ->
- it "should concat adjacent simple deltas", ->
- @result = @undoManager._concatSimpleDeltas [{
- insert: "foo"
- position: 5
- }, {
- insert: "bar"
- position: 8
- }, {
- insert: "baz"
- position: 11
- }, {
- insert: "one"
- position: 20
- }, {
- insert: "two"
- position: 23
- }, {
- remove: "three"
- position: 26
- }]
-
- @result.should.deep.equal [{
- insert: "foobarbaz"
- position: 5
- }, {
- insert: "onetwo"
- position: 20
- }, {
- remove: "three"
- position: 26
- }]
-
- it "should concat removes", ->
- @result = @undoManager._concatSimpleDeltas [{
- remove: "foo"
- position: 5
- }, {
- remove: "bar"
- position: 5
- }, {
- remove: "baz"
- position: 5
- }]
-
- @result.should.deep.equal [{
- remove: "foobarbaz"
- position: 5
- }]
-
- describe "_swapSimpleDeltaOrder", ->
- describe "insert - insert", ->
- describe "when the first delta is before the second", ->
- beforeEach ->
- # result: "**fooba"
- [@newFirstDelta, @newSecondDelta] = @undoManager._swapSimpleDeltaOrder(
- { insert: "foo", position: 2 }
- { insert: "ba", position: 5 }
- )
-
- it "should remove the length of the first insert from the second position", ->
- @newFirstDelta.should.deep.equal position: 2, insert: "ba"
- @newSecondDelta.should.deep.equal position: 2, insert: "foo"
-
- describe "when the second delta is inserted into the first", ->
- beforeEach ->
- # result "**fobao*"
- @result = @undoManager._swapSimpleDeltaOrder(
- { insert: "foo", position: 2 }
- { insert: "ba", position: 4 }
- )
-
- it "should return null", ->
- (@result?).should.equal false
-
- describe "when the second delta is before the first", ->
- beforeEach ->
- # result: "**bafoo"
- [@newFirstDelta, @newSecondDelta] = @undoManager._swapSimpleDeltaOrder(
- { insert: "foo", position: 2 }
- { insert: "ba", position: 2 }
- )
-
- it "should add the length of the second delta on to the first position", ->
- @newFirstDelta.should.deep.equal insert: "ba", position: 2
- @newSecondDelta.should.deep.equal insert: "foo", position: 4
-
- describe "remove - remove", ->
- describe "when the first delta is before the second", ->
- beforeEach ->
- # start "**fooba*
- [@newFirstDelta, @newSecondDelta] = @undoManager._swapSimpleDeltaOrder(
- { remove: "foo", position: 2 }
- { remove: "ba", position: 2 }
- )
-
- it "should add the length of the first remove onto the second position", ->
- @newFirstDelta.should.deep.equal position: 5, remove: "ba"
- @newSecondDelta.should.deep.equal position: 2, remove: "foo"
-
- describe "when the first and second delta overlap", ->
- beforeEach ->
- # start "**bfooa*
- @result = @undoManager._swapSimpleDeltaOrder(
- { remove: "foo", position: 3 }
- { remove: "ba", position: 2 }
- )
-
- it "should return null", ->
- (@result?).should.equal false
-
- describe "when the second delta is before the first", ->
- beforeEach ->
- # start "**bafoo*
- [@newFirstDelta, @newSecondDelta] = @undoManager._swapSimpleDeltaOrder(
- { remove: "foo", position: 4 }
- { remove: "ba", position: 2 }
- )
-
- it "should remove the length of the second delta from the first position", ->
- @newFirstDelta.should.deep.equal position: 2, remove: "ba"
- @newSecondDelta.should.deep.equal position: 2, remove: "foo"
-
- describe "insert - remove", ->
- describe "when the first delta is before the second", ->
- beforeEach ->
- # "**ba" -> "**fooba" -> "**foo"
- [@newFirstDelta, @newSecondDelta] = @undoManager._swapSimpleDeltaOrder(
- { insert: "foo", position: 2 }
- { remove: "ba", position: 5 }
- )
-
- it "should remove the length of the first delta from the second position", ->
- @newFirstDelta.should.deep.equal position: 2, remove: "ba"
- @newSecondDelta.should.deep.equal position: 2, insert: "foo"
-
- describe "when the deltas overlap", ->
- beforeEach ->
- # "**ba" -> "**bfooa" -> "**ooa"
- @result = @undoManager._swapSimpleDeltaOrder(
- { insert: "foo", position: 3 }
- { remove: "bf", position: 2 }
- )
-
- it "should return null", ->
- (@result?).should.equal false
-
- describe "when the second delta is before the first", ->
- beforeEach ->
- # "**ba" -> "**bafoo" -> "**foo"
- [@newFirstDelta, @newSecondDelta] = @undoManager._swapSimpleDeltaOrder(
- { insert: "foo", position: 4 }
- { remove: "ba", position: 2 }
- )
-
- it "should remove the length of the second delta from the first position", ->
- @newFirstDelta.should.deep.equal position: 2, remove: "ba"
- @newSecondDelta.should.deep.equal position: 2, insert: "foo"
-
- describe "remove - insert", ->
- describe "when the first delta is before the second", ->
- beforeEach ->
- # "**foo" -> "**" -> "**ba"
- [@newFirstDelta, @newSecondDelta] = @undoManager._swapSimpleDeltaOrder(
- { remove: "foo", position: 2 }
- { insert: "ba", position: 2 }
- )
-
- it "should add the length of the first delta to the second position", ->
- @newFirstDelta.should.deep.equal position: 5, insert: "ba"
- @newSecondDelta.should.deep.equal position: 2, remove: "foo"
-
- #Â I don't think the deltas can overlap in this case!
-
- describe "when the first delta is after the second", ->
- beforeEach ->
- # "**foo" -> "**" -> "*ba*"
- [@newFirstDelta, @newSecondDelta] = @undoManager._swapSimpleDeltaOrder(
- { remove: "foo", position: 2 }
- { insert: "ba", position: 1 }
- )
-
- it "should add the length of the second delta on to the first position", ->
- @newFirstDelta.should.deep.equal position: 1, insert: "ba"
- @newSecondDelta.should.deep.equal position: 4, remove: "foo"
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/services/web/public/coffee/tests/unit/auto-complete/SuggestionManager.coffee b/services/web/public/coffee/tests/unit/auto-complete/SuggestionManager.coffee
deleted file mode 100644
index 895226a83d..0000000000
--- a/services/web/public/coffee/tests/unit/auto-complete/SuggestionManager.coffee
+++ /dev/null
@@ -1,68 +0,0 @@
-define [
- "auto-complete/SuggestionManager"
- "libs/chai"
-], (SuggestionManager, chai) ->
- should = chai.should()
-
- describe "SuggestionManager", ->
- beforeEach ->
- @suggestionManager = new SuggestionManager()
-
- describe "loadCommandsFromDoc", ->
- it "should return commands with no arguments", ->
- @suggestionManager.loadCommandsFromDoc """
- \\alpha
- \\beta \\gamma
- """
- @suggestionManager.commands.should.deep.equal [
- ["alpha", 0, 0]
- ["beta", 0, 0]
- ["gamma", 0, 0]
- ]
-
- it "should return commands with arguments", ->
- @suggestionManager.loadCommandsFromDoc """
- \\begin{document}
- \\includegraphics[width=10pt]{foo.png}
- \\frac{1}{2}
- """
- @suggestionManager.commands.should.deep.equal [
- ["begin", 0, 1]
- ["includegraphics", 1, 1]
- ["frac", 0, 2]
- ]
-
- it "should not care about whitespace between arguments", ->
- @suggestionManager.loadCommandsFromDoc """
- \\includegraphics
- \t [blah]
- \t\t {woo}
- """
- @suggestionManager.commands.should.deep.equal [
- ["includegraphics", 1, 1]
- ]
-
- it "should parse nested commands", ->
- @suggestionManager.loadCommandsFromDoc """
- \\frac{
- \\overbrace{1}{2}
- }{2}
- """
- @suggestionManager.commands.should.deep.equal [
- ["frac", 0, 2]
- ["overbrace", 0, 2]
- ]
-
- it "should not duplicate commands", ->
- @suggestionManager.loadCommandsFromDoc """
- \\frac{2}{3}
- \\frac{4}{5}
- """
- @suggestionManager.commands.should.deep.equal [
- ["frac", 0, 2]
- ]
-
-
-
-
-
diff --git a/services/web/public/coffee/tests/unit/editor/DocumentTests.coffee b/services/web/public/coffee/tests/unit/editor/DocumentTests.coffee
deleted file mode 100644
index 2ac939227b..0000000000
--- a/services/web/public/coffee/tests/unit/editor/DocumentTests.coffee
+++ /dev/null
@@ -1,233 +0,0 @@
-define [
- "libs/chai"
- "editor/Document"
- "editor/ShareJsDoc"
- "libs/sinon"
-], (
- chai
- Document
- ShareJsDoc
-) ->
- should = chai.should()
-
- describe "Document", ->
- beforeEach ->
- @ide =
- socket: @socket =
- socket:
- sessionid: @session_id = "mock-session-id"
- connected: true
- emit: (name, args..., callback = () ->) ->
- if @handlers[name]?
- @handlers[name].call(@, args..., callback)
- handlers: {}
- sinon.spy @ide.socket, "emit"
- _.extend(@ide, Backbone.Events)
- _.extend(@socket, Backbone.Events)
- @socket.removeListener = @socket.off
-
- sinon.spy ShareJsDoc::, "flushPendingOps"
- sinon.spy ShareJsDoc::, "updateConnectionState"
-
- @doc_id = "mock-doc-id"
- @docLines = ["hello", "world"]
- @version = 5
- @remote_session_id = "remote-session-id"
-
- @socket.handlers["joinDoc"] = (doc_id, args...) =>
- doc_id.should.equal @doc_id
- callback = args.pop()
- callback null, @docLines, @version, []
- @socket.handlers["leaveDoc"] = (doc_id, args...) =>
- doc_id.should.equal @doc_id
- callback = args.pop()
- callback null
-
-
- @doc = new Document(@ide, @doc_id)
- sinon.spy @doc, "trigger"
-
- afterEach ->
- ShareJsDoc::flushPendingOps.restore()
- ShareJsDoc::updateConnectionState.restore()
-
- # This is a little pattern I'm trying out to make the tests below much
- # more readable when they have a lot of repetition
- CONDITIONS =
- "connected": () ->
- @doc.connected = true
-
- "not connected": () ->
- @doc.connected = false
-
- "reconnected": () ->
- @ide.trigger "afterJoinProject"
-
- "disconnected": () ->
- @ide.socket.trigger "disconnect"
-
- "joining": () ->
- @callback = sinon.stub()
- @doc.join @callback
-
- "joined": () ->
- @callback = sinon.stub()
- @doc.join @callback
-
- "leaving": () ->
- @callback = sinon.stub()
- @doc.leave @callback
-
- "there are buffered ops": () ->
- sinon.stub @doc.doc, "hasBufferedOps", () -> return true
-
- "the buffered ops have been sent and acknowledged": () ->
- sinon.stub @doc.doc, "processUpdateFromServer"
- @doc.doc.hasBufferedOps.restore()
- @ide.socket.trigger "otUpdateApplied", { doc: @doc_id }
- @doc.doc.processUpdateFromServer.restore()
-
- TESTS =
- "emit joinDoc": () ->
- @socket.emit.calledWith("joinDoc", @doc_id).should.equal true
-
- "not emit joinDoc": () ->
- @socket.emit.calledWith("joinDoc").should.equal false
-
- "emit leaveDoc": () ->
- @socket.emit.calledWith("leaveDoc", @doc_id).should.equal true
-
- "not emit leaveDoc": () ->
- @socket.emit.calledWith("leaveDoc").should.equal false
-
- "be joined": () ->
- @doc.joined.should.equal true
-
- "not be joined": () ->
- @doc.joined.should.equal false
-
- "be wanting to be joined": () ->
- @doc.wantToBeJoined.should.equal true
-
- "not be wanting to be joined": () ->
- @doc.wantToBeJoined.should.equal false
-
- "call the callback": () ->
- @callback.called.should.equal true
-
- "not call the callback": () ->
- @callback.called.should.equal false
-
- "flush any pending ops": () ->
- ShareJsDoc::flushPendingOps.called.should.equal true
-
- "update the connection state to ok": () ->
- ShareJsDoc::updateConnectionState.calledWith("ok").should.equal true
-
- WHEN = (condition, callback) -> [
- "when " + condition,
- () ->
- beforeEach CONDITIONS[condition]
- callback()
- ]
-
- SHOULD = (test) -> ["should " + test, TESTS[test]]
-
-
- describe WHEN("connected", ->
- describe WHEN("joining", ->
- it SHOULD("emit joinDoc")...
- it SHOULD("be joined")...
- it SHOULD("be wanting to be joined")...
- it SHOULD("call the callback")...
- )...
-
- describe WHEN("leaving", ->
- it SHOULD("emit leaveDoc")...
- it SHOULD("not be joined")...
- it SHOULD("not be wanting to be joined")...
- it SHOULD("call the callback")...
- )...
-
- describe WHEN("joined", ->
- describe WHEN("there are buffered ops", ->
- describe WHEN("leaving", ->
- it SHOULD("not emit leaveDoc")...
- it SHOULD("be joined")...
- it SHOULD("not be wanting to be joined")...
- it SHOULD("not call the callback")...
-
- describe WHEN("the buffered ops have been sent and acknowledged", ->
- it SHOULD("emit leaveDoc")...
- it SHOULD("not be joined")...
- it SHOULD("not be wanting to be joined")...
- it SHOULD("call the callback")...
- )...
- )...
- )...
-
- describe WHEN("disconnected", ->
- describe WHEN("leaving", ->
- it SHOULD("not be wanting to be joined")...
- it SHOULD("not emit leaveDoc")...
- it SHOULD("call the callback")...
-
- describe WHEN("reconnected", ->
- it SHOULD("not emit leaveDoc")...
- it SHOULD("not be joined")...
- it SHOULD("not be wanting to be joined")...
- )...
- )...
-
- describe WHEN("there are buffered ops", ->
- describe WHEN("leaving", ->
- it SHOULD("not be wanting to be joined")...
- it SHOULD("not emit leaveDoc")...
- it SHOULD("not call the callback")...
-
- describe WHEN("reconnected", ->
- it SHOULD("emit joinDoc")...
- it SHOULD("flush any pending ops")...
- describe WHEN("the buffered ops have been sent and acknowledged", ->
- it SHOULD("emit leaveDoc")...
- it SHOULD("not be joined")...
- it SHOULD("not be wanting to be joined")...
- it SHOULD("call the callback")...
- )...
- )...
- )...
- )...
- )...
- )...
- )...
-
- describe WHEN("not connected", ->
- describe WHEN("joining", ->
- it SHOULD("be wanting to be joined")...
- it SHOULD("not emit joinDoc")...
- it SHOULD("not be joined")...
- it SHOULD("not call the callback")...
- describe WHEN("reconnected", ->
- it SHOULD("emit joinDoc")...
- it SHOULD("be joined")...
- it SHOULD("be wanting to be joined")...
- it SHOULD("flush any pending ops")...
- it SHOULD("update the connection state to ok")...
- )...
- )...
- )...
-
- describe "leaving a doc", ->
- describe "when not connected", ->
- describe "with buffered ops", ->
- it "should not be wanting to be joined"
-
- describe "when reconnected", ->
- it "should emit joinDoc"
- it "should flush the bufferedOps"
- it "should emit leaveDoc"
- it "should not be joined"
- it "should be wanting to be joined"
- it "should call the callback"
-
-
diff --git a/services/web/public/coffee/tests/unit/editor/ShareJsDocTests.coffee b/services/web/public/coffee/tests/unit/editor/ShareJsDocTests.coffee
deleted file mode 100644
index 6d53e63f4a..0000000000
--- a/services/web/public/coffee/tests/unit/editor/ShareJsDocTests.coffee
+++ /dev/null
@@ -1,251 +0,0 @@
-define [
- "libs/chai"
- "editor/ShareJsDoc"
- "libs/sharejs"
- "libs/sinon"
-], (
- chai
- ShareJsDoc
- ShareJs
-) ->
- should = chai.should()
- expect = chai.expect
-
- describe "ShareJsDoc", ->
- beforeEach ->
- @lines = ["hello", "world"]
- @snapshot = @lines.join("\n")
- @doc_id = "mock-doc-id"
- @version = 42
-
- socket: @socket =
- socket:
- sessionid: @session_id = "mock-session-id"
- connected: true
- emit: sinon.stub()
-
- sinon.spy ShareJs.Doc::, "_onMessage"
- sinon.spy ShareJs.Doc::, "on"
- sinon.spy ShareJs, "Doc"
-
- afterEach ->
- ShareJs.Doc::_onMessage.restore()
- ShareJs.Doc::on.restore()
- ShareJs.Doc.restore()
-
- describe "Creating a ShareJsDoc", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
-
- it "should create a new ShareJs.Doc instance", ->
- expect(@shareJsDoc._doc instanceof ShareJs.Doc).to.be.true
-
- it "should set the ShareJs doc connection", ->
- ShareJs.Doc
- .calledWith(@shareJsDoc.connection)
- .should.equal true
-
- it "should set the ShareJs doc name", ->
- ShareJs.Doc
- .calledWith(sinon.match.any, @doc_id)
- .should.equal true
-
- it "should set the type of the ShareJs doc to 'text'", ->
- ShareJs.Doc
- .calledWith(sinon.match.any, sinon.match.any, type: "text")
- .should.equal true
-
- it "should open the ShareJs doc so that it is in an active state", ->
- ShareJs.Doc::_onMessage
- .calledWith({
- open: true
- snapshot: @snapshot
- v: @version
- })
- .should.equal true
-
- it "should bind to the ShareJs doc events", ->
- ShareJs.Doc::on.calledWith("change").should.equal true
- ShareJs.Doc::on.calledWith("acknowledge").should.equal true
- ShareJs.Doc::on.calledWith("remoteop").should.equal true
-
- describe "Sending an op", ->
- describe "when the server responds", ->
- beforeEach (done) ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- @shareJsDoc.submitOp @op = [p: 5, i: "foo"], () -> done()
-
- # Send the acknowledgement so that the callback is called
- setTimeout () =>
- @shareJsDoc.processUpdateFromServer {
- v: @version
- doc: @doc_id
- }
- , 10
-
-
- it "should send the op to the server", ->
- @socket.emit
- .calledWith("applyOtUpdate", @doc_id, {
- doc: @doc_id,
- op: @op
- v: @version
- })
- .should.equal true
-
- it "should update the document snapshot", ->
- @shareJsDoc.getSnapshot().should.equal "hellofoo\nworld"
-
- describe "when the server does not respond", ->
- beforeEach (done) ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- sinon.spy @shareJsDoc, "trigger"
- @shareJsDoc.INFLIGHT_OP_TIMEOUT = 50
- @shareJsDoc.submitOp @op = [p: 5, i: "foo"]
-
- setTimeout () ->
- done()
- , 100
-
- it "should trigger an error", ->
- @shareJsDoc.trigger
- .calledWith("error", "Doc op was not acknowledged in time")
- .should.equal true
-
- describe "clearInflightAndPendingOps", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- @shareJsDoc._doc.inflightOp = "mock-op-1"
- @shareJsDoc._doc.inflightCallbacks = ["mock-callback-1"]
- @shareJsDoc._doc.pendingOp = "mock-op-2"
- @shareJsDoc._doc.pendingCallbacks = ["mock-callback-2"]
-
- @shareJsDoc.clearInflightAndPendingOps()
-
- it "should clear any inflight or pendings ops", ->
- expect(@shareJsDoc._doc.inflightOp).to.be.null
- @shareJsDoc._doc.inflightCallbacks.should.deep.equal []
- expect(@shareJsDoc._doc.pendingOp).to.be.null
- @shareJsDoc._doc.pendingCallbacks.should.deep.equal []
-
- describe "flushPendingOps", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- @shareJsDoc._doc.flush = sinon.stub()
- @shareJsDoc.flushPendingOps()
-
- it "should call _doc.flush", ->
- @shareJsDoc._doc.flush.called.should.equal true
-
- describe "updateConnectionState", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- @shareJsDoc._doc._connectionStateChanged = sinon.stub()
- @shareJsDoc._doc.autoOpen = true
- @socket.socket.sessionid = "new-connection-id"
-
- @shareJsDoc.updateConnectionState "mock-state"
-
- it "should set autoOpen to false so that ShareJs doesn't try to send an open message", ->
- @shareJsDoc._doc.autoOpen.should.equal false
-
- it "should update the connection state", ->
- @shareJsDoc.connection.state.should.equal "mock-state"
-
- it "should notify the ShareJs doc of the state change", ->
- @shareJsDoc._doc._connectionStateChanged
- .calledWith("mock-state")
- .should.equal true
-
- it "should set the connection id to the latest socket id", ->
- @shareJsDoc.connection.id.should.equal "new-connection-id"
-
- describe "hasBufferedOps", ->
- describe "with inflight ops", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- @shareJsDoc._doc.inflightOp = "mock-op-1"
-
- it "should return true", ->
- @shareJsDoc.hasBufferedOps().should.equal true
-
- describe "with pending ops", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- @shareJsDoc._doc.pendingOp = "mock-op-1"
-
- it "should return true", ->
- @shareJsDoc.hasBufferedOps().should.equal true
-
- describe "with no buffered ops", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
-
- it "should return false", ->
- @shareJsDoc.hasBufferedOps().should.equal false
-
- describe "processUpdateFromServer", ->
- describe "successfully", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- @shareJsDoc._doc._onMessage = sinon.stub()
- @shareJsDoc.processUpdateFromServer "mock-message"
-
- it "should pass the message onto the ShareJs Doc", ->
- @shareJsDoc._doc._onMessage
- .calledWith( "mock-message" )
- .should.equal true
-
- describe "with an error", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- sinon.spy @shareJsDoc, "trigger"
- @shareJsDoc._doc._onMessage = sinon.stub().throws(@error = {message: "Mock error"})
- @shareJsDoc.processUpdateFromServer "mock-message"
-
- it "should trigger the error handler", ->
- @shareJsDoc.trigger
- .calledWith("error", @error)
- .should.equal true
-
- describe "with an external update", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- @shareJsDoc._doc._onMessage = sinon.stub()
- sinon.spy @shareJsDoc, "trigger"
- @shareJsDoc.processUpdateFromServer { op: "mock-op", meta: { type: "external" } }
-
- it "should trigger an externalUpdate event", ->
- @shareJsDoc.trigger
- .calledWith("externalUpdate")
- .should.equal true
-
-
- describe "catchUp", ->
- beforeEach ->
- @shareJsDoc = new ShareJsDoc(@doc_id, @lines, @version, @socket)
- sinon.stub @shareJsDoc, "processUpdateFromServer", () ->
- @_doc.version++
-
- @shareJsDoc.catchUp [{
- ops: ["mock-op-1"]
- }, {
- ops: ["mock-op-2"]
- }]
-
- it "should apply each update", ->
- @shareJsDoc.processUpdateFromServer
- .calledWith({
- doc: @doc_id
- v: @version
- ops: ["mock-op-1"]
- })
- .should.equal true
-
- @shareJsDoc.processUpdateFromServer
- .calledWith({
- doc: @doc_id
- v: @version + 1
- ops: ["mock-op-2"]
- })
- .should.equal true
diff --git a/services/web/public/coffee/tests/unit/helpers.coffee b/services/web/public/coffee/tests/unit/helpers.coffee
deleted file mode 100644
index 5ddbe6794a..0000000000
--- a/services/web/public/coffee/tests/unit/helpers.coffee
+++ /dev/null
@@ -1,10 +0,0 @@
-define [
- "underscore"
- "libs/backbone"
-], () ->
- class SocketIoMock
- constructor: () ->
- _.extend(SocketIoMock::, Backbone.Events)
- SocketIoMock::emit = () -> @trigger.apply(@, arguments)
-
- return SocketIoMock: SocketIoMock
diff --git a/services/web/public/coffee/tests/unit/history/FileDiff.coffee b/services/web/public/coffee/tests/unit/history/FileDiff.coffee
deleted file mode 100644
index 3b97b04016..0000000000
--- a/services/web/public/coffee/tests/unit/history/FileDiff.coffee
+++ /dev/null
@@ -1,140 +0,0 @@
-define [
- "libs/chai"
- "history/FileDiff"
- "history/FileDiffView"
- "libs/sinon"
-], (chai, FileDiff, FileDiffView) ->
- should = chai.should()
-
- describe "FileDiffView", ->
- describe "Updated text", ->
- beforeEach ->
- @model = new FileDiff
- path: "dir/file"
- type: "updated"
- sections: [{
- old_start_line: 5,
- new_start_line: 7,
- lines : [
- { type: "unchanged", content: "line 5" },
- { type: "removed", content: "removed line" },
- { type: "added", content: "added line" },
- { type: "unchanged", content: "line 7" }
- ]
- }, {
- old_start_line: 12,
- new_start_line: 12,
- lines : [
- { type: "unchanged", content: "line 12" },
- { type: "removed", content: "removed line" },
- { type: "added", content: "added line" },
- { type: "unchanged", content: "line 14" }
- ]
- }]
- @view = new FileDiffView
- model : @model
- @view.render()
-
- it "should render a pretty version of the diff", ->
- htmlLines = @view.$(".line")
- lines = for line in htmlLines
- old_line_number: $(line).find(".old_line_number").text()
- new_line_number: $(line).find(".new_line_number").text()
- symbol: $(line).find(".symbol").text()
- content: $(line).find(".content").text()
-
- lines[0].should.deep.equal(old_line_number: "5", new_line_number: "7", symbol: "", content: "line 5")
- lines[1].should.deep.equal(old_line_number: "6", new_line_number: "", symbol: "-", content: "removed line")
- lines[2].should.deep.equal(old_line_number: "", new_line_number: "8", symbol: "+", content: "added line")
- lines[3].should.deep.equal(old_line_number: "7", new_line_number: "9", symbol: "", content: "line 7")
-
- lines[4].should.deep.equal(old_line_number: "12", new_line_number: "12", symbol: "", content: "line 12")
- lines[5].should.deep.equal(old_line_number: "13", new_line_number: "", symbol: "-", content: "removed line")
- lines[6].should.deep.equal(old_line_number: "", new_line_number: "13", symbol: "+", content: "added line")
- lines[7].should.deep.equal(old_line_number: "14", new_line_number: "14", symbol: "", content: "line 14")
-
- it "should load the raw file when clicked", ->
- model = @model
- @model.fetch = (options) ->
- @set("content", "Test content")
- options.success(model) if options.success?
-
- @view.$(".rawFileContent").text().should.equal "Loading..."
- @view.$(".nav .raw").click()
- @view.$(".rawFileContent").text().should.equal "Test content"
-
- describe "Deleted text", ->
- beforeEach ->
- @model = new FileDiff
- path: "dir/file"
- type: "deleted"
- sections: [{
- old_start_line: 12,
- new_start_line: 12,
- lines : [
- { type: "removed", content: "line 12" }
- ]
- }]
- @view = new FileDiffView
- model : @model
- @view.render()
-
- it "shouldn't show a link to the raw file", ->
- @view.$(".nav .raw").length.should.equal 0
-
- describe "Blank diff", ->
- beforeEach ->
- @model = new FileDiff
- path: "dir/file"
- type: "deleted"
- @view = new FileDiffView
- model : @model
- @view.render()
-
- it "shouldn't show a diff", ->
- @view.$(".tab-diff").length.should.equal 0
-
-
- describe "Updated binary file", ->
- beforeEach ->
- @model = new FileDiff
- binary: true
- path: "dir/file"
- version_id: "123"
- type: "updated"
- @view = new FileDiffView
- model : @model
- @view.render()
-
- it "should display a link to the file", ->
- @view.$("a.rawFileLink").attr("href").should.equal "/project/#{userSettings.project_id}/version/123/file/dir/file"
-
- describe "Deleted binary file", ->
- beforeEach ->
- @model = new FileDiff
- binary: true
- type: "deleted"
- @view = new FileDiffView
- model : @model
- @view.render()
-
- it "should not display a link to the file", ->
- @view.$("a.rawFileLink").length.should.equal 0
-
-
- describe "Moved file", ->
- beforeEach ->
- @model = new FileDiff
- path: "new-path"
- oldPath: "old-path"
- type: "renamed"
- @view = new FileDiffView
- model : @model
- @view.render()
-
- it "should say the file was moved", ->
- @view.$("h3").text().should.equal("old-path")
- @view.$(".fileMoved").text().should.equal("Moved to new-path")
-
-
-
diff --git a/services/web/public/coffee/tests/unit/history/HistoryManager.coffee b/services/web/public/coffee/tests/unit/history/HistoryManager.coffee
deleted file mode 100644
index 9130c1a614..0000000000
--- a/services/web/public/coffee/tests/unit/history/HistoryManager.coffee
+++ /dev/null
@@ -1,158 +0,0 @@
-define [
- "libs/chai"
- "history/HistoryManager"
- "models/Project"
- "account/AccountManager"
- "libs/sinon"
-], (chai, HistoryManager, Project, AccountManager) ->
- should = chai.should()
-
- describe "HistoryManager", ->
- beforeEach ->
- @editor =
- sideBarView:
- addLink: sinon.stub()
- selectLink: (identifier) ->
- mainAreaManager:
- change: (selector) ->
- addArea: (options) ->
- $("#test-area").append(options.element)
- options.element.show()
- _.extend(@editor, Backbone.Events)
- @historyManager = new HistoryManager @editor
- sinon.stub @historyManager.versionListView, "loadUntilFull", () ->
- sinon.stub @historyManager.versionList, "fetchNewVersions", () ->
-
- afterEach -> @historyManager.historyPanel.remove()
-
- describe "takeSnapshot", ->
- beforeEach ->
- @error = false
- sinon.stub $, "ajax", (options) =>
- if @error
- options.error() if options.error?
- else
- options.success() if options.success?
- @project = new Project id: userSettings.project_id
- @message = "what a wonderful message"
- @editor.trigger("afterJoinProject", @project)
-
- afterEach ->
- $.ajax.restore()
-
- describe "success", ->
- beforeEach ->
- @error = false
- @callback = sinon.stub()
- @historyManager.takeSnapshot(@message, @callback)
-
- it "should POST to /project//snapshot", ->
- $.ajax.called.should.equal true
- options = $.ajax.args[0][0]
- options.type.should.equal "POST"
- options.url.should.equal "/project/#{userSettings.project_id}/snapshot"
- options.data.should.deep.equal message: @message
-
- it "should call the callback without an error", ->
- @callback.called.should.equal true
- @callback.args[0].length.should.equal 0
-
- it "should call fetchNewVersions on collection", ->
- @historyManager.versionList.fetchNewVersions
- .called.should.equal true
-
- describe "error", ->
- beforeEach ->
- @error = true
- @callback = sinon.stub()
- @historyManager.takeSnapshot(@message, @callback)
-
- it "should call the callback with an error", ->
- @callback.called.should.equal true
- (@callback.args[0][0] instanceof Error).should.equal true
-
- describe "with versioning to be shown", ->
- beforeEach ->
- @project = new Project
- features : {}
- @editor.project = @project
- @editor.trigger("afterJoinProject", @project)
-
- it "should insert the History link into the side bar", ->
- @editor.sideBarView.addLink.called.should.equal true
- args = @editor.sideBarView.addLink.args[0][0]
- args.identifier.should.equal "history"
- args.before.should.equal "settings"
- should.exist args.element
- args.element.text().should.equal "History"
-
- describe "when clicking on the history link", ->
- beforeEach ->
- @element = @editor.sideBarView.addLink.args[0][0].element
-
- describe "generally", ->
- beforeEach ->
- sinon.spy @historyManager, "showHistoryArea"
- sinon.spy @editor.sideBarView, "selectLink"
- sinon.spy @editor.mainAreaManager, "change"
- @element.click()
-
- it "should call showHistoryArea", ->
- @historyManager.showHistoryArea.called.should.equal true
-
- it "should select the history link", ->
- @editor.sideBarView.selectLink.called.should.equal true
- @editor.sideBarView.selectLink.calledWith("history").should.equal true
-
- it "should show the history area", ->
- @editor.mainAreaManager.change.called.should.equal true
- @editor.mainAreaManager.change.calledWith("history").should.equal true
-
- describe "with versioning enabled", ->
- beforeEach ->
- @project.get("features").versioning = true
- @element.click()
-
- it "should display the history", ->
- @historyManager.view.$("#versionListArea").is(":visible").should.equal true
- @historyManager.view.$("#diffViewArea").is(":visible").should.equal true
- @historyManager.view.$("#enableVersioningMessage").is(":visible").should.equal false
-
- it "should load versions when the history area is shown", ->
- @historyManager.showHistoryArea()
- @historyManager.versionListView.loadUntilFull.called.should.equal true
- @historyManager.versionList.fetchNewVersions.called.should.equal true
-
- describe "with versioning disabled", ->
- beforeEach ->
- @project.get("features").versioning = false
- @element.click()
-
- it "should display the prompt to enable versioning", ->
- @historyManager.view.$("#versionListArea").is(":visible").should.equal false
- @historyManager.view.$("#diffViewArea").is(":visible").should.equal false
- @historyManager.view.$("#enableVersioningMessage").is(":visible").should.equal true
-
- it "should not attempt to load versions when the history area is shown", ->
- @historyManager.showHistoryArea()
- @historyManager.versionListView.loadUntilFull.called.should.equal false
- @historyManager.versionList.fetchNewVersions.called.should.equal false
-
- describe "when the user clicks 'Enable now'", ->
- describe "when enabled succesfully", ->
- beforeEach ->
- sinon.stub AccountManager, "askToUpgrade", (editor, options) ->
- editor.project.set("features", versioning: true)
- options.onUpgrade()
- @historyManager.view.$("#enableVersioning").click()
-
- afterEach ->
- AccountManager.askToUpgrade.restore()
-
- it "should start the user on their free trial", ->
- AccountManager.askToUpgrade.called.should.equal true
-
- it "should display the history interface", ->
- @historyManager.view.$("#versionListArea").is(":visible").should.equal true
- @historyManager.view.$("#diffViewArea").is(":visible").should.equal true
- @historyManager.view.$("#enableVersioningMessage").is(":visible").should.equal false
diff --git a/services/web/public/coffee/tests/unit/history/HistoryView.coffee b/services/web/public/coffee/tests/unit/history/HistoryView.coffee
deleted file mode 100644
index d2d48fb00e..0000000000
--- a/services/web/public/coffee/tests/unit/history/HistoryView.coffee
+++ /dev/null
@@ -1,47 +0,0 @@
-define [
- "libs/chai"
- "history/HistoryView"
- "utils/Modal"
- "libs/sinon"
-], (chai, HistoryView, Modal) ->
- should = chai.should()
-
- describe "HistoryView", ->
- beforeEach ->
- @el = $("")
- $("#test-area").append(@el)
- @historyManager =
- takeSnapshot: sinon.stub()
- @historyView = new HistoryView el : @el, manager: @historyManager
-
- describe "takeSnapshot", ->
- beforeEach ->
- sinon.spy Modal, "createModal"
- @historyView.takeSnapshot()
-
- afterEach ->
- $(".modal").remove()
- Modal.createModal.restore()
-
- it "should display a modal asking for a comment", ->
- Modal.createModal.called.should.equal true
- options = Modal.createModal.args[0][0]
- should.exist options.title
- should.exist options.message
- options.buttons[0].text.should.equal "Cancel"
- options.buttons[1].text.should.equal "Take Snapshot"
-
- describe "user clicks ok", ->
- beforeEach ->
- @message = "what a wonderful message"
- $("#snapshotComment").val(@message)
- $(".modal-footer a.btn-primary").click()
-
- it "should call takeSnapshot with the message", ->
- @historyManager.takeSnapshot.calledWith(@message)
- .should.equal true
-
-
-
-
-
diff --git a/services/web/public/coffee/tests/unit/history/VersionListView.coffee b/services/web/public/coffee/tests/unit/history/VersionListView.coffee
deleted file mode 100644
index 4fe0b91a4a..0000000000
--- a/services/web/public/coffee/tests/unit/history/VersionListView.coffee
+++ /dev/null
@@ -1,71 +0,0 @@
-define [
- "libs/chai"
- "history/VersionListView"
- "history/VersionList"
- "history/Version"
- "libs/sinon"
-], (chai, VersionListView, VersionList, Version) ->
- should = chai.should()
-
- describe "VersionListView", ->
- beforeEach ->
- collection = @collection = new VersionList()
- versionCounter = 1
- @collection.fetchNextBatch = (options) ->
- for i in [1..3]
- collection.add new Version message: "Test Snapshot #{versionCounter}"
- versionCounter++
- options.success() if options.success?
-
- @view = new VersionListView
- collection : @collection
- @view.$el.css height: "200px"
- $("#test-area").append(@view.$el)
-
- afterEach ->
- @view.$el.remove()
-
- it "should add versions to the list as they are added to the collection", ->
- @collection.add new Version message : "Test Snapshot 1"
- @collection.add new Version message : "Test Snapshot 2"
- should.equal $(@view.$(".version-message")[0]).text(), "Test Snapshot 1"
- should.equal $(@view.$(".version-message")[1]).text(), "Test Snapshot 2"
-
- it "should load more versions when scrolled to the end", (done) ->
- # Add enough versions to make the list long enough to scroll
- originalVersions = 10
- for i in [1..originalVersions]
- @collection.add new Version message: "Test Snapshot"
-
- @view.$el.scrollTop(10000) # Should get us to the bottom
-
- collection = @collection
- setTimeout (() ->
- should.equal collection.models.length > originalVersions, true
- done()
- ), 0
-
- it "should load more versions when the list does not take up the whole view", (done) ->
- view = @view
- @view.loadUntilFull null, () ->
- should.equal view.$("#version-list").height() > view.$el.height(), true
- should.equal view.collection.models.length > 0, true
- done()
-
- it "should stop loading versions if the collection returns an error", (done) ->
- @collection.fetchNextBatch = (options) ->
- options.error() if options.error?
- view = @view
- @view.loadUntilFull null, () ->
- should.equal view.collection.models.length, 0
- done()
-
- it "should show the empty message if the collection is empty after fetching", (done) ->
- @collection.pop() until @collection.isEmpty()
- @collection.fetchNextBatch = (options) ->
- # error callback is triggered on 404
- options.error() if options.error()
-
- @view.loadUntilFull null, () =>
- @view.$(".empty-message").is(":visible").should.equal true
- done()
diff --git a/services/web/public/coffee/tests/unit/modal.coffee b/services/web/public/coffee/tests/unit/modal.coffee
deleted file mode 100644
index 5d4af8f0bb..0000000000
--- a/services/web/public/coffee/tests/unit/modal.coffee
+++ /dev/null
@@ -1,56 +0,0 @@
-define [
- "utils/Modal"
-], (Modal) ->
- describe "Modal", ->
- describe "initialization", ->
- beforeEach ->
- @modal = Modal.createModal
- title: "Test modal"
- message : "Test modal message"
- buttons: [{
- text : "OK"
- class : "btn-primary"
- }, {
- text : "Cancel"
- class : "btn-danger"
- }]
- afterEach ->
- @modal.remove()
-
- it "should display the modal", ->
- @modal.$el.is(":visible").should.equal true
-
- it "should include the buttons in reverse order", ->
- buttons = @modal.$(".modal-footer a")
- $(buttons[0]).text().should.equal "Cancel"
- $(buttons[0]).hasClass("btn-danger").should.equal true
- $(buttons[1]).text().should.equal "OK"
- $(buttons[1]).hasClass("btn-primary").should.equal true
-
- it "should include the title", ->
- @modal.$("h3").text().should.equal "Test modal"
-
- it "should include the message", ->
- @modal.$(".message").text().should.equal "Test modal message"
-
- describe "clicking buttons", ->
- beforeEach ->
- @callbackCalled = false
- @modal = Modal.createModal
- title: "Test modal"
- message : "Test modal message"
- buttons: [{
- text : "OK"
- class : "btn-primary"
- callback : () =>
- @callbackCalled = true
- }]
- @modal.$(".modal-footer a").click()
-
- it "should call the callback", ->
- @callbackCalled.should.equal true
-
- it "should remove the modal", ->
- @modal.$el.is(":visible").should.equal false
- @modal.$el.parent().length.should.equal 0
-
diff --git a/services/web/public/coffee/tests/unit/project-members.coffee b/services/web/public/coffee/tests/unit/project-members.coffee
deleted file mode 100644
index 6d32ad4fac..0000000000
--- a/services/web/public/coffee/tests/unit/project-members.coffee
+++ /dev/null
@@ -1,275 +0,0 @@
-define [
- "libs/chai"
- "project-members/ProjectMembersManager"
- "models/User"
- "models/Project"
- "account/AccountManager"
- "tests/unit/helpers"
- "libs/sinon"
-], (chai, PM, User, Project, AccountManager, Helpers) ->
- should = chai.should()
-
- describe "ProjectMembersManager", ->
- beforeEach ->
- @socket = new Helpers.SocketIoMock()
- @ide =
- socket: @socket
- mainAreaManager:
- addArea: sinon.stub()
- sideBarView:
- addLink: sinon.stub()
- tabManager:
- addTab: sinon.stub()
- _.extend(@ide, Backbone.Events)
- @projectMembersManager = new PM.ProjectMembersManager @ide
- @project = new Project {
- _id : "project-1"
- owner:
- _id : 1
- email : "owner@example.com"
- privileges : "owner"
- members : [{
- _id : 2
- email : "readonly@example.com"
- privileges : "readOnly"
- },
- {
- _id : 3
- email : "readwrite@example.com"
- privileges : "readAndWrite"
- }]
- features:
- collaborators: -1
- rootFolder: [
- _id: "root-folder-id"
- name: "root-folder"
- docs: []
- folders: []
- fileRefs: []
- ]
- }, {
- parse: true
- }
- @ide.project = @project
- @project.set "ide", @ide
-
- describe "when the project loaded is owned by the user", ->
- beforeEach ->
- @ide.security =
- permissionsLevel: "owner"
- @ide.trigger "afterJoinProject", @project
-
- it "should show the admin controls on the member list", ->
- @projectMembersManager.view.options.showAdminControls.should.equal true
-
- describe "when the project loaded is not owned by the user", ->
- beforeEach ->
- @ide.security =
- permissionsLevel: "readAndWrite"
- @ide.project = @project
- @ide.trigger "afterJoinProject", @project
-
- it "should socket show the admin controls on the member list", ->
- @projectMembersManager.view.options.showAdminControls.should.equal false
-
- describe "when the project is loaded with too many collaborators", ->
- beforeEach ->
- @project.get("features").collaborators = 1
- sinon.stub AccountManager, "showUpgradeDialog"
- @ide.project = @project
- @ide.trigger "afterJoinProject", @project
-
- afterEach ->
- AccountManager.showUpgradeDialog.restore()
-
- it "should show the upgrade dialog", ->
- AccountManager.showUpgradeDialog.called.should.equal true
- options = AccountManager.showUpgradeDialog.args[0][1]
- options.message.should.equal "This project has too many collaborators for your plan. Please upgrade your account or remove some collaborators"
-
- describe "when the project is loaded", ->
- beforeEach ->
- @ide.project = @project
- @ide.trigger "afterJoinProject", @project
-
- describe "when a member is removed client-side", ->
- beforeEach ->
- @socket.on "removeUserFromProject", @removeUserFromProject = sinon.spy()
- @projectMembersManager.removeMember id: "member-123"
-
- it "should call socket.removeUserFromProject", ->
- @removeUserFromProject.calledWith("member-123").should.equal true
-
- describe "when a member is removed server-side", ->
- beforeEach ->
- @projectMembersManager.members.add new User
- id: "user-1"
- @projectMembersManager.members.add new User
- id: "user-2"
- @ide.socket.emit "userRemovedFromProject", "user-1"
-
- it "should remove the member", ->
- for member in @projectMembersManager.members.models
- member.id.should.not.equal "user-1"
-
- describe "when a member is added client-side", ->
- describe "successfully", ->
- beforeEach ->
- @socket.on "addUserToProject", @addUserToProject =
- sinon.stub().callsArgWith(2, null, true)
- @projectMembersManager.addMember "new-member@example.com", "readAndWrite"
-
- it "should call socket.addUserToProject", ->
- @addUserToProject
- .calledWith("new-member@example.com", "readAndWrite")
- .should.equal true
-
- describe "when the user needs to upgrade", ->
- beforeEach ->
- @socket.on "addUserToProject", @addUserToProject =
- sinon.stub().callsArgWith(2, null, false)
- sinon.stub AccountManager, "askToUpgrade", (ide, options) ->
- ide.project.set("features", collaborators: 5)
- @projectMembersManager.addMember "new-member@example.com", "readAndWrite"
-
- afterEach ->
- AccountManager.askToUpgrade.restore()
-
- it "should start the user on their free trial", ->
- AccountManager.askToUpgrade.called.should.equal true
-
- describe "when a member is added server-side", ->
- beforeEach ->
- @socket.emit "userAddedToProject",
- _id: "new-user-1"
- email: "new-user@example.com",
- "readOnly"
-
- it "should add a member", ->
- for possibleMember in @projectMembersManager.members.models
- if possibleMember.id == "new-user-1"
- member = possibleMember
- should.exist member
- member.get("email").should.equal "new-user@example.com"
- member.get("privileges").should.equal "readOnly"
-
- describe "ProjectMembersView", ->
- beforeEach ->
- @socket = new Helpers.SocketIoMock()
- @ide =
- socket: @socket
- mainAreaManager:
- addArea: sinon.stub()
- sideBarView:
- addLink: sinon.stub()
- tabManager:
- addTab: sinon.stub()
- _.extend(@ide, Backbone.Events)
- @projectMembersManager = new PM.ProjectMembersManager(@ide)
- @collection = new PM.ProjectMemberList()
- @view = new PM.ProjectMemberListView
- collection: @collection
- manager: @projectMembersManager
- @view.render()
- $("#test-area").append(@view.$el)
-
- afterEach ->
- @view.$el.remove()
-
- describe "when formatting privileges", ->
- it "should format the read-write privileges nicely", ->
- @collection.add new User email: "test1@example.com", privileges: "readAndWrite"
- @view.$(".projectMember").find(".privileges").text().should.equal "Read and Write"
-
- it "should format the read-only privileges nicely", ->
- @collection.add new User email: "test1@example.com", privileges: "readOnly"
- @view.$(".projectMember").find(".privileges").text().should.equal "Read Only"
-
- it "should format the owner privileges nicely", ->
- @collection.add new User email: "test1@example.com", privileges: "owner"
- @view.$(".projectMember").find(".privileges").text().should.equal "Owner"
-
- describe "when the collection already has entries when the view is rendered", ->
- beforeEach ->
- @collection.add new User email: "test1@example.com", privileges: "readAndWrite"
- @view.render()
-
- it "should render the member in the view", ->
- @view.$(".projectMember").find(".email").text().should.equal "test1@example.com"
-
- describe "when the collection is updated", ->
- describe "by adding a member", ->
- beforeEach ->
- @collection.add new User email: "test1@example.com", privileges: "readAndWrite"
-
- it "should add the member to view", ->
- @view.$(".projectMember").find(".email").text().should.equal "test1@example.com"
-
- describe "by removing a member", ->
- beforeEach ->
- @member = new User email: "test1@example.com", privileges: "readAndWrite"
- @collection.add @member
- @collection.remove @member
-
- it "should remove the user from the view", ->
- @view.$(".projectMember").length.should.equal 0
-
- describe "with admin controls", ->
- beforeEach ->
- @view.options.showAdminControls = true
- @view.render()
-
- it "should show the remove link for members", ->
- @collection.add new User email: "test1@example.com", privileges: "readAndWrite"
- @view.$(".projectMember").find(".removeUser").length.should.equal 1
-
- it "should not show the remove link for the owner", ->
- @collection.add new User email: "test1@example.com", privileges: "owner"
- @view.$(".projectMember").find(".removeUser").length.should.equal 0
-
- it "should show the form to add a member", ->
- @view.$(".addUser").length.should.equal 1
-
- describe "without admin controls", ->
- it "should not show the remove link for members", ->
- @collection.add new User email: "test1@example.com", privileges: "readAndWrite"
- @view.$(".projectMember").find(".removeUser").length.should.equal 0
-
- it "should not show the form to add a member", ->
- @view.$(".addUser").length.should.equal 0
-
- describe "when removing a user", ->
- beforeEach ->
- sinon.stub @projectMembersManager, "removeMember", (user) ->
- @view.options.showAdminControls = true
- @member = new User email: "test1@example.com", privileges: "readAndWrite"
- @collection.add @member
-
- describe "when the remove link is clicked", ->
- beforeEach -> @view.$(".projectMember").find(".removeUser").click()
-
- it "should remove the user", ->
- @projectMembersManager.removeMember.called.should.equal true
- @projectMembersManager.removeMember.calledWith(@member).should.equal true
-
- describe "when adding a user", ->
- beforeEach ->
- sinon.stub @projectMembersManager, "addMember", (email, privileges) ->
- @view.options.showAdminControls = true
- @view.render()
-
-
- describe "when the add user button is clicked", ->
- beforeEach ->
- @view.$(".email").val("new-user@example.com")
- @view.$(".privileges").val("readOnly")
- @view.$(".addUser").click()
-
- it "should add a user", ->
- @projectMembersManager.addMember.called.should.equal true
- @projectMembersManager.addMember.calledWith("new-user@example.com", "readOnly").should.equal true
-
- it "should reset the email address", ->
- @view.$(".email").val().should.equal ""
-
-
diff --git a/services/web/public/coffee/tests/unit/project.coffee b/services/web/public/coffee/tests/unit/project.coffee
deleted file mode 100644
index 6807e35a0e..0000000000
--- a/services/web/public/coffee/tests/unit/project.coffee
+++ /dev/null
@@ -1,164 +0,0 @@
-define [
- "libs/chai"
- "models/Project"
- "models/ProjectMemberList"
- "models/User"
- "tests/unit/helpers"
- "libs/sinon"
-], (chai, Project, ProjectMemberList, User, Helpers) ->
- should = chai.should()
-
-
- describe "Project", ->
- beforeEach ->
- @socket = new Helpers.SocketIoMock()
-
- describe "parsing", ->
- beforeEach ->
- @socket.on "setProjectName", @setProjectName = sinon.spy()
- @socket.on "setPublicAccessLevel", @setPublicAccessLevel = sinon.spy()
- @socket.on "setCompiler", @setCompiler = sinon.spy()
- @socket.on "setRootDoc", @setRootDoc = sinon.spy()
- @project = new Project {
- _id : "project-1"
- rootDoc_id : "root-doc-id"
- name : "Project Name"
- publicAccesLevel : "readOnly"
- owner:
- _id : 1
- email : "owner@example.com"
- privileges : "owner"
- members : [{
- _id : 2
- email : "readonly@example.com"
- privileges : "readOnly"
- },
- {
- _id : 3
- email : "readwrite@example.com"
- privileges : "readAndWrite"
- }]
- features:
- versioning: true
- collaborators: 3
- rootFolder: [
- _id: "root-folder-id"
- name: "root-folder"
- docs: []
- folders: []
- fileRefs: []
- ]
-
- }, {
- parse: true
- }
- @project.set "ide", socket: @socket
-
- it "should set the id", ->
- @project.id.should.equal "project-1"
-
- it "should set the name without calling socket.setProjectName", ->
- @project.get("name").should.equal "Project Name"
- @setProjectName.called.should.equal false
-
- it "should set the public access level without called socket.setPublicAccessLevel", ->
- @project.get("publicAccesLevel").should.equal "readOnly"
- @setPublicAccessLevel.called.should.equal false
-
- it "should create a collection of collaborators", ->
- should.exist @project.get("members")
- (@project.get("members") instanceof ProjectMemberList).should.equal true
-
- members = @project.get("members").models
- members[0].id.should.equal 1
- members[0].get("email").should.equal "owner@example.com"
- members[0].get("privileges").should.equal "owner"
- members[1].id.should.equal 2
- members[1].get("email").should.equal "readonly@example.com"
- members[1].get("privileges").should.equal "readOnly"
- members[2].id.should.equal 3
- members[2].get("email").should.equal "readwrite@example.com"
- members[2].get("privileges").should.equal "readAndWrite"
-
- it "should create an owner model", ->
- should.exist @project.get("owner")
- (@project.get("owner") instanceof User).should.equal true
- @project.get("owner").get("privileges").should.equal "owner"
-
- it "should include the features", ->
- @project.get("features").should.deep.equal
- versioning: true
- collaborators: 3
-
- it "should set the rootDoc_id without calling socket.setRootDoc", ->
- @project.get("rootDoc_id").should.equal "root-doc-id"
- @setRootDoc.called.should.equal false
-
- describe "updating the root document", ->
- beforeEach ->
- @project = new Project
- id: "project-1",
- rootDoc_id: "old-root-doc-id"
- @project.set "ide", socket: @socket
-
- it "should call socket.setRootDoc when the root doc is changed", ->
- @socket.on "setRootDoc", @setRootDoc = sinon.spy()
- @project.set("rootDoc_id", "new-root-doc-id")
- @setRootDoc.calledWith("new-root-doc-id").should.equal true
-
- it "should update the root doc when socket.rootDocUpdated is called", ->
- @socket.emit "rootDocUpdated", "new-root-doc-id"
- @project.get("rootDoc_id").should.equal "new-root-doc-id"
-
-
- describe "updating the compiler the project uses", ->
- beforeEach ->
- @project = new Project
- id : "project-1",
- compiler : "latex"
- @project.set "ide", socket: @socket
-
- it "should call socket.setCompiler when the compiler is changed", ->
- @socket.on "setCompiler", @setCompiler = sinon.spy()
- @project.set("compiler", "xetex")
- @setCompiler.calledWith("xetex").should.equal true
-
- it "should update the compiler when socket.compilerUpdated is called", ->
- @socket.emit "compilerUpdated", "xetex"
- @project.get("compiler").should.equal "xetex"
-
- describe "updating the project's name", ->
- beforeEach ->
- @project = new Project
- id : "project-1",
- name : "Old Name"
- @project.set "ide", socket: @socket
-
- it "should call socket.setProjectName when the name is changed", ->
- @socket.on "setProjectName", @setProjectName = sinon.spy()
- @project.set("name", "New Name")
- @setProjectName.calledWith(window.window_id, "New Name").should.equal true
-
- it "should update the name when socket.projectNameUpdated is called", ->
- @socket.emit "projectNameUpdated", "window-id-2", "New Name"
- @project.get("name").should.equal "New Name"
-
- describe "updating the project's public access level", ->
- beforeEach ->
- @project = new Project
- id : "project-1",
- publicAccesLevel : "readOnly"
- @project.set "ide", socket: @socket
-
- it "should call socket.setPublicAccessLevel when the level is changed", ->
- @socket.on "setPublicAccessLevel", @setPublicAccessLevel = sinon.spy()
- @project.set("publicAccesLevel", "readAndWrite")
- @setPublicAccessLevel.calledWith("readAndWrite").should.equal true
-
- it "should update the level when socket.publicAccessLevelUpdated is called", ->
- @socket.emit "publicAccessLevelUpdated", "readAndWrite"
- @project.get("publicAccesLevel").should.equal "readAndWrite"
-
-
-
-
diff --git a/services/web/public/coffee/tests/unit/run.coffee b/services/web/public/coffee/tests/unit/run.coffee
deleted file mode 100644
index 211a7af294..0000000000
--- a/services/web/public/coffee/tests/unit/run.coffee
+++ /dev/null
@@ -1,21 +0,0 @@
-require ["libs/mocha", "libs/underscore", "libs/jquery"], ->
- mocha.setup({
- ui: "bdd",
- globals: ["now", "socket"]
- })
- require [
- "tests/unit/UndoManagerTests"
- "tests/unit/history/FileDiff"
- "tests/unit/history/VersionListView"
- "tests/unit/history/HistoryView"
- "tests/unit/spelling/HighlightedWordManagerTests"
- "tests/unit/spelling/SpellingManagerTests"
- "tests/unit/auto-complete/SuggestionManager"
- "tests/unit/project-members"
- "tests/unit/user"
- "tests/unit/project"
- "tests/unit/modal"
- "tests/unit/editor/DocumentTests"
- "tests/unit/editor/ShareJsDocTests"
- ], ->
- mocha.run()
diff --git a/services/web/public/coffee/tests/unit/spelling/HighlightedWordManagerTests.coffee b/services/web/public/coffee/tests/unit/spelling/HighlightedWordManagerTests.coffee
deleted file mode 100644
index e2c3007fc4..0000000000
--- a/services/web/public/coffee/tests/unit/spelling/HighlightedWordManagerTests.coffee
+++ /dev/null
@@ -1,295 +0,0 @@
-define [
- "spelling/HighlightedWordManager"
- "libs/chai"
-], (HighlightedWordsManager, chai) ->
- should = chai.should()
-
- describe "HighlightedWordsManager", ->
- beforeEach ->
- @currentMarkerId = 0
- @ide =
- editor:
- addMarker: () => ++@currentMarkerId
- removeMarker: sinon.stub()
- sinon.spy @ide.editor, "addMarker"
- @highlightedWordsManager = new HighlightedWordsManager(@ide)
- @highlightedWordsManager.doesHighlightExist = (row, column, word) ->
- for highlight in @highlights.rows[row]
- if highlight.row == row and highlight.column == column and highlight.word == word
- return true
- return false
-
- describe "addHighlight", ->
- beforeEach ->
- @highlightedWordsManager.addHighlight
- row: 1
- column: 5
- word: "sharelatex"
-
- it "should add a marker into ace", ->
- @ide.editor.addMarker.called.should.equal true
- [options, klass] = @ide.editor.addMarker.args[0]
- options.should.deep.equal row: 1, column: 5, length: 10
- klass.should.equal "sharelatex-spelling-highlight"
-
- it "should record the highlight internally", ->
- row = @highlightedWordsManager.highlights.rows[1]
- highlight = row[0]
- highlight.word.should.equal "sharelatex"
- highlight.markerId.should.equal @currentMarkerId
-
- describe "removeHighlight", ->
- beforeEach ->
- @highlightedWordsManager.addHighlight row: 0, column: 5, word: "sharelatex"
- @highlightedWordsManager.addHighlight row: 0, column: 17, word: "sharelatex"
- @highlightedWordsManager.addHighlight row: 0, column: 42, word: "sharelatex"
- @highlight = @highlightedWordsManager.highlights.rows[0][1]
- @highlightedWordsManager.removeHighlight @highlight
-
- it "should remove the marker", ->
- @ide.editor.removeMarker.calledWith(@highlight.markerId).should.equal true
-
- it "should remove the highlight internally", ->
- @highlightedWordsManager.doesHighlightExist(0, 17, "sharelatex").should.equal false
-
- describe "removeWord", ->
- beforeEach ->
- @highlightedWordsManager.addHighlight row: 0, column: 5, word: "sharelatex"
- @highlightedWordsManager.addHighlight row: 1, column: 5, word: "banana"
- @highlightedWordsManager.addHighlight row: 3, column: 5, word: "sharelatex"
- @highlightedWordsManager.addHighlight row: 4, column: 5, word: "monkey"
- @highlightedWordsManager.removeWord "sharelatex"
-
- it "should remove all instances of the word", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal false
- @highlightedWordsManager.doesHighlightExist(3, 5, "sharelatex").should.equal false
-
- it "should not remove other highlights", ->
- @highlightedWordsManager.doesHighlightExist(1, 5, "banana").should.equal true
- @highlightedWordsManager.doesHighlightExist(4, 5, "monkey").should.equal true
-
- describe "moveHighlight", ->
- beforeEach ->
- @highlightedWordsManager.addHighlight row: 0, column: 5, word: "sharelatex"
- @highlight = @highlightedWordsManager.highlights.rows[0][0]
- @oldMarkerId = @highlight.markerId
- @highlightedWordsManager.moveHighlight @highlight, row: 1, column: 17
-
- it "should remove the old marker", ->
- @ide.editor.removeMarker.calledWith(@oldMarkerId).should.equal true
-
- it "should insert a new marker", ->
- @ide.editor.addMarker.calledWith(
- {row: 1, column: 17, length: 10},
- "sharelatex-spelling-highlight"
- ).should.equal true
-
- it "should move the highlight internally", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal false
- @highlightedWordsManager.doesHighlightExist(1, 17, "sharelatex").should.equal true
-
- describe "clearHighlights", ->
- beforeEach ->
- @highlightedWordsManager.addHighlight row: 0, column: 5, word: "sharelatex"
- @highlightedWordsManager.addHighlight row: 1, column: 5, word: "sharelatex"
- @highlightedWordsManager.addHighlight row: 3, column: 5, word: "sharelatex"
- @highlightedWordsManager.addHighlight row: 4, column: 5, word: "sharelatex"
-
- describe "with a range", ->
- beforeEach ->
- @highlightedWordsManager.clearRows 1, 3
-
- it "should clear the given rows", ->
- @highlightedWordsManager.doesHighlightExist(1, 5, "sharelatex").should.equal false
- @highlightedWordsManager.doesHighlightExist(3, 5, "sharelatex").should.equal false
-
- it "should not clear the other rows", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal true
- @highlightedWordsManager.doesHighlightExist(4, 5, "sharelatex").should.equal true
-
- describe "with no range", ->
- beforeEach ->
- @highlightedWordsManager.clearRows()
-
- it "should clear all the rows", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal false
- @highlightedWordsManager.doesHighlightExist(1, 5, "sharelatex").should.equal false
- @highlightedWordsManager.doesHighlightExist(3, 5, "sharelatex").should.equal false
- @highlightedWordsManager.doesHighlightExist(4, 5, "sharelatex").should.equal false
-
-
- describe "applyChange", ->
- beforeEach ->
- @highlightedWordsManager.addHighlight row: 0, column: 5, word: "sharelatex"
- @highlightedWordsManager.addHighlight row: 0, column: 17, word: "latex"
- @highlightedWordsManager.addHighlight row: 0, column: 25, word: "monkey"
- @highlightedWordsManager.addHighlight row: 1, column: 5, word: "banana"
-
- describe "inserting text into a single line", ->
- beforeEach ->
- @highlightedWordsManager.applyChange
- action: "insertText"
- range:
- start:
- row: 0
- column: 17
- end:
- row: 0
- column: 22
- text: "share"
-
- it "should move highlights after the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 25, "monkey").should.equal false
- @highlightedWordsManager.doesHighlightExist(0, 30, "monkey").should.equal true
-
- it "should remove highlights affected by the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 17, "latex").should.equal false
-
- it "should not affect highlights before the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal true
-
- describe "inserting text into multiple lines", ->
- beforeEach ->
- @highlightedWordsManager.applyChange
- action: "insertText"
- range:
- start:
- row: 0
- column: 18
- end:
- row: 1
- column: 0
- text: "\n"
-
- it "should move highlights after the change onto the new line", ->
- @highlightedWordsManager.doesHighlightExist(0, 25, "monkey").should.equal false
- @highlightedWordsManager.doesHighlightExist(1, 7, "monkey").should.equal true
-
- it "should remove highlights affected by the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 17, "latex").should.equal false
-
- it "should not affect highlights before the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal true
-
- describe "inserting lines", ->
- beforeEach ->
- @highlightedWordsManager.applyChange
- action: "insertLines"
- range:
- start:
- row: 1
- column: 0
- end:
- row: 3
- column: 0
- lines: ["", ""]
-
- it "should move the highlights after the inserted lines", ->
- @highlightedWordsManager.doesHighlightExist(1, 5, "banana").should.equal false
- @highlightedWordsManager.doesHighlightExist(3, 5, "banana").should.equal true
-
- it "should not affect highlights before the inserted lines", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal true
-
- describe "deleting text from a single line", ->
- beforeEach ->
- @highlightedWordsManager.applyChange
- action: "removeText"
- range:
- start:
- row: 0
- column: 19
- end:
- row: 0
- column: 20
- text: "t"
-
- it "should move highlights after the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 25, "monkey").should.equal false
- @highlightedWordsManager.doesHighlightExist(0, 24, "monkey").should.equal true
-
- it "should remove highlights affected by the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 17, "latex").should.equal false
-
- it "should not affect highlights before the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal true
-
- describe "deleting text from a multiple lines", ->
- beforeEach ->
- @highlightedWordsManager.applyChange
- action: "removeText"
- range:
- start:
- row: 0
- column: 27
- end:
- row: 1
- column: 3
-
- it "should move highlights after the change onto the first line", ->
- @highlightedWordsManager.doesHighlightExist(1, 5, "banana").should.equal false
- @highlightedWordsManager.doesHighlightExist(0, 29, "banana").should.equal true
-
- it "should remove highlights affected by the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 25, "monkey").should.equal false
-
- it "should not affect highlights before the change", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal true
-
- describe "deleting lines", ->
- beforeEach ->
- @highlightedWordsManager.applyChange
- action: "removeLines"
- range:
- start:
- row: 0
- column: 0
- end:
- row: 1
- column: 0
- lines: [""]
-
- it "should move the highlights after the inserted lines", ->
- @highlightedWordsManager.doesHighlightExist(1, 5, "banana").should.equal false
- @highlightedWordsManager.doesHighlightExist(0, 5, "banana").should.equal true
-
- it "should remove the highlights in the removed lines", ->
- @highlightedWordsManager.doesHighlightExist(0, 5, "sharelatex").should.equal false
-
- describe "findHighlightWithinRange", ->
- beforeEach ->
- @highlightedWordsManager.addHighlight row: 0, column: 5, word: "sharelatex"
- @highlightedWordsManager.addHighlight row: 0, column: 17, word: "latex"
- @highlightedWordsManager.addHighlight row: 0, column: 25, word: "monkey"
- @highlightedWordsManager.addHighlight row: 1, column: 5, word: "banana"
-
- describe "with range inside word", ->
- beforeEach ->
- @highlight = @highlightedWordsManager.findHighlightWithinRange
- start: row: 0, column: 6
- end: row: 0, column: 7
-
- it "should return the highlight", ->
- @highlight.row.should.equal 0
- @highlight.column.should.equal 5
- @highlight.word.should.equal "sharelatex"
-
- describe "with range outside word", ->
- beforeEach ->
- @highlight = @highlightedWordsManager.findHighlightWithinRange
- start: row: 0, column: 3
- end: row: 0, column: 4
-
- it "should return null", ->
- should.not.exist @highlight
-
- describe "with range equal to word", ->
- beforeEach ->
- @highlight = @highlightedWordsManager.findHighlightWithinRange
- start: row: 0, column: 5
- end: row: 0, column: 15
-
- it "should return null", ->
- @highlight.row.should.equal 0
- @highlight.column.should.equal 5
- @highlight.word.should.equal "sharelatex"
diff --git a/services/web/public/coffee/tests/unit/spelling/SpellingManagerTests.coffee b/services/web/public/coffee/tests/unit/spelling/SpellingManagerTests.coffee
deleted file mode 100644
index 1866785371..0000000000
--- a/services/web/public/coffee/tests/unit/spelling/SpellingManagerTests.coffee
+++ /dev/null
@@ -1,218 +0,0 @@
-define [
- "spelling/SpellingManager"
- "libs/chai"
-], (SpellingManager, chai) ->
- should = chai.should()
-
- describe "SpellingManager", ->
- beforeEach ->
- @el = $("")
- $("#test-area").append(@el)
- window.userSettings =
- spellCheckLanguage: "en"
- @lines = []
- @project =
- attributes: {}
- get: (attribute) -> @attributes[attribute]
- @ide =
- editor:
- on: () ->
- getLines: => @lines
- getContainerElement: () => @el
- project: @project
- _.extend(@ide, Backbone.Events)
- _.extend(@project, Backbone.Events)
- @spellingManager = new SpellingManager(@ide)
- @ide.trigger "afterJoinProject", @project
-
- describe "runSpellCheck", ->
- beforeEach ->
- @misspellings = [
- { row: 1, column: 2, word: "bussines", index: 1, suggestions: ["business"] }
- { row: 4, column: 0, word: "misteak", index: 2, suggestions: ["mistake"] }
- ]
- @words =
- words: ["sharelatex", "bussines", "misteak"]
- positions: [
- { row: 0, column: 5 },
- { row: 1, column: 2 },
- { row: 4, column: 0 }
- ]
- @spellingManager.getWords = () => @words
- sinon.spy @spellingManager, "getWords"
- @spellingManager.apiRequest = (endpoint, data, callback) => callback null, misspellings: @misspellings
- @spellingManager.highlightedWordManager.clearRows = sinon.stub()
- @spellingManager.highlightedWordManager.addHighlight = sinon.stub()
-
- describe "with no argument", ->
- beforeEach ->
- @spellingManager.runSpellCheck()
-
- it "should clear all of the existing highlights", ->
- @spellingManager.highlightedWordManager.clearRows.calledWithExactly().should.equal true
-
- it "should add in highlights for all misspellings", ->
- for misspelling, i in @misspellings
- highlight =
- row: misspelling.row
- column: misspelling.column
- word: misspelling.word
- suggestions: misspelling.suggestions
- @spellingManager.highlightedWordManager.addHighlight.args[i][0]
- .should.deep.equal highlight
-
- it "should get all words", ->
- @spellingManager.getWords.calledWithExactly(undefined).should.equal true
-
- describe "with specified lines", ->
- beforeEach ->
- @spellingManager.runSpellCheck(@lines = [false, true, false, true])
-
- it "should only get words on the given lines", ->
- @spellingManager.getWords.calledWithExactly(@lines).should.equal true
-
- it "should only clear the given rows of highlights", ->
- @spellingManager.highlightedWordManager.clearRows.calledWithExactly(1,1).should.equal true
- @spellingManager.highlightedWordManager.clearRows.calledWithExactly(3,3).should.equal true
-
- describe "markLinesAsUpdated", ->
- describe "inserts on a single line", ->
- beforeEach ->
- @spellingManager.markLinesAsUpdated
- action: "insertText"
- range:
- start: { row: 1, column: 3 }
- end: { row: 1, column: 8 }
-
- it "should mark the line as updated", ->
- @spellingManager.updatedLines[1].should.equal true
-
- describe "inserts on multiple lines", ->
- beforeEach ->
- @spellingManager.updatedLines = [false, false, false, false, true]
- @spellingManager.markLinesAsUpdated
- action: "insertText"
- range:
- start: { row: 1, column: 3 }
- end: { row: 3, column: 8 }
-
- it "should mark the lines as updated", ->
- @spellingManager.updatedLines[1].should.equal true
- @spellingManager.updatedLines[2].should.equal true
- @spellingManager.updatedLines[3].should.equal true
-
- it "should move existing lines", ->
- @spellingManager.updatedLines[4].should.equal false
- @spellingManager.updatedLines[6].should.equal true
-
- describe "deletes on a single line", ->
- beforeEach ->
- @spellingManager.markLinesAsUpdated
- action: "removeText"
- range:
- start: { row: 1, column: 3 }
- end: { row: 1, column: 8 }
-
- it "should mark the line as updated", ->
- @spellingManager.updatedLines[1].should.equal true
-
- describe "deletes on multiple lines", ->
- beforeEach ->
- @spellingManager.updatedLines = [false, false, false, false, false, true]
- @spellingManager.markLinesAsUpdated
- action: "removeText"
- range:
- start: { row: 1, column: 3 }
- end: { row: 3, column: 8 }
-
- it "should mark the lines as updated", ->
- @spellingManager.updatedLines[1].should.equal true
-
- it "should move existing lines", ->
- @spellingManager.updatedLines[3].should.equal true
-
- describe "insertLines", ->
- beforeEach ->
- @spellingManager.updatedLines = [false, false, false, false, true]
- @spellingManager.markLinesAsUpdated
- action: "insertLines"
- range:
- start: { row: 1, column: 0 }
- end: { row: 3, column: 0 }
-
- it "should mark the lines as updated", ->
- @spellingManager.updatedLines[1].should.equal true
- @spellingManager.updatedLines[2].should.equal true
-
- it "should move existing lines", ->
- @spellingManager.updatedLines[4].should.equal false
- @spellingManager.updatedLines[6].should.equal true
-
- describe "removeLines", ->
- beforeEach ->
- @spellingManager.updatedLines = [false, false, false, false, true]
- @spellingManager.markLinesAsUpdated
- action: "removeLines"
- range:
- start: { row: 1, column: 0 }
- end: { row: 3, column: 0 }
-
- it "should move existing lines", ->
- @spellingManager.updatedLines.length.should.equal 3
- @spellingManager.updatedLines[2].should.equal true
-
- describe "getWords", ->
- describe "with no argument", ->
- beforeEach ->
- @lines = [
- "\\documentclass{article}"
- "\\begin{document}"
- "Hello world"
- "\\end{document}"
- ]
- {@words, @positions} = @spellingManager.getWords()
-
- it "should return all of the words", ->
- @words.should.deep.equal [
- "\\documentclass", "article", "\\begin", "document", "Hello", "world", "\\end", "document"
- ]
-
- it "should return the correct positions", ->
- @positions.length.should.equal @words.length
- @positions[5].should.deep.equal row: 2, column: 6
-
- describe "with selective lines", ->
- beforeEach ->
- @lines = [
- "zero"
- "one uno"
- "two dos"
- "three"
- ]
- {@words, @positions} = @spellingManager.getWords([false, true, true, false])
-
- it "should only return words from the given lines", ->
- @words.should.deep.equal [
- "one", "uno", "two", "dos"
- ]
-
- describe "with accents", ->
- beforeEach ->
- @lines = ["accént"]
- {@words, @positions} = @spellingManager.getWords()
-
- it "should treat the accented character as a letter", ->
- @words.should.deep.equal ["accént"]
-
- describe "with single quote marks", ->
- beforeEach ->
- @lines = ["you'll 'words appear in quotes' and with apostrophe's"]
- {@words, @positions} = @spellingManager.getWords()
-
- it "should understand the difference between apostophes and quotes", ->
- @words.should.deep.equal [
- "you'll", "words", "appear", "in", "quotes", "and", "with", "apostrophe's"
- ]
-
-
-
diff --git a/services/web/public/coffee/tests/unit/user.coffee b/services/web/public/coffee/tests/unit/user.coffee
deleted file mode 100644
index 32b5210fb9..0000000000
--- a/services/web/public/coffee/tests/unit/user.coffee
+++ /dev/null
@@ -1,24 +0,0 @@
-define [
- "libs/chai"
- "models/User"
- "libs/sinon"
-], (chai, User) ->
- describe "User", ->
- describe "findOrBuild", ->
- describe "with an existing model", ->
- beforeEach ->
- @user = User.build "user-1"
- @user.set("email", "test@example.com")
-
- it "should return the same model", ->
- @newUser = User.findOrBuild "user-1", email: "new@example.com"
- @newUser.should.equal @user
- @newUser.get("email").should.equal "new@example.com"
-
- describe "without an existing model", ->
- it "should return new a model with the correct id", ->
- user = User.findOrBuild("user-2")
- user.id.should.equal "user-2"
-
- afterEach ->
- User.loadedModels = {}
diff --git a/services/web/public/coffee/tour/IdeTour.coffee b/services/web/public/coffee/tour/IdeTour.coffee
deleted file mode 100644
index 0779a3114c..0000000000
--- a/services/web/public/coffee/tour/IdeTour.coffee
+++ /dev/null
@@ -1,92 +0,0 @@
-
-define [
- "libs/intro"
- "utils/Modal"
- "libs/jquery.storage"
-], (introJs, Modal, storage)->
-
- key = "tour.hadIntroduction"
-
- saveTourHasBeenFinished = ->
- $.localStorage(key, true)
-
- getIfTourHasStarted = ->
- return $.localStorage(key)
-
- class IdeTour
- template: $("#editorTourTemplate").html()
-
- constructor: (@ide) ->
- @ide.on "afterJoinProject", () =>
- if !@inited
- @inited = true
- signUpDate = new Date(@ide.user.get("signUpDate")).getTime()
- oneDay = 24 * 60 * 60 * 1000
- yesterday = new Date().getTime() - oneDay
-
- signedUpToday = signUpDate > yesterday
-
- hadIntroduction = getIfTourHasStarted()
-
- if !hadIntroduction? and signedUpToday
- @run()
-
- @$el = $(@template)
- $("#toolbar-footer").append(@$el)
- @$el.on "click", (e) =>
- e.preventDefault()
- @run()
-
- run : ->
- $('#code-tab-li a').click()
-
- intro = introJs()
- intro.oncomplete saveTourHasBeenFinished
- intro.onexit saveTourHasBeenFinished
-
- isSplitView = $('#recompilePdf').is(":visible")
-
- if isSplitView
- pdfStep =
- element: '#pdfToolBar'
- intro: "Compile your project, check logs, download and change editor layout."
- else
- pdfStep =
- element: "li#pdf"
- intro: "Compile and preview your project."
-
- chatIsMinimized = $('.chat-window.minimized').is(":visible")
- if chatIsMinimized
- $('.js-minimize-toggle.minimize-toggle').click()
-
-
- steps = [
- {
- element: '.actions'
- intro: "Welcome to ShareLaTeX!
You can add, upload, rename and delete your documents here."
- },
- pdfStep,
- {
- element: '#history-tab-li'
- intro: "View what has changed in your project."
- position: 'right'
- }, {
- element: '#collaborators-tab-li'
- intro: "Add collaborators and share your project."
- position: 'right'
- }, {
- element: '#settings-tab-li'
- intro: "Change your project settings."
- position: 'right'
- }, {
- element: '.chat-window'
- intro: "Chat to your collaborators."
- position: 'top'
- }
- ]
-
- intro.setOptions(skipLabel: "Skip tour", steps:steps, showStepNumbers:false)
-
- intro.start()
-
-
diff --git a/services/web/public/coffee/track-changes/ChangeListView.coffee b/services/web/public/coffee/track-changes/ChangeListView.coffee
deleted file mode 100644
index 1212faa1fe..0000000000
--- a/services/web/public/coffee/track-changes/ChangeListView.coffee
+++ /dev/null
@@ -1,320 +0,0 @@
-define [
- "moment"
- "libs/mustache"
- "libs/backbone"
-], (moment)->
-
- moment.lang "en", calendar:
- lastDay : '[Yesterday]'
- sameDay : '[Today]'
- nextDay : '[Tomorrow]'
- lastWeek : "ddd, Do MMM YY"
- nextWeek : "ddd, Do MMM YY"
- sameElse : 'ddd, Do MMM YY'
-
- ChangeListView = Backbone.View.extend
- template: $("#changeListTemplate").html()
-
- events:
- "scroll" : () -> @loadUntilFull()
-
- initialize: () ->
- @itemViews = []
- @atEndOfCollection = false
-
- self = this
- @collection.on "add", (model) ->
- self.addItem model
- @collection.on "reset", (collection) ->
- self.addItem model for model in collection.models
-
- @selectedFromIndex = 0
- @selectedToIndex = 0
-
- @render()
- @hideLoading()
-
- render: ->
- @$el.html Mustache.to_html @template
- @$el.css
- overflow: "scroll"
- this
-
- remove: () ->
- @undelegateEvents()
-
- addItem: (model) ->
- index = @collection.indexOf(model)
- previousModel = @collection.models[index - 1]
- view = new ChangeListItemView(model : model, previousModel: previousModel)
- @itemViews.push view
- elementAtIndex = @$(".change-list").children()[index]
- view.$el.insertBefore(elementAtIndex)
-
- view.on "click", (e, v) =>
- if e.shiftKey
- @selectRangeTo(index)
- else
- @setSelectionRange(index, index)
-
- view.on "selected:to", (e, v) =>
- @setSelectionRange(@selectedFromIndex, index)
-
- view.on "selected:from", (e, v) =>
- @setSelectionRange(index, @selectedToIndex)
-
- view.on "mouseenter:to", (e) =>
- @hoverToIndex = index
- @resetHoverStates()
-
- view.on "mouseleave:to", (e) =>
- delete @hoverToIndex
- @resetHoverStates()
-
- view.on "mouseenter:from", (e) =>
- @hoverFromIndex = index
- @resetHoverStates()
-
- view.on "mouseleave:from", (e) =>
- delete @hoverFromIndex
- @resetHoverStates()
-
- view.resetSelector(index, @selectedFromIndex, @selectedToIndex)
-
- setSelectionRange: (fromIndex, toIndex) ->
- @selectedFromIndex = fromIndex
- @selectedToIndex = toIndex
- @resetAllSelectors()
- @triggerChangeDiff()
-
- selectRangeTo: (index) ->
- return unless @selectedFromIndex? and @selectedToIndex?
- if index < @selectedToIndex
- @setSelectionRange(@selectedFromIndex, index)
- else
- @setSelectionRange(index, @selectedToIndex)
-
- resetAllSelectors: () ->
- for view, i in @itemViews
- view.resetSelector(i, @selectedFromIndex, @selectedToIndex)
-
- resetHoverStates: () ->
- if @hoverToIndex? and @hoverToIndex != @selectedToIndex
- @$("ul").addClass("hover-state")
- for view, i in @itemViews
- view.resetHoverState(i, @selectedFromIndex, @hoverToIndex)
- else if @hoverFromIndex? and @hoverFromIndex != @selectedFromIndex
- @$("ul").addClass("hover-state")
- for view, i in @itemViews
- view.resetHoverState(i, @hoverFromIndex, @selectedToIndex)
- else
- @$("ul").removeClass("hover-state")
- for view, i in @itemViews
- view.setHoverUnselected()
-
- triggerChangeDiff: () ->
- @trigger "change_diff", @selectedFromIndex, @selectedToIndex
-
- listShorterThanContainer: ->
- @$el.height() > @$(".change-list").height()
-
- atEndOfListView: ->
- @$el.scrollTop() + @$el.height() >= @$(".change-list").height() - 30
-
- loadUntilFull: (callback = (error) ->) ->
- if (@listShorterThanContainer() or @atEndOfListView()) and not @atEndOfCollection and not @loading
- @showLoading()
- @hideEmptyMessage()
- @collection.fetchNextBatch
- error: (error) =>
- @hideLoading()
- @showEmptyMessageIfCollectionEmpty()
- callback(error)
- success: (collection, response) =>
- @hideLoading()
- if @collection.isAtEnd()
- @atEndOfCollection = true
- @showEmptyMessageIfCollectionEmpty()
- callback()
- else
- @loadUntilFull(callback)
-
- else
- callback() if callback?
-
- showEmptyMessageIfCollectionEmpty: ()->
- if @collection.isEmpty()
- @$(".empty-message").show()
- else
- @$(".empty-message").hide()
-
- hideEmptyMessage: () ->
- @$(".empty-message").hide()
-
- showLoading: ->
- @loading = true
- @$(".loading-changes").show()
-
- hideLoading: ->
- @loading = false
- @$(".loading-changes").hide()
-
- ChangeListItemView = Backbone.View.extend
- tagName: "li"
-
- events:
- "click .change-description" : "onClick"
- "click .change-selector-from" : "onFromSelectorClick"
- "click .change-selector-to" : "onToSelectorClick"
- "mouseenter .change-selector-to": (args...) ->
- @trigger "mouseenter:to", args...
- "mouseleave .change-selector-to": (args...) ->
- @trigger "mouseleave:to", args...
- "mouseenter .change-selector-from": (args...) ->
- @trigger "mouseenter:from", args...
- "mouseleave .change-selector-from": (args...) ->
- @trigger "mouseleave:from", args...
-
- templates:
- item: $("#changeListItemTemplate").html()
- user: $("#changeListItemUserTemplate").html()
-
- initialize: ->
- @render()
-
- render: ->
- userHtml = for user in @model.get("users")
- Mustache.to_html @templates.user, {
- hue: user.hue()
- name: user.name()
- }
- docNames = []
- for doc in @model.get("docs")
- if doc.entity?
- docNames.push doc.entity.get("name")
- else
- docNames.push "deleted"
- data = {
- day: moment(parseInt(@model.get("end_ts"), 10)).calendar()
- time: moment(parseInt(@model.get("end_ts"), 10)).format("h:mm a")
- users: userHtml.join("")
- docs: docNames.join(", ")
- }
-
- @$el.html Mustache.to_html(@templates.item, data)
-
- if @options.previousModel?
- prevDate = @options.previousModel.get("end_ts")
- date = @model.get("end_ts")
- if not moment(prevDate).isSame(date, "day")
- @$el.addClass("first-in-day")
- else
- @$el.addClass("first-in-day")
-
- @$(".change-selector-from").tooltip({
- title: "Show back to this change",
- placement: "left",
- animation: false
- })
- @$(".change-selector-to").tooltip({
- title: "Show up to this change",
- placement: "left",
- animation: false
- })
-
- return this
-
- onClick: (e) ->
- e.preventDefault()
- @trigger "click", e, @
-
- onToSelectorClick: (e) ->
- @trigger "selected:to", e, @
-
- onFromSelectorClick: (e) ->
- @trigger "selected:from", e, @
-
- isSelectedFrom: () ->
- @$(".change-selector-from").is(":checked")
-
- isSelectedTo: () ->
- @$(".change-selector-to").is(":checked")
-
- hideFromSelector: () ->
- @$(".change-selector-from").hide()
-
- showFromSelector: () ->
- @$(".change-selector-from").show()
-
- hideToSelector: () ->
- @$(".change-selector-to").hide()
-
- showToSelector: () ->
- @$(".change-selector-to").show()
-
- setFromChecked: (checked) ->
- @$(".change-selector-from").prop("checked", checked)
-
- setToChecked: (checked) ->
- @$(".change-selector-to").prop("checked", checked)
-
- setSelected: (first, last) ->
- @$el.addClass("selected-change")
- if first
- @$el.addClass("selected-change-to")
- else
- @$el.removeClass("selected-change-to")
- if last
- @$el.addClass("selected-change-from")
- else
- @$el.removeClass("selected-change-from")
-
- setUnselected: () ->
- @$el.removeClass("selected-change-to")
- @$el.removeClass("selected-change-from")
- @$el.removeClass("selected-change")
-
- setHoverSelected: (first, last) ->
- @$el.addClass("hover-selected")
- if first
- @$el.addClass("hover-selected-to")
- else
- @$el.removeClass("hover-selected-to")
- if last
- @$el.addClass("hover-selected-from")
- else
- @$el.removeClass("hover-selected-from")
-
- setHoverUnselected: () ->
- @$el.removeClass("hover-selected-to")
- @$el.removeClass("hover-selected-from")
- @$el.removeClass("hover-selected")
-
- resetSelector: (myIndex, selectedFromIndex, selectedToIndex) ->
- if myIndex >= selectedToIndex
- @showFromSelector()
- else
- @hideFromSelector()
-
- if myIndex <= selectedFromIndex
- @showToSelector()
- else
- @hideToSelector()
-
- if selectedToIndex <= myIndex <= selectedFromIndex
- @setSelected(selectedToIndex == myIndex, selectedFromIndex == myIndex)
- else
- @setUnselected()
-
- @setFromChecked(myIndex == selectedFromIndex)
- @setToChecked(myIndex == selectedToIndex)
-
- resetHoverState: (myIndex, hoverFromIndex, hoverToIndex) ->
- if hoverToIndex <= myIndex <= hoverFromIndex
- @setHoverSelected(hoverToIndex == myIndex, hoverFromIndex == myIndex)
- else
- @setHoverUnselected()
-
-
- return ChangeListView
-
diff --git a/services/web/public/coffee/track-changes/DiffView.coffee b/services/web/public/coffee/track-changes/DiffView.coffee
deleted file mode 100644
index b5c3269ce4..0000000000
--- a/services/web/public/coffee/track-changes/DiffView.coffee
+++ /dev/null
@@ -1,285 +0,0 @@
-define [
- "ace/ace"
- "ace/mode/latex"
- "ace/range"
- "moment"
- "libs/backbone"
- "libs/mustache"
-], (Ace, LatexMode, Range, moment)->
- DiffView = Backbone.View.extend
- template: $("#trackChangesDiffTemplate").html()
-
- events:
- "click .restore": (e) ->
- e.preventDefault()
- @trigger "restore"
- "click .restore-deleted": (e) ->
- e.preventDefault()
- @$("a.restore-deleted").attr("disabled", true)
- @$("a.restore-deleted").text("Restoring...")
- @trigger "restore-deleted"
-
- initialize: () ->
- if !@model.get("doc").get("deleted")
- @model.on "change:diff", () => @render()
- @model.fetch()
- else
- @render()
-
- render: ->
- changes = @getNumberOfChanges()
- html = Mustache.to_html @template, {
- changes: "#{changes} change#{if changes == 1 then "" else "s"}"
- name: @model.get("doc")?.get("name")
- }
- @$el.html(html)
-
- if @model.get("doc").get("deleted")
- @$(".change-info").hide()
- @$(".deleted-info").show()
- else
- diff = @model.get("diff")
- return unless diff?
-
- if !@model.get("from")? or !@model.get("to")? or changes == 0
- @$(".restore").hide()
-
- @createAceEditor()
- @aceEditor.setValue(@getPlainDiffContent())
- @aceEditor.clearSelection()
- @$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
- @insertMarkers()
- @insertNameTag()
- @insertMoreChangeLabels()
- @bindToScrollEvents()
- @scrollToFirstChange()
-
- return @
-
- remove: () ->
- @$editor?.remove()
- @undelegateEvents()
-
- createAceEditor: () ->
- @$editor = @$(".track-changes-diff-editor")
- @$el.append(@$editor)
- @aceEditor = Ace.edit(@$editor[0])
- @aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
- @aceEditor.setReadOnly true
- @aceEditor.setShowPrintMargin(false)
- session = @aceEditor.getSession()
- session.setMode(new LatexMode.Mode())
- session.setUseWrapMode(true)
-
- @aceEditor.on "mousemove", (e) =>
- position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
- e.position = position
- @updateVisibleNames(e)
-
- bindToScrollEvents: () ->
- @aceEditor.getSession().on "changeScrollTop", (e) =>
- @updateMoreChangeLabels()
-
- getPlainDiffContent: () ->
- content = ""
- for entry in @model.get("diff") or []
- content += entry.u or entry.i or entry.d or ""
- return content
-
- getNumberOfChanges: () ->
- changes = 0
- for entry in @model.get("diff") or []
- changes += 1 if entry.i? or entry.d?
- return changes
-
- insertMarkers: () ->
- row = 0
- column = 0
- @entries = []
- for entry, i in @model.get("diff") or []
- content = entry.u or entry.i or entry.d
- content ||= ""
- lines = content.split("\n")
- startRow = row
- startColumn = column
- if lines.length > 1
- endRow = startRow + lines.length - 1
- endColumn = lines[lines.length - 1].length
- else
- endRow = startRow
- endColumn = startColumn + lines[0].length
- row = endRow
- column = endColumn
-
- range = new Range.Range(
- startRow, startColumn, endRow, endColumn
- )
- entry.range = range
- @addMarker(range, entry)
- if entry.i? or entry.d?
- @entries.push entry
-
- addMarker: (range, entry) ->
- session = @aceEditor.getSession()
- markerBackLayer = @aceEditor.renderer.$markerBack
- markerFrontLayer = @aceEditor.renderer.$markerFront
- lineHeight = @aceEditor.renderer.lineHeight
-
- dark = @_isDarkTheme()
-
- if entry.i? or entry.d?
- hue = entry.meta.user.hue()
- if entry.i?
- if dark
- style = "background-color : hsl(#{hue}, 100%, 28%);"
- else
- style = "background-color : hsl(#{hue}, 70%, 85%);"
- @_addMarkerWithCustomStyle session, markerBackLayer, range, "inserted-change-background", false, style
- if entry.d?
- if dark
- bgStyle = "background-color: hsl(#{hue}, 100%, 20%);"
- fgStyle = "border-bottom: 2px solid hsl(#{hue}, 100%, 60%);"
- else
- bgStyle = "background-color: hsl(#{hue}, 70%, 95%);"
- fgStyle = "border-bottom: 2px solid hsl(#{hue}, 70%, 40%);"
- fgStyle += "; height: #{Math.round(lineHeight/2) - 1}px;"
- @_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, bgStyle
- @_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-foreground", true, fgStyle
-
- _addMarkerWithCustomStyle: (session, markerLayer, range, klass, foreground, style) ->
- session.addMarker range, klass, (html, range, left, top, config) ->
- if range.isMultiLine()
- markerLayer.drawTextMarker(html, range, klass, config, style)
- else
- markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, style)
- , foreground
-
- _isDarkTheme: () ->
- rgb = $(".ace_editor").css("background-color");
- [m, r, g, b] = rgb.match(/rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)/)
- r = parseInt(r, 10)
- g = parseInt(g, 10)
- b = parseInt(b, 10)
- return r + g + b < 3 * 128
-
- insertNameTag: () ->
- @$nameTagEl = $("")
- @$nameTagEl.css({
- position: "absolute"
- })
- @$nameTagEl.hide()
- @$ace.append(@$nameTagEl)
-
- insertMoreChangeLabels: () ->
- @$changesBefore = $(" ")
- @$changesAfter = $(" ")
- @$ace.append(@$changesBefore)
- @$ace.append(@$changesAfter)
- @$changesBefore.on "click", () =>
- @gotoLastHiddenChangeBefore()
- @$changesAfter.on "click", () =>
- @gotoFirstHiddenChangeAfter()
- @updateMoreChangeLabels()
-
- scrollToFirstChange: () ->
- @aceEditor.scrollToLine(0)
- setTimeout () =>
- if @entries? and @entries[0]?
- row = @entries[0].range.start.row
- @aceEditor.scrollToLine(row, true, false)
- , 10
-
- _drawNameTag: (entry, position) ->
- @$nameTagEl.show()
-
- if entry.i?
- text = "Added by #{entry.meta.user.name()}"
- else if entry.d?
- text = "Deleted by #{entry.meta.user.name()}"
- date = moment(parseInt(entry.meta.end_ts, 10)).format("Do MMM YYYY, h:mm a")
- text += " on #{date}"
- @$nameTagEl.text(text)
-
- position = @aceEditor.renderer.textToScreenCoordinates(position.row, position.column)
- offset = @$ace.offset()
- position.pageX = position.pageX - offset.left
- position.pageY = position.pageY - offset.top
- height = @$ace.height()
-
- hue = entry.meta.user.hue()
- if @_isDarkTheme()
- css = { "background-color" : "hsl(#{hue}, 100%, 20%)"; }
- else
- css = { "background-color" : "hsl(#{hue}, 70%, 90%)"; }
-
- if position.pageX + @$nameTagEl.width() < @$ace.width()
- css["left"] = position.pageX
- css["right"] = "auto"
- else
- css["right"] = 0
- css["left"] = "auto"
-
- if position.pageY > 2 * @$nameTagEl.height()
- css["bottom"] = height - position.pageY
- css["top"] = "auto"
- else
- css["top"] = position.pageY + @aceEditor.renderer.lineHeight
- css["bottom"] = "auto"
-
- @$nameTagEl.css css
-
- _hideNameTag: () ->
- @$nameTagEl?.hide()
-
- updateVisibleNames: (e) ->
- visibleName = false
- for entry in @entries or []
- if entry.range.contains(e.position.row, e.position.column)
- @_drawNameTag(entry, e.position)
- visibleName = true
- break
- if !visibleName
- @_hideNameTag()
-
- updateMoreChangeLabels: () ->
- return if !@$changesBefore or !@$changesAfter
- setTimeout () =>
- firstRow = @aceEditor.getFirstVisibleRow()
- lastRow = @aceEditor.getLastVisibleRow()
- changesBefore = 0
- changesAfter = 0
- @lastHiddenChangeBefore = null
- @firstHiddenChangeAfter = null
- for entry in @entries or []
- if entry.range.start.row < firstRow
- changesBefore += 1
- @lastHiddenChangeBefore = entry
- if entry.range.end.row > lastRow
- changesAfter += 1
- @firstHiddenChangeAfter ||= entry
-
- if changesBefore > 0
- @$changesBefore.find("span").text("#{changesBefore} more change#{if changesBefore > 1 then "s" else ""} above")
- @$changesBefore.show()
- else
- @$changesBefore.hide()
- if changesAfter > 0
- @$changesAfter.find("span").text("#{changesAfter} more change#{if changesAfter > 1 then "s" else ""} below")
- @$changesAfter.show()
- else
- @$changesAfter.hide()
- , 100
-
- gotoLastHiddenChangeBefore: () ->
- return if !@lastHiddenChangeBefore
- @aceEditor.scrollToLine(@lastHiddenChangeBefore.range.start.row, true, false)
-
- gotoFirstHiddenChangeAfter: () ->
- return if !@firstHiddenChangeAfter
- @aceEditor.scrollToLine(@firstHiddenChangeAfter.range.end.row, true, false)
-
- resize: () ->
- @aceEditor?.resize()
-
- return DiffView
-
diff --git a/services/web/public/coffee/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/track-changes/TrackChangesManager.coffee
deleted file mode 100644
index dfa6d52ead..0000000000
--- a/services/web/public/coffee/track-changes/TrackChangesManager.coffee
+++ /dev/null
@@ -1,264 +0,0 @@
-define [
- "track-changes/models/ChangeList"
- "track-changes/models/Diff"
- "track-changes/ChangeListView"
- "track-changes/DiffView"
- "account/AccountManager"
- "utils/Modal"
- "models/Doc"
- "moment"
-], (ChangeList, Diff, ChangeListView, DiffView, AccountManager, Modal, Doc, moment) ->
- class TrackChangesManager
- template: $("#trackChangesPanelTemplate").html()
-
- constructor: (@ide) ->
- @project_id = window.userSettings.project_id
- @$el = $(@template)
- @ide.mainAreaManager.addArea
- identifier: "trackChanges"
- element: @$el
-
- @ide.tabManager.addTab
- id: "history"
- name: "History"
- show: "code"
- after: "code"
- contract: true
- onShown: () => @show()
- onHidden: () => @hide()
-
- @ide.editor.on "resize", () =>
- @diffView?.resize()
-
- @$el.find(".track-changes-close").on "click", (e) =>
- e.preventDefault
- @hide()
-
- @bindToFileTreeEvents()
-
- @disable()
-
- bindToFileTreeEvents: () ->
- @ide.fileTreeManager.on "open:doc", (doc_id) =>
- @doc_id = doc_id
- if @enabled
- @updateDiff()
-
- AB_BUCKETS: ["control", "one-week", "pop-up"]
- show: () ->
- @changes = new ChangeList([], project_id: @project_id, ide: @ide)
-
- if @changeListView?
- @changeListView.remove()
- @changeListView = new ChangeListView(
- el: @$el.find(".change-list-area")
- collection: @changes
- )
- @changeListView.render()
- @changeListView.loadUntilFull (error) =>
- @autoSelectDiff()
-
- @changeListView.on "change_diff", (fromIndex, toIndex) =>
- @findDocsInChange(fromIndex, toIndex)
- @updateLabels()
- @updateDiff()
-
- @showUpgradeView()
-
- if @diffView?
- @diffView.remove()
-
- @ide.mainAreaManager.change "trackChanges"
- @ide.editor.disable()
- @ide.fileViewManager.disable()
-
- @ide.fileTreeManager.makeReadOnly()
- @ide.fileTreeManager.showDeletedDocs()
-
- @enable()
-
- showUpgradeView: () ->
- @$el.find("button.start-free-trial").off "click.track-changes"
- @$el.find("button.start-free-trial").on "click.track-changes", () => @gotoFreeTrial()
-
- if !@ide.project.get("features").versioning
- ga('send', 'event', 'subscription-funnel', 'askToUgrade', "trackchanges")
- @$el.find(".track-changes-upgrade-popup").show()
-
- if @ide.project.get("owner") == @ide.user
- @$el.find(".show-when-not-owner").hide()
- else
- @$el.find(".show-when-owner").hide()
-
- hide: () ->
- @ide.editor.enable()
- @ide.fileViewManager.enable()
- @disable()
-
- doc = @ide.fileTreeManager.getEntity(@doc_id, include_deleted: true)
- if doc? and doc.get("deleted")
- @ide.fileTreeManager.openDoc(@ide.project.get("rootDoc_id"))
- else
- @ide.fileTreeManager.openDoc(@doc_id)
-
- @ide.tabManager.show "code"
- @resetLabels()
- @ide.fileTreeManager.makeReadWriteIfAllowed()
- @ide.fileTreeManager.hideDeletedDocs()
-
- autoSelectDiff: () ->
- if @changes.models.length == 0
- return
-
- # Find all change until the last one we made
- fromIndex = null
- for change, i in @changes.models
- if ide.user in change.get("users")
- if i > 0
- fromIndex = i - 1
- else
- fromIndex = 0
- break
- fromIndex = 0 if !fromIndex
-
- toChange = @changes.models[0]
- fromChange = @changes.models[fromIndex]
- @changeListView.setSelectionRange(fromIndex, 0)
- @updateDiff()
-
- findDocsInChange: (fromIndex, toIndex) ->
- @changed_doc_ids = []
- for change in @changes.models.slice(toIndex, fromIndex + 1)
- for doc in change.get("docs") or []
- @changed_doc_ids.push doc.id if doc.id not in @changed_doc_ids
-
- if !@doc_id? or @doc_id not in @changed_doc_ids
- @doc_id = @changed_doc_ids[0]
-
- @updateDiff()
-
- updateLabels: () ->
- labels = {}
- for doc_id in @changed_doc_ids
- labels[doc_id] = true
- @ide.fileTreeManager.setLabels(labels)
-
- resetLabels: () ->
- @ide.fileTreeManager.setLabels({})
-
-
- updateDiff: () ->
- fromIndex = @changeListView.selectedFromIndex
- toIndex = @changeListView.selectedToIndex
-
- if !toIndex? or !fromIndex?
- console.log "No selection - what should we do!?"
- return
-
- {from, to, start_ts, end_ts} = @_findDocVersionsRangeInSelection(@doc_id, fromIndex, toIndex)
-
- @diff = new Diff({
- project_id: @project_id
- doc_id: @doc_id
- from: from
- to: to
- start_ts: start_ts
- end_ts: end_ts
- }, {
- ide: @ide
- })
-
- if @diffView?
- @diffView.remove()
-
- if !@diff.get("doc")?
- console.log "This document does not exist. What should we do?"
- return
-
- @diffView = new DiffView(
- model: @diff
- el: @$el.find(".track-changes-diff")
- )
-
- @diffView.on "restore", () =>
- @restoreDiff(@diff)
-
- @diffView.on "restore-deleted", () =>
- @restoreDeletedDoc @diff.get("doc"), (error, doc_id) =>
- return if error? or !doc_id?
- setTimeout () =>
- # Give doc a chance to appear in file tree via socket.io
- @hide()
- @ide.fileTreeManager.openDoc(doc_id)
- , 1000
-
- @ide.fileTreeManager.selectEntity(@doc_id)
-
-
- _findDocVersionsRangeInSelection: (doc_id, fromIndex, toIndex) ->
- from = to = start_ts = end_ts = null
-
- for change in @changes.models.slice(toIndex, fromIndex + 1)
- for doc in change.get("docs")
- if doc.id == doc_id
- if from? and to?
- from = Math.min(from, doc.fromV)
- to = Math.max(to, doc.toV)
- start_ts = Math.min(start_ts, change.get("start_ts"))
- end_ts = Math.max(end_ts, change.get("end_ts"))
- else
- from = doc.fromV
- to = doc.toV
- start_ts = change.get("start_ts")
- end_ts = change.get("end_ts")
- break
-
- return {from, to, start_ts, end_ts}
-
- restoreDiff: (diff) ->
- name = diff.get("doc")?.get("name")
- date = moment(diff.get("start_ts")).format("Do MMM YYYY, h:mm:ss a")
- modal = new Modal({
- title: "Restore document"
- message: "Are you sure you want to restore #{name} to before the changes on #{date}?"
- buttons: [{
- text: "Cancel"
- }, {
- text: "Restore"
- class: "btn-success"
- close: false
- callback: ($button) =>
- $button.text("Restoring...")
- $button.prop("disabled", true)
- diff.restore (error) =>
- modal.remove()
- @hide()
- }]
- })
-
- restoreDeletedDoc: (doc, callback) ->
- $.ajax {
- url: "/project/#{@project_id}/doc/#{doc.get("id")}/restore"
- type: "POST"
- dataType: "json"
- data:
- name: doc.get("name")
- headers:
- "X-CSRF-Token": window.csrfToken
- success: (body, status, response) ->
- callback(null, body?.doc_id)
- error: (error) ->
- callback(error)
- }
-
- enable: () ->
- @enabled = true
-
- disable: () ->
- @enabled = false
-
- gotoFreeTrial: () ->
- AccountManager.gotoSubscriptionsPage()
- ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "trackchanges")
-
- return TrackChangesManager
diff --git a/services/web/public/coffee/track-changes/models/Change.coffee b/services/web/public/coffee/track-changes/models/Change.coffee
deleted file mode 100644
index ac48997583..0000000000
--- a/services/web/public/coffee/track-changes/models/Change.coffee
+++ /dev/null
@@ -1,26 +0,0 @@
-define [
- "models/User"
- "libs/backbone"
-], (User)->
- Change = Backbone.Model.extend
- parse: (change) ->
- model = {
- start_ts: change.meta.start_ts
- end_ts: change.meta.end_ts
- }
- model.users = []
- for user in change.meta.users or []
- model.users.push User.findOrBuild(user.id, user)
- if model.users.length == 0
- model.users.push User.getAnonymousUser()
- model.docs = []
- for doc_id, data of change.docs
- model.docs.push
- id: doc_id
- fromV: data.fromV
- toV: data.toV
- # TODO: We should not use a global reference here, but
- # it's hard to get @ide into Backbone at this point.
- entity: ide.fileTreeManager.getEntity(doc_id, include_deleted: true)
-
- return model
\ No newline at end of file
diff --git a/services/web/public/coffee/track-changes/models/ChangeList.coffee b/services/web/public/coffee/track-changes/models/ChangeList.coffee
deleted file mode 100644
index 82fd84fef7..0000000000
--- a/services/web/public/coffee/track-changes/models/ChangeList.coffee
+++ /dev/null
@@ -1,33 +0,0 @@
-define [
- "track-changes/models/Change"
- "libs/backbone"
-], (Change)->
- ChangeList = Backbone.Collection.extend
- model: Change
- batchSize: 10
-
- initialize: (models, @options) ->
- @ide = @options.ide
- @atEnd = false
-
- url: () ->
- url = "/project/#{@options.project_id}/updates?min_count=#{@batchSize}"
- if @nextBeforeTimestamp?
- url += "&before=#{@nextBeforeTimestamp}"
- return url
-
- isAtEnd: () -> @atEnd
-
- parse: (json) ->
- @nextBeforeTimestamp = json.nextBeforeTimestamp
- @atEnd = !@nextBeforeTimestamp
- return json.updates
-
- fetchNextBatch: (options = {}) ->
- if @isAtEnd()
- options.success?(@)
- return
- options.add = true
- @fetch options
-
-
diff --git a/services/web/public/coffee/track-changes/models/Diff.coffee b/services/web/public/coffee/track-changes/models/Diff.coffee
deleted file mode 100644
index e0c108ac66..0000000000
--- a/services/web/public/coffee/track-changes/models/Diff.coffee
+++ /dev/null
@@ -1,35 +0,0 @@
-define [
- "models/User"
- "libs/backbone"
-], (User) ->
- Diff = Backbone.Model.extend
- initialize: (attributes, options) ->
- @ide = options.ide
- @set "doc", @ide.fileTreeManager.getEntity(@get("doc_id"), include_deleted: true)
-
- url: () ->
- url = "/project/#{@get("project_id")}/doc/#{@get("doc_id")}/diff"
- if @get("from")? and @get("to")?
- url += "?from=#{@get("from")}&to=#{@get("to")}"
- return url
-
- parse: (diff) ->
- for entry in diff.diff
- if entry.meta?
- if entry.meta.user?
- entry.meta.user = User.findOrBuild(entry.meta.user.id, entry.meta.user)
- else
- entry.meta.user = User.getAnonymousUser()
- return diff
-
- restore: (callback = (error) ->) ->
- $.ajax {
- url: "/project/#{@get("project_id")}/doc/#{@get("doc_id")}/version/#{@get("from")}/restore"
- type: "POST"
- headers:
- "X-CSRF-Token": window.csrfToken
- success: () ->
- callback()
- error: (error) ->
- callback(error)
- }
diff --git a/services/web/public/coffee/undo/UndoManager.coffee b/services/web/public/coffee/undo/UndoManager.coffee
deleted file mode 100644
index db1c9f0880..0000000000
--- a/services/web/public/coffee/undo/UndoManager.coffee
+++ /dev/null
@@ -1,358 +0,0 @@
-define [
- "ace/range"
- "ace/edit_session"
- "ace/document"
-], () ->
- Range = require("ace/range").Range
- EditSession = require("ace/edit_session").EditSession
- Doc = require("ace/document").Document
-
- class UndoManager
- constructor: (@manager) ->
- @reset()
- @nextUpdateIsRemote = false
-
- reset: () ->
- @undoStack = []
- @redoStack = []
-
- execute: (options) ->
- aceDeltaSets = options.args[0]
- @session = options.args[1]
- return if !aceDeltaSets?
-
- lines = @session.getDocument().getAllLines()
- linesBeforeChange = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, lines)
- simpleDeltaSets = @_aceDeltaSetsToSimpleDeltaSets(aceDeltaSets, linesBeforeChange)
- @undoStack.push(
- deltaSets: simpleDeltaSets
- remote: @nextUpdateIsRemote
- )
- @redoStack = []
- @nextUpdateIsRemote = false
-
- undo: (dontSelect) ->
- localUpdatesMade = @_shiftLocalChangeToTopOfUndoStack()
- return if !localUpdatesMade
-
- update = @undoStack.pop()
- return if !update?
-
- if update.remote
- @manager.showUndoConflictWarning()
-
- lines = @session.getDocument().getAllLines()
- linesBeforeDelta = @_revertSimpleDeltaSetsOnDocLines(update.deltaSets, lines)
- deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, linesBeforeDelta)
- selectionRange = @session.undoChanges(deltaSets, dontSelect)
- @redoStack.push(update)
- return selectionRange
-
- redo: (dontSelect) ->
- update = @redoStack.pop()
- return if !update?
- lines = @session.getDocument().getAllLines()
- deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, lines)
- selectionRange = @session.redoChanges(deltaSets, dontSelect)
- @undoStack.push(update)
- return selectionRange
-
- _shiftLocalChangeToTopOfUndoStack: () ->
- head = []
- localChangeExists = false
- while @undoStack.length > 0
- update = @undoStack.pop()
- head.unshift update
- if !update.remote
- localChangeExists = true
- break
-
- if !localChangeExists
- @undoStack = @undoStack.concat head
- return false
- else
- # Undo stack looks like undoStack ++ reorderedhead ++ head
- # Reordered head starts of empty and consumes entries from head
- # while keeping the localChange at the top for as long as it can
- localChange = head.shift()
- reorderedHead = [localChange]
- while head.length > 0
- remoteChange = head.shift()
- localChange = reorderedHead.pop()
- result = @_swapSimpleDeltaSetsOrder(localChange.deltaSets, remoteChange.deltaSets)
- if result?
- remoteChange.deltaSets = result[0]
- localChange.deltaSets = result[1]
- reorderedHead.push remoteChange
- reorderedHead.push localChange
- else
- reorderedHead.push localChange
- reorderedHead.push remoteChange
- break
- @undoStack = @undoStack.concat(reorderedHead).concat(head)
- return true
-
-
- _swapSimpleDeltaSetsOrder: (firstDeltaSets, secondDeltaSets) ->
- newFirstDeltaSets = @_copyDeltaSets(firstDeltaSets)
- newSecondDeltaSets = @_copyDeltaSets(secondDeltaSets)
- for firstDeltaSet in newFirstDeltaSets.slice(0).reverse()
- for firstDelta in firstDeltaSet.deltas.slice(0).reverse()
- for secondDeltaSet in newSecondDeltaSets
- for secondDelta in secondDeltaSet.deltas
- success = @_swapSimpleDeltaOrderInPlace(firstDelta, secondDelta)
- return null if !success
- return [newSecondDeltaSets, newFirstDeltaSets]
-
- _copyDeltaSets: (deltaSets) ->
- newDeltaSets = []
- for deltaSet in deltaSets
- newDeltaSet =
- deltas: []
- group: deltaSet.group
- newDeltaSets.push newDeltaSet
- for delta in deltaSet.deltas
- newDelta =
- position: delta.position
- newDelta.insert = delta.insert if delta.insert?
- newDelta.remove = delta.remove if delta.remove?
- newDeltaSet.deltas.push newDelta
- return newDeltaSets
-
- _swapSimpleDeltaOrderInPlace: (firstDelta, secondDelta) ->
- result = @_swapSimpleDeltaOrder(firstDelta, secondDelta)
- return false if !result?
- firstDelta.position = result[1].position
- secondDelta.position = result[0].position
- return true
-
- _swapSimpleDeltaOrder: (firstDelta, secondDelta) ->
- if firstDelta.insert? and secondDelta.insert?
- if secondDelta.position >= firstDelta.position + firstDelta.insert.length
- secondDelta.position -= firstDelta.insert.length
- return [secondDelta, firstDelta]
- else if secondDelta.position > firstDelta.position
- return null
- else
- firstDelta.position += secondDelta.insert.length
- return [secondDelta, firstDelta]
- else if firstDelta.remove? and secondDelta.remove?
- if secondDelta.position >= firstDelta.position
- secondDelta.position += firstDelta.remove.length
- return [secondDelta, firstDelta]
- else if secondDelta.position + secondDelta.remove.length > firstDelta.position
- return null
- else
- firstDelta.position -= secondDelta.remove.length
- return [secondDelta, firstDelta]
- else if firstDelta.insert? and secondDelta.remove?
- if secondDelta.position >= firstDelta.position + firstDelta.insert.length
- secondDelta.position -= firstDelta.insert.length
- return [secondDelta, firstDelta]
- else if secondDelta.position + secondDelta.remove.length > firstDelta.position
- return null
- else
- firstDelta.position -= secondDelta.remove.length
- return [secondDelta, firstDelta]
- else if firstDelta.remove? and secondDelta.insert?
- if secondDelta.position >= firstDelta.position
- secondDelta.position += firstDelta.remove.length
- return [secondDelta, firstDelta]
- else
- firstDelta.position += secondDelta.insert.length
- return [secondDelta, firstDelta]
- else
- throw "Unknown delta types"
-
- _applyAceDeltasToDocLines: (deltas, docLines) ->
- doc = new Doc(docLines.join("\n"))
- doc.applyDeltas(deltas)
- return doc.getAllLines()
-
- _revertAceDeltaSetsOnDocLines: (deltaSets, docLines) ->
- session = new EditSession(docLines.join("\n"))
- session.undoChanges(deltaSets)
- return session.getDocument().getAllLines()
-
- _revertSimpleDeltaSetsOnDocLines: (deltaSets, docLines) ->
- doc = docLines.join("\n")
- for deltaSet in deltaSets.slice(0).reverse()
- for delta in deltaSet.deltas.slice(0).reverse()
- if delta.remove?
- doc = doc.slice(0, delta.position) + delta.remove + doc.slice(delta.position)
- else if delta.insert?
- doc = doc.slice(0, delta.position) + doc.slice(delta.position + delta.insert.length)
- else
- throw "Unknown delta type"
- return doc.split("\n")
-
- _aceDeltaSetsToSimpleDeltaSets: (aceDeltaSets, docLines) ->
- for deltaSet in aceDeltaSets
- simpleDeltas = []
- for delta in deltaSet.deltas
- simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
- docLines = @_applyAceDeltasToDocLines([delta], docLines)
- {
- deltas: simpleDeltas
- group: deltaSet.group
- }
-
- _simpleDeltaSetsToAceDeltaSets: (simpleDeltaSets, docLines) ->
- for deltaSet in simpleDeltaSets
- aceDeltas = []
- for delta in deltaSet.deltas
- newAceDeltas = @_simpleDeltaToAceDeltas(delta, docLines)
- docLines = @_applyAceDeltasToDocLines(newAceDeltas, docLines)
- aceDeltas = aceDeltas.concat newAceDeltas
- {
- deltas: aceDeltas
- group: deltaSet.group
- }
-
- _aceDeltaToSimpleDelta: (aceDelta, docLines) ->
- start = aceDelta.range.start
- linesBefore = docLines.slice(0, start.row)
- position =
- linesBefore.join("").length + # full lines
- linesBefore.length + # new line characters
- start.column # partial line
- switch aceDelta.action
- when "insertText"
- return {
- position: position
- insert: aceDelta.text
- }
- when "insertLines"
- return {
- position: position
- insert: aceDelta.lines.join("\n") + "\n"
- }
- when "removeText"
- return {
- position: position
- remove: aceDelta.text
- }
- when "removeLines"
- return {
- position: position
- remove: aceDelta.lines.join("\n") + "\n"
- }
- else
- throw "Unknown Ace action: #{aceDelta.action}"
-
- _simplePositionToAcePosition: (position, docLines) ->
- column = 0
- row = 0
- for line in docLines
- if position > line.length
- row++
- position -= (line + "\n").length
- else
- column = position
- break
- return {row: row, column: column}
-
- _textToAceActions: (simpleText, row, column, type) ->
- aceDeltas = []
- lines = simpleText.split("\n")
-
- range = (options) -> new Range(options.start.row, options.start.column, options.end.row, options.end.column)
-
- do stripFirstLine = () ->
- firstLine = lines.shift()
- if firstLine.length > 0
- aceDeltas.push {
- text: firstLine
- range: range(
- start: column: column, row: row
- end: column: column + firstLine.length, row: row
- )
- action: "#{type}Text"
- }
- column += firstLine.length
-
- do stripFirstNewLine = () ->
- if lines.length > 0
- aceDeltas.push {
- text: "\n"
- range: range(
- start: column: column, row: row
- end: column: 0, row: row + 1
- )
- action: "#{type}Text"
- }
- row += 1
-
- do stripMiddleFullLines = () ->
- middleLines = lines.slice(0, -1)
- if middleLines.length > 0
- aceDeltas.push {
- lines: middleLines
- range: range(
- start: column: 0, row: row
- end: column: 0, row: row + middleLines.length
- )
- action: "#{type}Lines"
- }
- row += middleLines.length
-
- do stripLastLine = () ->
- if lines.length > 0
- lastLine = lines.pop()
- aceDeltas.push {
- text: lastLine
- range: range(
- start: column: 0, row: row
- end: column: lastLine.length , row: row
- )
- action: "#{type}Text"
- }
-
- return aceDeltas
-
-
- _simpleDeltaToAceDeltas: (simpleDelta, docLines) ->
- {row, column} = @_simplePositionToAcePosition(simpleDelta.position, docLines)
-
- if simpleDelta.insert?
- return @_textToAceActions(simpleDelta.insert, row, column, "insert")
- if simpleDelta.remove?
- return @_textToAceActions(simpleDelta.remove, row, column, "remove").reverse()
- else
- throw "Unknown simple delta: #{simpleDelta}"
-
- _concatSimpleDeltas: (deltas) ->
- return [] if deltas.length == 0
-
- concattedDeltas = []
- previousDelta = deltas.shift()
- for delta in deltas
- if delta.insert? and previousDelta.insert?
- if previousDelta.position + previousDelta.insert.length == delta.position
- previousDelta =
- insert: previousDelta.insert + delta.insert
- position: previousDelta.position
- else
- concattedDeltas.push previousDelta
- previousDelta = delta
-
- else if delta.remove? and previousDelta.remove?
- if previousDelta.position == delta.position
- previousDelta =
- remove: previousDelta.remove + delta.remove
- position: delta.position
- else
- concattedDeltas.push previousDelta
- previousDelta = delta
- else
- concattedDeltas.push previousDelta
- previousDelta = delta
- concattedDeltas.push previousDelta
-
-
- return concattedDeltas
-
-
- hasUndo: () -> @undoStack.length > 0
- hasRedo: () -> @redoStack.length > 0
-
diff --git a/services/web/public/coffee/utils/ContextMenu.coffee b/services/web/public/coffee/utils/ContextMenu.coffee
deleted file mode 100644
index 10a62aabe2..0000000000
--- a/services/web/public/coffee/utils/ContextMenu.coffee
+++ /dev/null
@@ -1,62 +0,0 @@
-define [
- "libs/backbone"
- "libs/mustache"
-], () ->
- ContextMenuEntry = Backbone.View.extend
- template: $("#contextMenuEntryTemplate").html()
-
- events:
- "click a" : "onClick"
-
- render: () ->
- @setElement($(Mustache.to_html(@template, @options)))
- return @
-
- onClick: (e) ->
- e.preventDefault()
- if @options.onClick
- @options.onClick()
-
- ContextMenu = Backbone.View.extend
- templates:
- menu: $("#contextMenuTemplate").html()
- divider: $("#contextMenuDividerTemplate").html()
-
- initialize: (position, entries) ->
- if ContextMenu.currentMenu?
- ContextMenu.currentMenu.destroy()
- ContextMenu.currentMenu = @
- @render()
- for entry in entries
- @addEntry(entry)
- @show(position)
-
- render: () ->
- @setElement($(@templates.menu))
- $(document.body).append(@$el)
- return @
-
- destroy: () ->
- @$el.remove()
- @trigger "destroy"
-
- show: (position) ->
- page = $(document.body)
- page.on "click.hideContextMenu", (e) =>
- page.off "click.hideContextMenu"
- @destroy()
- @$el.css
- position: "absolute"
- "z-index": 10000
- @$el.css position
-
- addEntry: (options) ->
- if options.divider
- @$el.append $(@templates.divider)
- else
- entry = new ContextMenuEntry(options)
- @$el.append entry.render().el
-
-
-
-
diff --git a/services/web/public/coffee/utils/Effects.coffee b/services/web/public/coffee/utils/Effects.coffee
deleted file mode 100644
index 7d3d1c197f..0000000000
--- a/services/web/public/coffee/utils/Effects.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-define () ->
- Effects =
- highlight: (element, color, length, callback = () ->)->
- element.animate
- backgroundColor: color
- , length, -> callback()
-
- fadeElementIn: (element, callback = () -> )->
- element.hide()
- element.slideDown "slow"
- element.fadeIn "slow"
- @highlight element, "#BDDFB3", 1500, =>
- setTimeout ( () -> element.removeAttr('style') ), 0
- callback()
-
- fadeElementOut: (element, callback = () -> )->
- @highlight element, "#FF8E8A", 800, ->
- element.slideUp "slow"
- element.fadeOut "slow", ->
- callback()
-
-
diff --git a/services/web/public/coffee/utils/Modal.coffee b/services/web/public/coffee/utils/Modal.coffee
deleted file mode 100644
index 66c5b317af..0000000000
--- a/services/web/public/coffee/utils/Modal.coffee
+++ /dev/null
@@ -1,65 +0,0 @@
-define [
- "libs/backbone"
- "libs/mustache"
-], () ->
- Modal = Backbone.View.extend {
- templates:
- modal: $("#genericModalTemplate").html()
- button: $("#genericModalButtonTemplate").html()
-
- initialize: () ->
- @render()
- @options.buttons ||= []
- self = @
- for buttonOptions in @options.buttons
- do (buttonOptions) ->
- button = $(Mustache.to_html self.templates.button, buttonOptions)
- self.$(".modal-footer").prepend button
- button.on "click", (e) ->
- e.preventDefault()
- if buttonOptions.callback?
- buttonOptions.callback(button)
- if !buttonOptions.close? or buttonOptions.close
- self.remove()
-
- @$el.modal
- # make sure we control when the modal is hidden
- keyboard: false
- backdrop: "static"
-
- @$el.on "hidden", () => @remove()
-
- if @options.clearBackdrop
- $(".modal-backdrop").addClass("clear-modal-backdrop")
-
- @$el.find('input').on "keydown", (event)->
- code = event.keyCode || event.which
- if code == 13
- self.$el.find('.btn-primary').click()
-
- @$el.find('input').focus()
-
- remove: () ->
- @$el.modal("hide")
- Backbone.View.prototype.remove.call(this)
-
- render: () ->
- @setElement $(
- Mustache.to_html @templates.modal, @options
- )
- if @options.el?
- @$(".message").append @options.el
- $(document.body).append(@$el)
- }, {
- createModal: (options) ->
- new Modal(options)
- }
-
- return Modal
-
-
-
-
-
-
-
diff --git a/services/web/public/js/libs/moment-2.4.0.js b/services/web/public/js/libs/moment-2.4.0.js
deleted file mode 100755
index 568ad05cec..0000000000
--- a/services/web/public/js/libs/moment-2.4.0.js
+++ /dev/null
@@ -1,6 +0,0 @@
-//! moment.js
-//! version : 2.4.0
-//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
-//! license : MIT
-//! momentjs.com
-(function(a){function b(a,b){return function(c){return i(a.call(this,c),b)}}function c(a,b){return function(c){return this.lang().ordinal(a.call(this,c),b)}}function d(){}function e(a){u(a),g(this,a)}function f(a){var b=o(a),c=b.year||0,d=b.month||0,e=b.week||0,f=b.day||0,g=b.hour||0,h=b.minute||0,i=b.second||0,j=b.millisecond||0;this._input=a,this._milliseconds=+j+1e3*i+6e4*h+36e5*g,this._days=+f+7*e,this._months=+d+12*c,this._data={},this._bubble()}function g(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return b.hasOwnProperty("toString")&&(a.toString=b.toString),b.hasOwnProperty("valueOf")&&(a.valueOf=b.valueOf),a}function h(a){return 0>a?Math.ceil(a):Math.floor(a)}function i(a,b){for(var c=a+"";c.lengthd;d++)(c&&a[d]!==b[d]||!c&&q(a[d])!==q(b[d]))&&g++;return g+f}function n(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=Kb[a]||Lb[b]||b}return a}function o(a){var b,c,d={};for(c in a)a.hasOwnProperty(c)&&(b=n(c),b&&(d[b]=a[c]));return d}function p(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}bb[b]=function(e,f){var g,h,i=bb.fn._lang[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=bb().utc().set(d,a);return i.call(bb.fn._lang,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function q(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function r(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function s(a){return t(a)?366:365}function t(a){return 0===a%4&&0!==a%100||0===a%400}function u(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[gb]<0||a._a[gb]>11?gb:a._a[hb]<1||a._a[hb]>r(a._a[fb],a._a[gb])?hb:a._a[ib]<0||a._a[ib]>23?ib:a._a[jb]<0||a._a[jb]>59?jb:a._a[kb]<0||a._a[kb]>59?kb:a._a[lb]<0||a._a[lb]>999?lb:-1,a._pf._overflowDayOfYear&&(fb>b||b>hb)&&(b=hb),a._pf.overflow=b)}function v(a){a._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function w(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function x(a){return a?a.toLowerCase().replace("_","-"):a}function y(a,b){return b.abbr=a,mb[a]||(mb[a]=new d),mb[a].set(b),mb[a]}function z(a){delete mb[a]}function A(a){var b,c,d,e,f=0,g=function(a){if(!mb[a]&&nb)try{require("./lang/"+a)}catch(b){}return mb[a]};if(!a)return bb.fn._lang;if(!k(a)){if(c=g(a))return c;a=[a]}for(;f0;){if(c=g(e.slice(0,b).join("-")))return c;if(d&&d.length>=b&&m(e,d,!0)>=b-1)break;b--}f++}return bb.fn._lang}function B(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function C(a){var b,c,d=a.match(rb);for(b=0,c=d.length;c>b;b++)d[b]=Pb[d[b]]?Pb[d[b]]:B(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function D(a,b){return a.isValid()?(b=E(b,a.lang()),Mb[b]||(Mb[b]=C(b)),Mb[b](a)):a.lang().invalidDate()}function E(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(sb.lastIndex=0;d>=0&&sb.test(a);)a=a.replace(sb,c),sb.lastIndex=0,d-=1;return a}function F(a,b){var c;switch(a){case"DDDD":return vb;case"YYYY":case"GGGG":case"gggg":return wb;case"YYYYY":case"GGGGG":case"ggggg":return xb;case"S":case"SS":case"SSS":case"DDD":return ub;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return zb;case"a":case"A":return A(b._l)._meridiemParse;case"X":return Cb;case"Z":case"ZZ":return Ab;case"T":return Bb;case"SSSS":return yb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"ww":case"W":case"WW":case"e":case"E":return tb;default:return c=new RegExp(N(M(a.replace("\\","")),"i"))}}function G(a){var b=(Ab.exec(a)||[])[0],c=(b+"").match(Hb)||["-",0,0],d=+(60*c[1])+q(c[2]);return"+"===c[0]?-d:d}function H(a,b,c){var d,e=c._a;switch(a){case"M":case"MM":null!=b&&(e[gb]=q(b)-1);break;case"MMM":case"MMMM":d=A(c._l).monthsParse(b),null!=d?e[gb]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[hb]=q(b));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=q(b));break;case"YY":e[fb]=q(b)+(q(b)>68?1900:2e3);break;case"YYYY":case"YYYYY":e[fb]=q(b);break;case"a":case"A":c._isPm=A(c._l).isPM(b);break;case"H":case"HH":case"h":case"hh":e[ib]=q(b);break;case"m":case"mm":e[jb]=q(b);break;case"s":case"ss":e[kb]=q(b);break;case"S":case"SS":case"SSS":case"SSSS":e[lb]=q(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=G(b);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":a=a.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=b)}}function I(a){var b,c,d,e,f,g,h,i,j,k,l=[];if(!a._d){for(d=K(a),a._w&&null==a._a[hb]&&null==a._a[gb]&&(f=function(b){return b?b.length<3?parseInt(b,10)>68?"19"+b:"20"+b:b:null==a._a[fb]?bb().weekYear():a._a[fb]},g=a._w,null!=g.GG||null!=g.W||null!=g.E?h=X(f(g.GG),g.W||1,g.E,4,1):(i=A(a._l),j=null!=g.d?T(g.d,i):null!=g.e?parseInt(g.e,10)+i._week.dow:0,k=parseInt(g.w,10)||1,null!=g.d&&js(e)&&(a._pf._overflowDayOfYear=!0),c=S(e,0,a._dayOfYear),a._a[gb]=c.getUTCMonth(),a._a[hb]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=l[b]=d[b];for(;7>b;b++)a._a[b]=l[b]=null==a._a[b]?2===b?1:0:a._a[b];l[ib]+=q((a._tzm||0)/60),l[jb]+=q((a._tzm||0)%60),a._d=(a._useUTC?S:R).apply(null,l)}}function J(a){var b;a._d||(b=o(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],I(a))}function K(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function L(a){a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=A(a._l),h=""+a._i,i=h.length,j=0;for(d=E(a._f,g).match(rb)||[],b=0;b0&&a._pf.unusedInput.push(f),h=h.slice(h.indexOf(c)+c.length),j+=c.length),Pb[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),H(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=i-j,h.length>0&&a._pf.unusedInput.push(h),a._isPm&&a._a[ib]<12&&(a._a[ib]+=12),a._isPm===!1&&12===a._a[ib]&&(a._a[ib]=0),I(a),u(a)}function M(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function N(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function O(a){var b,c,d,e,f;if(0===a._f.length)return a._pf.invalidFormat=!0,a._d=new Date(0/0),void 0;for(e=0;ef)&&(d=f,c=b));g(a,c||b)}function P(a){var b,c=a._i,d=Db.exec(c);if(d){for(a._pf.iso=!0,b=4;b>0;b--)if(d[b]){a._f=Fb[b-1]+(d[6]||" ");break}for(b=0;4>b;b++)if(Gb[b][1].exec(c)){a._f+=Gb[b][0];break}Ab.exec(c)&&(a._f+="Z"),L(a)}else a._d=new Date(c)}function Q(b){var c=b._i,d=ob.exec(c);c===a?b._d=new Date:d?b._d=new Date(+d[1]):"string"==typeof c?P(b):k(c)?(b._a=c.slice(0),I(b)):l(c)?b._d=new Date(+c):"object"==typeof c?J(b):b._d=new Date(c)}function R(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function S(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function T(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function U(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function V(a,b,c){var d=eb(Math.abs(a)/1e3),e=eb(d/60),f=eb(e/60),g=eb(f/24),h=eb(g/365),i=45>d&&["s",d]||1===e&&["m"]||45>e&&["mm",e]||1===f&&["h"]||22>f&&["hh",f]||1===g&&["d"]||25>=g&&["dd",g]||45>=g&&["M"]||345>g&&["MM",eb(g/30)]||1===h&&["y"]||["yy",h];return i[2]=b,i[3]=a>0,i[4]=c,U.apply({},i)}function W(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=bb(a).add("d",f),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function X(a,b,c,d,e){var f,g,h=new Date(Date.UTC(a,0)).getUTCDay();return c=null!=c?c:e,f=e-h+(h>d?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:s(a-1)+g}}function Y(a){var b=a._i,c=a._f;return"undefined"==typeof a._pf&&v(a),null===b?bb.invalid({nullInput:!0}):("string"==typeof b&&(a._i=b=A().preparse(b)),bb.isMoment(b)?(a=g({},b),a._d=new Date(+b._d)):c?k(c)?O(a):L(a):Q(a),new e(a))}function Z(a,b){bb.fn[a]=bb.fn[a+"s"]=function(a){var c=this._isUTC?"UTC":"";return null!=a?(this._d["set"+c+b](a),bb.updateOffset(this),this):this._d["get"+c+b]()}}function $(a){bb.duration.fn[a]=function(){return this._data[a]}}function _(a,b){bb.duration.fn["as"+a]=function(){return+this/b}}function ab(a){var b=!1,c=bb;"undefined"==typeof ender&&(this.moment=a?function(){return!b&&console&&console.warn&&(b=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),c.apply(null,arguments)}:bb)}for(var bb,cb,db="2.4.0",eb=Math.round,fb=0,gb=1,hb=2,ib=3,jb=4,kb=5,lb=6,mb={},nb="undefined"!=typeof module&&module.exports,ob=/^\/?Date\((\-?\d+)/i,pb=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,qb=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,rb=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,sb=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,tb=/\d\d?/,ub=/\d{1,3}/,vb=/\d{3}/,wb=/\d{1,4}/,xb=/[+\-]?\d{1,6}/,yb=/\d+/,zb=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Ab=/Z|[\+\-]\d\d:?\d\d/i,Bb=/T/i,Cb=/[\+\-]?\d+(\.\d{1,3})?/,Db=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d:?\d\d|Z)?)?$/,Eb="YYYY-MM-DDTHH:mm:ssZ",Fb=["YYYY-MM-DD","GGGG-[W]WW","GGGG-[W]WW-E","YYYY-DDD"],Gb=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Hb=/([\+\-]|\d\d)/gi,Ib="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Jb={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Kb={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Lb={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Mb={},Nb="DDD w W M D d".split(" "),Ob="M D H h m s w W".split(" "),Pb={M:function(){return this.month()+1},MMM:function(a){return this.lang().monthsShort(this,a)},MMMM:function(a){return this.lang().months(this,a)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(a){return this.lang().weekdaysMin(this,a)},ddd:function(a){return this.lang().weekdaysShort(this,a)},dddd:function(a){return this.lang().weekdays(this,a)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return i(this.year()%100,2)},YYYY:function(){return i(this.year(),4)},YYYYY:function(){return i(this.year(),5)},gg:function(){return i(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return i(this.weekYear(),5)},GG:function(){return i(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return i(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return q(this.milliseconds()/100)},SS:function(){return i(q(this.milliseconds()/10),2)},SSS:function(){return i(this.milliseconds(),3)},SSSS:function(){return i(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(a/60),2)+":"+i(q(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(10*a/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}},Qb=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];Nb.length;)cb=Nb.pop(),Pb[cb+"o"]=c(Pb[cb],cb);for(;Ob.length;)cb=Ob.pop(),Pb[cb+cb]=b(Pb[cb],2);for(Pb.DDDD=b(Pb.DDD,3),g(d.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=bb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=bb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return W(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),bb=function(b,c,d,e){return"boolean"==typeof d&&(e=d,d=a),Y({_i:b,_f:c,_l:d,_strict:e,_isUTC:!1})},bb.utc=function(b,c,d,e){var f;return"boolean"==typeof d&&(e=d,d=a),f=Y({_useUTC:!0,_isUTC:!0,_l:d,_i:b,_f:c,_strict:e}).utc()},bb.unix=function(a){return bb(1e3*a)},bb.duration=function(a,b){var c,d,e,g=bb.isDuration(a),h="number"==typeof a,i=g?a._input:h?{}:a,j=null;return h?b?i[b]=a:i.milliseconds=a:(j=pb.exec(a))?(c="-"===j[1]?-1:1,i={y:0,d:q(j[hb])*c,h:q(j[ib])*c,m:q(j[jb])*c,s:q(j[kb])*c,ms:q(j[lb])*c}):(j=qb.exec(a))&&(c="-"===j[1]?-1:1,e=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*c},i={y:e(j[2]),M:e(j[3]),d:e(j[4]),h:e(j[5]),m:e(j[6]),s:e(j[7]),w:e(j[8])}),d=new f(i),g&&a.hasOwnProperty("_lang")&&(d._lang=a._lang),d},bb.version=db,bb.defaultFormat=Eb,bb.updateOffset=function(){},bb.lang=function(a,b){var c;return a?(b?y(x(a),b):null===b?(z(a),a="en"):mb[a]||A(a),c=bb.duration.fn._lang=bb.fn._lang=A(a),c._abbr):bb.fn._lang._abbr},bb.langData=function(a){return a&&a._lang&&a._lang._abbr&&(a=a._lang._abbr),A(a)},bb.isMoment=function(a){return a instanceof e},bb.isDuration=function(a){return a instanceof f},cb=Qb.length-1;cb>=0;--cb)p(Qb[cb]);for(bb.normalizeUnits=function(a){return n(a)},bb.invalid=function(a){var b=bb.utc(0/0);return null!=a?g(b._pf,a):b._pf.userInvalidated=!0,b},bb.parseZone=function(a){return bb(a).parseZone()},g(bb.fn=e.prototype,{clone:function(){return bb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return D(bb(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var a=this;return[a.year(),a.month(),a.date(),a.hours(),a.minutes(),a.seconds(),a.milliseconds()]},isValid:function(){return w(this)},isDSTShifted:function(){return this._a?this.isValid()&&m(this._a,(this._isUTC?bb.utc(this._a):bb(this._a)).toArray())>0:!1},parsingFlags:function(){return g({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(a){var b=D(this,a||bb.defaultFormat);return this.lang().postformat(b)},add:function(a,b){var c;return c="string"==typeof a?bb.duration(+b,a):bb.duration(a,b),j(this,c,1),this},subtract:function(a,b){var c;return c="string"==typeof a?bb.duration(+b,a):bb.duration(a,b),j(this,c,-1),this},diff:function(a,b,c){var d,e,f=this._isUTC?bb(a).zone(this._offset||0):bb(a).local(),g=6e4*(this.zone()-f.zone());return b=n(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-bb(this).startOf("month")-(f-bb(f).startOf("month")))/d,e-=6e4*(this.zone()-bb(this).startOf("month").zone()-(f.zone()-bb(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:h(e)},from:function(a,b){return bb.duration(this.diff(a)).lang(this.lang()._abbr).humanize(!b)},fromNow:function(a){return this.from(bb(),a)},calendar:function(){var a=this.diff(bb().zone(this.zone()).startOf("day"),"days",!0),b=-6>a?"sameElse":-1>a?"lastWeek":0>a?"lastDay":1>a?"sameDay":2>a?"nextDay":7>a?"nextWeek":"sameElse";return this.format(this.lang().calendar(b,this))},isLeapYear:function(){return t(this.year())},isDST:function(){return this.zone()+bb(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+bb(a).startOf(b)},isSame:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)===+bb(a).startOf(b)},min:function(a){return a=bb.apply(null,arguments),this>a?this:a},max:function(a){return a=bb.apply(null,arguments),a>this?this:a},zone:function(a){var b=this._offset||0;return null==a?this._isUTC?b:this._d.getTimezoneOffset():("string"==typeof a&&(a=G(a)),Math.abs(a)<16&&(a=60*a),this._offset=a,this._isUTC=!0,b!==a&&j(this,bb.duration(b-a,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?bb(a).zone():0,0===(this.zone()-a)%60},daysInMonth:function(){return r(this.year(),this.month())},dayOfYear:function(a){var b=eb((bb(this).startOf("day")-bb(this).startOf("year"))/864e5)+1;return null==a?b:this.add("d",a-b)},weekYear:function(a){var b=W(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==a?b:this.add("y",a-b)},isoWeekYear:function(a){var b=W(this,1,4).year;return null==a?b:this.add("y",a-b)},week:function(a){var b=this.lang().week(this);return null==a?b:this.add("d",7*(a-b))},isoWeek:function(a){var b=W(this,1,4).week;return null==a?b:this.add("d",7*(a-b))},weekday:function(a){var b=(this.day()+7-this.lang()._week.dow)%7;return null==a?b:this.add("d",a-b)},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},get:function(a){return a=n(a),this[a]()},set:function(a,b){return a=n(a),"function"==typeof this[a]&&this[a](b),this},lang:function(b){return b===a?this._lang:(this._lang=A(b),this)}}),cb=0;cb