overleaf/services/web/frontend/js/ide/file-tree/FileTreeManager.js
Alf Eaton 5a5436d38f Run $digest in a timeout (#7996)
GitOrigin-RevId: e3c0e9f3cdf917f6a112064086454d64fea489cf
2022-05-19 08:03:25 +00:00

742 lines
20 KiB
JavaScript

/* eslint-disable
camelcase,
n/handle-callback-err,
max-len,
no-dupe-class-members,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS202: Simplify dynamic range loops
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import './directives/fileEntity'
import './controllers/FileTreeController'
import './controllers/FileTreeEntityController'
import './controllers/FileTreeFolderController'
import '../../features/file-tree/controllers/file-tree-controller'
let FileTreeManager
export default FileTreeManager = class FileTreeManager {
constructor(ide, $scope) {
this.ide = ide
this.$scope = $scope
this.$scope.$on('project:joined', () => {
this.loadRootFolder()
this.loadDeletedDocs()
return this.$scope.$emit('file-tree:initialized')
})
this.$scope.$on('entities:multiSelected', (_event, data) => {
this.$scope.$apply(() => {
this.$scope.multiSelectedCount = data.count
this.$scope.editor.multiSelectedCount = data.count
})
})
this.$scope.$watch('rootFolder', rootFolder => {
if (rootFolder != null) {
return this.recalculateDocList()
}
})
this._bindToSocketEvents()
this.$scope.multiSelectedCount = 0
$(document).on('click', () => {
this.clearMultiSelectedEntities()
setTimeout(() => this.$scope.$digest(), 0)
})
}
_bindToSocketEvents() {
this.ide.socket.on('reciveNewDoc', (parent_folder_id, doc) => {
const parent_folder =
this.findEntityById(parent_folder_id) || this.$scope.rootFolder
return this.$scope.$apply(() => {
parent_folder.children.push({
name: doc.name,
id: doc._id,
type: 'doc',
})
return this.recalculateDocList()
})
})
this.ide.socket.on(
'reciveNewFile',
(parent_folder_id, file, source, linkedFileData) => {
const parent_folder =
this.findEntityById(parent_folder_id) || this.$scope.rootFolder
return this.$scope.$apply(() => {
parent_folder.children.push({
name: file.name,
id: file._id,
type: 'file',
linkedFileData,
created: file.created,
})
return this.recalculateDocList()
})
}
)
this.ide.socket.on('reciveNewFolder', (parent_folder_id, folder) => {
const parent_folder =
this.findEntityById(parent_folder_id) || this.$scope.rootFolder
return this.$scope.$apply(() => {
parent_folder.children.push({
name: folder.name,
id: folder._id,
type: 'folder',
children: [],
})
return this.recalculateDocList()
})
})
this.ide.socket.on('reciveEntityRename', (entity_id, name) => {
const entity = this.findEntityById(entity_id)
if (entity == null) {
return
}
return this.$scope.$apply(() => {
entity.name = name
return this.recalculateDocList()
})
})
this.ide.socket.on('removeEntity', entity_id => {
const entity = this.findEntityById(entity_id)
if (entity == null) {
return
}
this.$scope.$apply(() => {
this._deleteEntityFromScope(entity)
return this.recalculateDocList()
})
return this.$scope.$broadcast('entity:deleted', entity)
})
return this.ide.socket.on('reciveEntityMove', (entity_id, folder_id) => {
const entity = this.findEntityById(entity_id)
const folder = this.findEntityById(folder_id)
return this.$scope.$apply(() => {
this._moveEntityInScope(entity, folder)
return this.recalculateDocList()
})
})
}
selectEntity(entity) {
this.selected_entity_id = entity.id // For reselecting after a reconnect
this.ide.fileTreeManager.forEachEntity(entity => (entity.selected = false))
return (entity.selected = true)
}
toggleMultiSelectEntity(entity) {
entity.multiSelected = !entity.multiSelected
this.$scope.multiSelectedCount = this.multiSelectedCount()
this.$scope.editor.multiSelectedCount = this.$scope.multiSelectedCount
}
multiSelectedCount() {
let count = 0
this.forEachEntity(function (entity) {
if (entity.multiSelected) {
return count++
}
})
return count
}
getMultiSelectedEntities() {
const entities = []
this.forEachEntity(function (e) {
if (e.multiSelected) {
return entities.push(e)
}
})
return entities
}
getFullCount() {
const entities = []
this.forEachEntity(function (e) {
if (!e.deleted) entities.push(e)
})
return entities.length
}
getMultiSelectedEntityChildNodes() {
// use pathnames with a leading slash to avoid
// problems with reserved Object properties
const entities = this.getMultiSelectedEntities()
const paths = {}
for (const entity of Array.from(entities)) {
paths['/' + this.getEntityPath(entity)] = entity
}
const prefixes = {}
for (const path in paths) {
const entity = paths[path]
const parts = path.split('/')
if (parts.length <= 2) {
continue
} else {
// Record prefixes a/b/c.tex -> 'a' and 'a/b'
for (
let i = 1, end = parts.length - 1, asc = end >= 1;
asc ? i <= end : i >= end;
asc ? i++ : i--
) {
prefixes['/' + parts.slice(0, i).join('/')] = true
}
}
}
const child_entities = []
for (const path in paths) {
// If the path is in the prefixes, then it's a parent folder and
// should be ignore
const entity = paths[path]
if (prefixes[path] == null) {
child_entities.push(entity)
}
}
return child_entities
}
clearMultiSelectedEntities() {
if (this.$scope.multiSelectedCount === 0) {
return
} // Be efficient, this is called a lot on 'click'
this.forEachEntity(entity => (entity.multiSelected = false))
return (this.$scope.multiSelectedCount = 0)
}
multiSelectSelectedEntity() {
const entity = this.findSelectedEntity()
if (entity) {
entity.multiSelected = true
}
this.$scope.multiSelectedCount = this.multiSelectedCount()
}
existsInFolder(folder_id, name) {
const folder = this.findEntityById(folder_id)
if (folder == null) {
return false
}
const entity = this._findEntityByPathInFolder(folder, name)
return entity != null
}
findSelectedEntity() {
let selected = null
this.forEachEntity(function (entity) {
if (entity.selected) {
return (selected = entity)
}
})
return selected
}
findEntityById(id, options) {
if (options == null) {
options = {}
}
if (this.$scope.rootFolder.id === id) {
return this.$scope.rootFolder
}
let entity = this._findEntityByIdInFolder(this.$scope.rootFolder, id)
if (entity != null) {
return entity
}
if (options.includeDeleted) {
for (entity of Array.from(this.$scope.deletedDocs)) {
if (entity.id === id) {
return entity
}
}
}
return null
}
_findEntityByIdInFolder(folder, id) {
for (const entity of Array.from(folder.children || [])) {
if (entity.id === id) {
return entity
} else if (entity.children != null) {
const result = this._findEntityByIdInFolder(entity, id)
if (result != null) {
return result
}
}
}
return null
}
findEntityByPath(path) {
return this._findEntityByPathInFolder(this.$scope.rootFolder, path)
}
_findEntityByPathInFolder(folder, path) {
if (path == null || folder == null) {
return null
}
if (path === '') {
return folder
}
const parts = path.split('/')
const name = parts.shift()
const rest = parts.join('/')
if (name === '.') {
return this._findEntityByPathInFolder(folder, rest)
}
for (const entity of Array.from(folder.children)) {
if (entity.name === name) {
if (rest === '') {
return entity
} else if (entity.type === 'folder') {
return this._findEntityByPathInFolder(entity, rest)
}
}
}
return null
}
forEachEntity(callback) {
if (callback == null) {
callback = function () {}
}
this._forEachEntityInFolder(this.$scope.rootFolder, null, callback)
return (() => {
const result = []
for (const entity of Array.from(this.$scope.deletedDocs || [])) {
result.push(callback(entity))
}
return result
})()
}
_forEachEntityInFolder(folder, path, callback) {
return (() => {
const result = []
for (const entity of Array.from(folder.children || [])) {
let childPath
if (path != null) {
childPath = path + '/' + entity.name
} else {
childPath = entity.name
}
callback(entity, folder, childPath)
if (entity.children != null) {
result.push(this._forEachEntityInFolder(entity, childPath, callback))
} else {
result.push(undefined)
}
}
return result
})()
}
getEntityPath(entity) {
return this._getEntityPathInFolder(this.$scope.rootFolder, entity)
}
_getEntityPathInFolder(folder, entity) {
for (const child of Array.from(folder.children || [])) {
if (child === entity) {
return entity.name
} else if (child.type === 'folder') {
const path = this._getEntityPathInFolder(child, entity)
if (path != null) {
return child.name + '/' + path
}
}
}
return null
}
getRootDocDirname() {
const rootDoc = this.findEntityById(this.$scope.project.rootDoc_id)
if (rootDoc == null) {
return
}
return this._getEntityDirname(rootDoc)
}
_getEntityDirname(entity) {
const path = this.getEntityPath(entity)
if (path == null) {
return
}
return path.split('/').slice(0, -1).join('/')
}
_findParentFolder(entity) {
const dirname = this._getEntityDirname(entity)
if (dirname == null) {
return
}
return this.findEntityByPath(dirname)
}
loadRootFolder() {
return (this.$scope.rootFolder = this._parseFolder(
__guard__(
this.$scope != null ? this.$scope.project : undefined,
x => x.rootFolder[0]
)
))
}
_parseFolder(rawFolder) {
const folder = {
name: rawFolder.name,
id: rawFolder._id,
type: 'folder',
children: [],
selected: rawFolder._id === this.selected_entity_id,
}
for (const doc of Array.from(rawFolder.docs || [])) {
folder.children.push({
name: doc.name,
id: doc._id,
type: 'doc',
selected: doc._id === this.selected_entity_id,
})
}
for (const file of Array.from(rawFolder.fileRefs || [])) {
folder.children.push({
name: file.name,
id: file._id,
type: 'file',
selected: file._id === this.selected_entity_id,
linkedFileData: file.linkedFileData,
created: file.created,
})
}
for (const childFolder of Array.from(rawFolder.folders || [])) {
folder.children.push(this._parseFolder(childFolder))
}
return folder
}
loadDeletedDocs() {
this.$scope.deletedDocs = []
return Array.from(this.$scope.project.deletedDocs || []).map(doc =>
this.$scope.deletedDocs.push({
name: doc.name,
id: doc._id,
type: 'doc',
deleted: true,
})
)
}
recalculateDocList() {
this.$scope.docs = []
this.forEachEntity((entity, parentFolder, path) => {
if (entity.type === 'doc' && !entity.deleted) {
return this.$scope.docs.push({
doc: entity,
path,
})
}
})
// Keep list ordered by folders, then name
return this.$scope.docs.sort(function (a, b) {
const aDepth = (a.path.match(/\//g) || []).length
const bDepth = (b.path.match(/\//g) || []).length
if (aDepth - bDepth !== 0) {
return -(aDepth - bDepth) // Deeper path == folder first
} else if (a.path < b.path) {
return -1
} else if (a.path > b.path) {
return 1
} else {
return 0
}
})
}
getCurrentFolder() {
// Return the root folder if nothing is selected
return (
this._getCurrentFolder(this.$scope.rootFolder) || this.$scope.rootFolder
)
}
_getCurrentFolder(startFolder) {
if (startFolder == null) {
startFolder = this.$scope.rootFolder
}
for (const entity of Array.from(startFolder.children || [])) {
// 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') {
const result = this._getCurrentFolder(entity)
if (result != null) {
return result
}
}
}
return null
}
projectContainsFolder() {
for (const entity of Array.from(this.$scope.rootFolder.children)) {
if (entity.type === 'folder') {
return true
}
}
return false
}
existsInThisFolder(folder, name) {
for (const entity of Array.from(
(folder != null ? folder.children : undefined) || []
)) {
if (entity.name === name) {
return true
}
}
return false
}
nameExistsError(message) {
if (message == null) {
message = 'already exists'
}
const nameExists = this.ide.$q.defer()
nameExists.reject({ data: message })
return nameExists.promise
}
createDoc(name, parent_folder) {
// check if a doc/file/folder already exists with this name
if (parent_folder == null) {
parent_folder = this.getCurrentFolder()
}
if (this.existsInThisFolder(parent_folder, name)) {
return this.nameExistsError()
}
// We'll wait for the socket.io notification to actually
// add the doc for us.
return this.ide.$http.post(`/project/${this.ide.project_id}/doc`, {
name,
parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
_csrf: window.csrfToken,
})
}
createFolder(name, parent_folder) {
// check if a doc/file/folder already exists with this name
if (parent_folder == null) {
parent_folder = this.getCurrentFolder()
}
if (this.existsInThisFolder(parent_folder, name)) {
return this.nameExistsError()
}
// We'll wait for the socket.io notification to actually
// add the folder for us.
return this.ide.$http.post(`/project/${this.ide.project_id}/folder`, {
name,
parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
_csrf: window.csrfToken,
})
}
createLinkedFile(name, parent_folder, provider, data) {
// check if a doc/file/folder already exists with this name
if (parent_folder == null) {
parent_folder = this.getCurrentFolder()
}
if (this.existsInThisFolder(parent_folder, name)) {
return this.nameExistsError()
}
// We'll wait for the socket.io notification to actually
// add the file for us.
return this.ide.$http.post(
`/project/${this.ide.project_id}/linked_file`,
{
name,
parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
provider,
data,
_csrf: window.csrfToken,
},
{
disableAutoLoginRedirect: true,
}
)
}
refreshLinkedFile(file) {
const parent_folder = this._findParentFolder(file)
const provider =
file.linkedFileData != null ? file.linkedFileData.provider : undefined
if (provider == null) {
console.warn(`>> no provider for ${file.name}`, file)
return
}
return this.ide.$http.post(
`/project/${this.ide.project_id}/linked_file/${file.id}/refresh`,
{
_csrf: window.csrfToken,
},
{
disableAutoLoginRedirect: true,
}
)
}
renameEntity(entity, name, callback) {
if (callback == null) {
callback = function () {}
}
if (entity.name === name) {
return
}
if (name.length >= 150) {
return
}
// check if a doc/file/folder already exists with this name
const parent_folder = this.getCurrentFolder()
if (this.existsInThisFolder(parent_folder, name)) {
return this.nameExistsError()
}
entity.renamingToName = name
return this.ide.$http
.post(
`/project/${this.ide.project_id}/${entity.type}/${entity.id}/rename`,
{
name,
_csrf: window.csrfToken,
}
)
.then(() => (entity.name = name))
.finally(() => (entity.renamingToName = null))
}
deleteEntity(entity, callback) {
// We'll wait for the socket.io notification to
// delete from scope.
if (callback == null) {
callback = function () {}
}
return this.ide.queuedHttp({
method: 'DELETE',
url: `/project/${this.ide.project_id}/${entity.type}/${entity.id}`,
headers: {
'X-Csrf-Token': window.csrfToken,
},
})
}
moveEntity(entity, parent_folder) {
// Abort move if the folder being moved (entity) has the parent_folder as child
// since that would break the tree structure.
if (this._isChildFolder(entity, parent_folder)) {
return
}
// check if a doc/file/folder already exists with this name
if (this.existsInThisFolder(parent_folder, entity.name)) {
throw new Error('file exists in this location')
}
// Wait for the http response before doing the move
this.ide.queuedHttp
.post(
`/project/${this.ide.project_id}/${entity.type}/${entity.id}/move`,
{
folder_id: parent_folder.id,
_csrf: window.csrfToken,
}
)
.then(() => {
this._moveEntityInScope(entity, parent_folder)
})
}
_isChildFolder(parent_folder, child_folder) {
const parent_path = this.getEntityPath(parent_folder) || '' // null if root folder
const child_path = this.getEntityPath(child_folder) || '' // null if root folder
// is parent path the beginning of child path?
return child_path.slice(0, parent_path.length) === parent_path
}
_deleteEntityFromScope(entity, options) {
if (options == null) {
options = { moveToDeleted: true }
}
if (entity == null) {
return
}
let parent_folder = null
this.forEachEntity(function (possible_entity, folder) {
if (possible_entity === entity) {
return (parent_folder = folder)
}
})
if (parent_folder != null) {
const index = parent_folder.children.indexOf(entity)
if (index > -1) {
parent_folder.children.splice(index, 1)
}
}
if (entity.type !== 'folder' && entity.selected) {
this.$scope.ui.view = null
}
if (entity.type === 'doc' && options.moveToDeleted) {
entity.deleted = true
return this.$scope.deletedDocs.push(entity)
}
}
_moveEntityInScope(entity, parent_folder) {
if (Array.from(parent_folder.children).includes(entity)) {
return
}
this._deleteEntityFromScope(entity, { moveToDeleted: false })
return parent_folder.children.push(entity)
}
}
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}