merge/moving js stuff around half done

This commit is contained in:
Henry Oswald 2014-07-08 12:32:50 +01:00
commit 0080809489
198 changed files with 2717 additions and 9375 deletions

View file

@ -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"
}
]

View file

@ -29,15 +29,10 @@ 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/angular-sanitize1.2.17.js')
script(src=jsPath+'libs/moment-2.4.0.js')
script(src=jsPath+'libs/underscore-1.3.3.js')
script(src=jsPath+'libs/algolia-2.5.2.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")
@ -64,10 +59,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')
)

View file

@ -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')

View file

@ -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

View file

@ -1,88 +0,0 @@
require [
"libs/mustache"
"./main"
"underscore"
], (m)->
$(document).ready ->
tableRowTemplate = '''
<tr>
<td> <input type="checkbox" class="select-one"></td>
<td> {{ email }} </td>
<td> {{ first_name }} {{ last_name }} </td>
<td> {{ !holdingAccount }} </td>
<td>
<input type="hidden" name="user_id" value="{{_id}}" class="user_id">
</td>
</tr>
'''
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 )

View file

@ -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
}]

View file

@ -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)->

View file

@ -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

View file

@ -1,71 +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"
"directives/scroll"
"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"])

View file

@ -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

View file

@ -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
}

View file

@ -1,8 +1,6 @@
define [
"../libs/angular-autocomplete/angular-autocomplete"
"../libs/ui-bootstrap"
"libs"
"modules/recursionHelper"
"../libs/ng-context-menu-0.1.4"
"utils/underscore"
], () ->

View file

@ -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 = """
<div
id='cursor-#{clientId}'
class='sharelatex-remote-cursor custom ace_start sharelatex-remote-cursor-#{colorId}'
style='height: #{config.lineHeight}px; top:#{top}px; left:#{left}px;'
>
<div class="nubbin" style="bottom: #{config.lineHeight - 2}px"></div>
<div class="name" style="display: none; bottom: #{config.lineHeight - 2}px">#{$('<div/>').text(clientData.name).html()}</div>
</div>
"""
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]

View file

@ -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:
<p><pre>
user-agent: #{useragent}
server-id: #{server_id}
transport: #{transport}
</pre></p>
"""
buttons: [
text: "OK"
]
)

View file

@ -1,6 +1,5 @@
define [
"base"
"../../libs/fineuploader"
], (App) ->
App.directive 'fineUpload', ($timeout) ->
console.log "7777777777"

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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'

View file

@ -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: () ->

View file

@ -1,9 +0,0 @@
define [
"file-tree/EntityView"
"libs/mustache"
], (EntityView) ->
DocView = EntityView.extend
onClick: (e) ->
e.preventDefault()
@options.manager.openDoc(@model)

View file

@ -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

View file

@ -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 <strong>#{entity.get("name")}</strong>?"
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()

View file

@ -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)

View file

@ -1,8 +0,0 @@
define [
"file-tree/EntityView"
"libs/mustache"
], (EntityView) ->
FileView = EntityView.extend
onClick: (e) ->
e.preventDefault()
@options.manager.openFile(@model)

View file

@ -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

View file

@ -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()

View file

@ -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"

View file

@ -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

View file

@ -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")

View file

@ -1,210 +1,71 @@
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/scroll"
"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"])

View file

@ -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

View file

@ -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 = $("<div/>")
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: """
<div class="qq-uploader">
<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>
<div class="qq-upload-button btn btn-primary btn-large">
<div>{uploadButtonText}</div>
</div>
<span class="or btn-large"> or </span>
<span class="drag-here btn-large">drag file(s)</span>
<span class="qq-drop-processing"><span>{dropProcessingText}</span><span class="qq-drop-processing-spinner"></span></span>
<div class="help">Hint: Press and hold the Control (Ctrl) key to select multiple files</div>
<ul class="qq-upload-list"></ul>
</div>
"""
$(".qq-uploader input").addClass("js-file-uploader")

View file

@ -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()

View file

@ -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

View file

@ -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."

View file

@ -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')

View file

@ -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")

View file

@ -1,5 +1,6 @@
define [
"base"
"libs/jquery-layout"
], (App) ->
App.directive "layout", ["$parse", ($parse) ->
return {

View file

@ -1,7 +1,6 @@
define [
"utils/EventEmitter"
"ide/editor/ShareJsDoc"
"underscore"
], (EventEmitter, ShareJsDoc) ->
class Document extends EventEmitter
@getDocument: (ide, doc_id) ->

View file

@ -1,6 +1,6 @@
define [
"utils/EventEmitter"
"../../../libs/sharejs"
"libs/sharejs"
], (EventEmitter, ShareJs) ->
class ShareJsDoc extends EventEmitter
constructor: (@doc_id, docLines, version, @socket) ->

View file

@ -1,11 +1,11 @@
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"
"ide/editor/directives/aceEditor/undo/UndoManager"
"ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager"
"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
"ide/editor/directives/aceEditor/highlights/HighlightsManager"
"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
"ace/keyboard/vim"
"ace/keyboard/emacs"
"ace/mode/latex"

View file

@ -62,13 +62,11 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents) ->
# 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()
if keepEditorContents
doc.del 0, doc.getText().length
doc.insert 0, editorDoc.getValue()
else
editorDoc.setValue doc.getText()
check()

Some files were not shown because too many files have changed in this diff Show more