diff --git a/services/web/.gitignore b/services/web/.gitignore index 2a29f414d1..a48481690a 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -73,3 +73,4 @@ Gemfile.lock app/views/external /modules/ +docker-shared.yml diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index b93cc8dcbb..c49add4a91 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -196,6 +196,7 @@ module.exports = (grunt) -> "mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML" "pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf" "ace": "#{PackageVersions.lib('ace')}" + "fineuploader": "libs/#{PackageVersions.lib('fineuploader')}" shim: "pdfjs-dist/build/pdf": deps: ["libs/#{PackageVersions.lib('pdfjs')}/compatibility"] diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index 33aa807fdd..6421296330 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -42,7 +42,7 @@ pipeline { checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/tpr-webmodule'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/tpr-webmodule.git ']]]) checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/learn-wiki'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/learn-wiki-web-module.git']]]) checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/templates'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/templates-webmodule.git']]]) - checkout([$class: 'GitSCM', branches: [[name: '*/sk-unlisted-projects']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]]) checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-integration'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-integration-web-module.git']]]) checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-account-merge'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-account-merge.git']]]) } @@ -70,6 +70,19 @@ pipeline { sh 'ls -l node_modules/.bin' } } + + stage('Unit Tests') { + steps { + sh 'make clean install' // Removes js files, so do before compile + sh 'make test_unit MOCHA_ARGS="--reporter=tap"' + } + } + + stage('Acceptance Tests') { + steps { + sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"' + } + } stage('Compile') { agent { @@ -109,30 +122,6 @@ pipeline { } } - stage('Unit Test') { - agent { - docker { - image 'node:6.9.5' - reuseNode true - } - } - steps { - sh 'env NODE_ENV=development ./node_modules/.bin/grunt mochaTest:unit --reporter=tap' - } - } - - stage('Acceptance Tests') { - steps { - // This tagged relase of the acceptance test runner is a temporary fix - // to get the acceptance tests working before we move to a - // docker-compose workflow. See: - // https://github.com/sharelatex/web-sharelatex-internal/pull/148 - - sh 'docker pull sharelatex/sl-acceptance-test-runner:node-6.9-mongo-3.4' - sh 'docker run --rm -v $(pwd):/app --env SHARELATEX_ALLOW_PUBLIC_ACCESS=true sharelatex/sl-acceptance-test-runner:node-6.9-mongo-3.4 || (cat forever/app.log && false)' - } - } - stage('Package') { steps { sh 'rm -rf ./node_modules/grunt*' diff --git a/services/web/Makefile b/services/web/Makefile new file mode 100644 index 0000000000..98695a8c1f --- /dev/null +++ b/services/web/Makefile @@ -0,0 +1,77 @@ +DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml +NPM := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm npm -q +BUILD_NUMBER ?= local +BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) +PROJECT_NAME = web + +all: install test + @echo "Run:" + @echo " make install to set up the project dependencies (in docker)" + @echo " make test to run all the tests for the project (in docker)" + +add: docker-shared.yml + $(NPM) install --save ${P} + +add_dev: docker-shared.yml + $(NPM) install --save-dev ${P} + +install: docker-shared.yml + $(NPM) install + +clean: + rm -f app.js + rm -rf app/js + rm -rf test/unit/js + rm -rf test/acceptance/js + for dir in modules/*; \ + do \ + rm -f $$dir/index.js; \ + rm -rf $$dir/app/js; \ + rm -rf $$dir/test/unit/js; \ + rm -rf $$dir/test/acceptance/js; \ + done + # Regenerate docker-shared.yml - not stictly a 'clean', + # but lets `make clean install` work nicely + bin/generate_volumes_file + # Deletes node_modules volume + docker-compose down --volumes + +# Need regenerating if you change the web modules you have installed +docker-shared.yml: + bin/generate_volumes_file + +test: test_unit test_acceptance + +test_unit: docker-shared.yml + docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:unit -- ${MOCHA_ARGS} + +test_acceptance: test_acceptance_app test_acceptance_modules + +test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run test_acceptance_app_stop_service + +test_acceptance_app_start_service: test_acceptance_app_stop_service docker-shared.yml + docker-compose ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance + +test_acceptance_app_stop_service: docker-shared.yml + docker-compose ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo + +test_acceptance_app_run: docker-shared.yml + docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm -q run test:acceptance -- ${MOCHA_ARGS} + +test_acceptance_modules: docker-shared.yml + # Break and error on any module failure + set -e; \ + for dir in modules/*; \ + do \ + if [ -e $$dir/Makefile ]; then \ + (make test_acceptance_module MODULE=$$dir) \ + fi \ + done + +test_acceptance_module: docker-shared.yml + cd $(MODULE) && make test_acceptance + +.PHONY: + all add install update test test_unit test_acceptance \ + test_acceptance_start_service test_acceptance_stop_service \ + test_acceptance_run diff --git a/services/web/README.md b/services/web/README.md index f777e7e5f5..51f73d02ec 100644 --- a/services/web/README.md +++ b/services/web/README.md @@ -17,6 +17,69 @@ web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`). +New Docker-based build process +------------------------------ + +Note that the Grunt workflow from above should still work, but we are transitioning to a +Docker based testing workflow, which is documented below: + +### Running the app + +The app runs natively using npm and Node on the local system: + +``` +$ npm install +$ npm run start +``` + +*Ideally the app would run in Docker like the tests below, but with host networking not supported in OS X, we need to run it natively until all services are Dockerised.* + +### Unit Tests + +The test suites run in Docker. + +Unit tests can be run in the `test_unit` container defined in `docker-compose.tests.yml`. + +The makefile contains a short cut to run these: + +``` +make install # Only needs running once, or when npm packages are updated +make unit_test +``` + +During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI: + +``` +make unit_test MOCHA_ARGS='--grep=AuthorizationManager' +``` + +### Acceptance Tests + +Acceptance tests are run against a live service, which runs in the `acceptance_test` container defined in `docker-compose.tests.yml`. + +To run the tests out-of-the-box, the makefile defines: + +``` +make install # Only needs running once, or when npm packages are updated +make acceptance_test +``` + +However, during development it is often useful to leave the service running for rapid iteration on the acceptance tests. This can be done with: + +``` +make acceptance_test_start_service +make acceptance_test_run # Run as many times as needed during development +make acceptance_test_stop_service +``` + +`make acceptance_test` just runs these three commands in sequence. + +During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI: + +``` +make acceptance_test_run MOCHA_ARGS='--grep=AuthorizationManager' +``` + Unit test status ---------------- diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee index d8613680a8..f7dd9f3e17 100644 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -204,11 +204,11 @@ module.exports = DocumentUpdaterHandler = logger.error {project_id, doc_id, thread_id}, "doc updater returned a non-success status code: #{res.statusCode}" callback new Error("doc updater returned a non-success status code: #{res.statusCode}") - updateProjectStructure : (project_id, userId, oldDocs, newDocs, oldFiles, newFiles, callback = (error) ->)-> + updateProjectStructure : (project_id, userId, changes, callback = (error) ->)-> return callback() if !settings.apis.project_history?.enabled - docUpdates = DocumentUpdaterHandler._getRenameUpdates('doc', oldDocs, newDocs) - fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', oldFiles, newFiles) + docUpdates = DocumentUpdaterHandler._getRenameUpdates('doc', changes.oldDocs, changes.newDocs) + fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', changes.oldFiles, changes.newFiles) timer = new metrics.Timer("set-document") url = "#{settings.apis.documentupdater.url}/project/#{project_id}" @@ -231,14 +231,25 @@ module.exports = DocumentUpdaterHandler = callback new Error("doc updater returned a non-success status code: #{res.statusCode}") _getRenameUpdates: (entityType, oldEntities, newEntities) -> + oldEntities ||= [] + newEntities ||= [] updates = [] - for oldEntity in oldEntities - id = oldEntity[entityType]._id - newEntity = _.find newEntities, (newEntity) -> - newEntity[entityType]._id.toString() == id.toString() + oldEntitiesHash = _.indexBy oldEntities, (entity) -> entity[entityType]._id.toString() + newEntitiesHash = _.indexBy newEntities, (entity) -> entity[entityType]._id.toString() - if newEntity.path != oldEntity.path + for id, newEntity of newEntitiesHash + oldEntity = oldEntitiesHash[id] + + if !oldEntity? + # entity added + updates.push + id: id + pathname: newEntity.path + docLines: newEntity.docLines + url: newEntity.url + else if newEntity.path != oldEntity.path + # entity renamed updates.push id: id pathname: oldEntity.path diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index a43d6f0447..f66094f9e8 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -19,48 +19,48 @@ module.exports = EditorController = DocumentUpdaterHandler.flushDocToMongo project_id, doc_id, callback - addDoc: (project_id, folder_id, docName, docLines, source, callback = (error, doc)->)-> + addDoc: (project_id, folder_id, docName, docLines, source, user_id, callback = (error, doc)->)-> LockManager.getLock project_id, (err)-> if err? logger.err err:err, project_id:project_id, source:source, "could not get lock to addDoc" return callback(err) - EditorController.addDocWithoutLock project_id, folder_id, docName, docLines, source, (error, doc)-> + EditorController.addDocWithoutLock project_id, folder_id, docName, docLines, source, user_id, (error, doc)-> LockManager.releaseLock project_id, -> callback(error, doc) - addDocWithoutLock: (project_id, folder_id, docName, docLines, source, callback = (error, doc)->)-> + addDocWithoutLock: (project_id, folder_id, docName, docLines, source, user_id, callback = (error, doc)->)-> docName = docName.trim() logger.log {project_id, folder_id, docName, source}, "sending new doc to project" Metrics.inc "editor.add-doc" - ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, (err, doc, folder_id)=> + ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, user_id, (err, doc, folder_id)=> if err? logger.err err:err, project_id:project_id, docName:docName, "error adding doc without lock" return callback(err) EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source) callback(err, doc) - addFile: (project_id, folder_id, fileName, path, source, callback = (error, file)->)-> + addFile: (project_id, folder_id, fileName, path, source, user_id, callback = (error, file)->)-> LockManager.getLock project_id, (err)-> if err? logger.err err:err, project_id:project_id, source:source, "could not get lock to addFile" return callback(err) - EditorController.addFileWithoutLock project_id, folder_id, fileName, path, source, (error, file)-> + EditorController.addFileWithoutLock project_id, folder_id, fileName, path, source, user_id, (error, file)-> LockManager.releaseLock project_id, -> callback(error, file) - addFileWithoutLock: (project_id, folder_id, fileName, path, source, callback = (error, file)->)-> + addFileWithoutLock: (project_id, folder_id, fileName, path, source, user_id, callback = (error, file)->)-> fileName = fileName.trim() logger.log {project_id, folder_id, fileName, path}, "sending new file to project" Metrics.inc "editor.add-file" - ProjectEntityHandler.addFile project_id, folder_id, fileName, path, (err, fileRef, folder_id)=> + ProjectEntityHandler.addFile project_id, folder_id, fileName, path, user_id, (err, fileRef, folder_id)=> if err? logger.err err:err, project_id:project_id, folder_id:folder_id, fileName:fileName, "error adding file without lock" return callback(err) EditorRealTimeController.emitToRoom(project_id, 'reciveNewFile', folder_id, fileRef, source) callback(err, fileRef) - replaceFile: (project_id, file_id, fsPath, source, callback = (error) ->)-> - ProjectEntityHandler.replaceFile project_id, file_id, fsPath, callback + replaceFile: (project_id, file_id, fsPath, source, user_id, callback = (error) ->)-> + ProjectEntityHandler.replaceFile project_id, file_id, fsPath, user_id, callback addFolder : (project_id, folder_id, folderName, source, callback = (error, folder)->)-> LockManager.getLock project_id, (err)-> diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index e9bac7300d..cbc296b69f 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -81,10 +81,11 @@ module.exports = EditorHttpController = project_id = req.params.Project_id name = req.body.name parent_folder_id = req.body.parent_folder_id + user_id = AuthenticationController.getLoggedInUserId(req) logger.log project_id:project_id, name:name, parent_folder_id:parent_folder_id, "getting request to add doc to project" if !EditorHttpController._nameIsAcceptableLength(name) return res.sendStatus 400 - EditorController.addDoc project_id, parent_folder_id, name, [], "editor", (error, doc) -> + EditorController.addDoc project_id, parent_folder_id, name, [], "editor", user_id, (error, doc) -> if error == "project_has_to_many_files" res.status(400).json(req.i18n.translate("project_has_to_many_files")) else if error? @@ -113,9 +114,9 @@ module.exports = EditorHttpController = entity_id = req.params.entity_id entity_type = req.params.entity_type name = req.body.name - user_id = AuthenticationController.getLoggedInUserId(req) if !EditorHttpController._nameIsAcceptableLength(name) return res.sendStatus 400 + user_id = AuthenticationController.getLoggedInUserId(req) EditorController.renameEntity project_id, entity_id, entity_type, name, user_id, (error) -> return next(error) if error? res.sendStatus 204 diff --git a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee index 92efe7bfcb..ef363fbbc7 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee @@ -21,9 +21,9 @@ module.exports = FileStoreHandler = return callback(new Error("can not upload symlink")) _cb = callback - callback = (err) -> + callback = (err, url) -> callback = -> # avoid double callbacks - _cb(err) + _cb(err, url) logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk" readStream = fs.createReadStream(fsPath) @@ -31,9 +31,10 @@ module.exports = FileStoreHandler = logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk" callback err readStream.on "open", () -> + url = FileStoreHandler._buildUrl(project_id, file_id) opts = method: "post" - uri: FileStoreHandler._buildUrl(project_id, file_id) + uri: url timeout:fiveMinsInMs writeStream = request(opts) writeStream.on "error", (err)-> @@ -45,7 +46,7 @@ module.exports = FileStoreHandler = logger.err {err, statusCode: response.statusCode}, "error uploading to filestore" callback(err) else - callback(null) + callback(null, url) readStream.pipe writeStream getFileStream: (project_id, file_id, query, callback)-> @@ -91,7 +92,7 @@ module.exports = FileStoreHandler = request opts, (err)-> if err? logger.err err:err, oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "something went wrong telling filestore api to copy file" - callback(err) + callback(err, opts.uri) _buildUrl: (project_id, file_id)-> return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}" diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 58893060ff..c7fad83bf6 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -170,6 +170,8 @@ module.exports = ProjectController = return notification projects = ProjectController._buildProjectList results.projects, results.v1Projects?.projects user = results.user + warnings = ProjectController._buildWarningsList results.v1Projects + ProjectController._injectProjectOwners projects, (error, projects) -> return next(error) if error? viewModel = { @@ -181,7 +183,7 @@ module.exports = ProjectController = user: user hasSubscription: results.hasSubscription[0] isShowingV1Projects: results.v1Projects? - noV1Connection: results.v1Projects?.noConnection + warnings: warnings } if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key? @@ -251,7 +253,7 @@ module.exports = ProjectController = # Extract data from user's ObjectId timestamp = parseInt(user_id.toString().substring(0, 8), 16) - rolloutPercentage = 10 # Percentage of users to roll out to + rolloutPercentage = 40 # Percentage of users to roll out to if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage) # Don't show if user is not part of roll out return cb(null, { enabled: false, showOnboarding: false }) @@ -427,6 +429,14 @@ module.exports = ProjectController = project.owner = users[project.owner_ref.toString()] callback null, projects + _buildWarningsList: (v1ProjectData = {}) -> + warnings = [] + if v1ProjectData.noConnection + warnings.push 'No V1 Connection' + if v1ProjectData.hasHiddenV1Projects + warnings.push "Looks like you've got a lot of V1 projects! Some of them may be hidden on V2. To view them all, use the V1 dashboard." + return warnings + defaultSettingsForAnonymousUser = (user_id)-> id : user_id ace: diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee index a38b214a9e..cd78cddc09 100644 --- a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee @@ -52,7 +52,7 @@ module.exports = ProjectCreationHandler = return callback(error) if error? self._buildTemplate "mainbasic.tex", owner_id, projectName, (error, docLines)-> return callback(error) if error? - ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, (error, doc)-> + ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, owner_id, (error, doc)-> if error? logger.err err:error, "error adding doc when creating basic project" return callback(error) @@ -67,17 +67,17 @@ module.exports = ProjectCreationHandler = (callback) -> self._buildTemplate "main.tex", owner_id, projectName, (error, docLines)-> return callback(error) if error? - ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, (error, doc)-> + ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, owner_id, (error, doc)-> return callback(error) if error? ProjectEntityHandler.setRootDoc project._id, doc._id, callback (callback) -> self._buildTemplate "references.bib", owner_id, projectName, (error, docLines)-> return callback(error) if error? - ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "references.bib", docLines, (error, doc)-> + ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "references.bib", docLines, owner_id, (error, doc)-> callback(error) (callback) -> universePath = Path.resolve(__dirname + "/../../../templates/project_files/universe.jpg") - ProjectEntityHandler.addFile project._id, project.rootFolder[0]._id, "universe.jpg", universePath, callback + ProjectEntityHandler.addFile project._id, project.rootFolder[0]._id, "universe.jpg", universePath, owner_id, callback ], (error) -> callback(error, project) diff --git a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee index d945326395..815e31eb27 100644 --- a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee @@ -12,7 +12,7 @@ logger = require("logger-sharelatex") module.exports = ProjectDuplicator = - _copyDocs: (newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)-> + _copyDocs: (owner_id, newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)-> setRootDoc = _.once (doc_id)-> projectEntityHandler.setRootDoc newProject._id, doc_id docs = originalFolder.docs or [] @@ -21,7 +21,7 @@ module.exports = ProjectDuplicator = if !doc?._id? return callback() content = docContents[doc._id.toString()] - projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, (err, newDoc)-> + projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)-> if err? logger.err err:err, "error copying doc" return callback(err) @@ -31,15 +31,15 @@ module.exports = ProjectDuplicator = async.series jobs, callback - _copyFiles: (newProject, originalProject_id, originalFolder, desFolder, callback)-> + _copyFiles: (owner_id, newProject, originalProject_id, originalFolder, desFolder, callback)-> fileRefs = originalFolder.fileRefs or [] jobs = fileRefs.map (file)-> return (cb)-> - projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, cb + projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, owner_id, cb async.parallelLimit jobs, 5, callback - _copyFolderRecursivly: (newProject_id, originalProject_id, originalRootDoc, originalFolder, desFolder, docContents, callback)-> + _copyFolderRecursivly: (owner_id, newProject_id, originalProject_id, originalRootDoc, originalFolder, desFolder, docContents, callback)-> ProjectGetter.getProject newProject_id, {rootFolder:true, name:true}, (err, newProject)-> if err? logger.err project_id:newProject_id, "could not get project" @@ -53,12 +53,12 @@ module.exports = ProjectDuplicator = return cb() projectEntityHandler.addFolderWithProject newProject, desFolder?._id, childFolder.name, (err, newFolder)-> return cb(err) if err? - ProjectDuplicator._copyFolderRecursivly newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb + ProjectDuplicator._copyFolderRecursivly owner_id, newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb jobs.push (cb)-> - ProjectDuplicator._copyFiles newProject, originalProject_id, originalFolder, desFolder, cb + ProjectDuplicator._copyFiles owner_id, newProject, originalProject_id, originalFolder, desFolder, cb jobs.push (cb)-> - ProjectDuplicator._copyDocs newProject, originalRootDoc, originalFolder, desFolder, docContents, cb + ProjectDuplicator._copyDocs owner_id, newProject, originalRootDoc, originalFolder, desFolder, docContents, cb async.series jobs, callback @@ -90,7 +90,7 @@ module.exports = ProjectDuplicator = projectOptionsHandler.setCompiler newProject._id, originalProject.compiler, -> - ProjectDuplicator._copyFolderRecursivly newProject._id, originalProject_id, originalRootDoc, originalProject.rootFolder[0], newProject.rootFolder[0], docContents, -> + ProjectDuplicator._copyFolderRecursivly owner._id, newProject._id, originalProject_id, originalRootDoc, originalProject.rootFolder[0], newProject.rootFolder[0], docContents, -> if err? logger.err err:err, originalProject_id:originalProject_id, newProjectName:newProjectName, "error cloning project" callback(err, newProject) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index abd5c7f3d8..446786fa57 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -16,7 +16,7 @@ projectUpdateHandler = require('./ProjectUpdateHandler') DocstoreManager = require "../Docstore/DocstoreManager" ProjectGetter = require "./ProjectGetter" CooldownManager = require '../Cooldown/CooldownManager' - +DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') module.exports = ProjectEntityHandler = getAllFolders: (project_id, callback) -> @@ -106,8 +106,7 @@ module.exports = ProjectEntityHandler = flushProjectToThirdPartyDataStore: (project_id, callback) -> self = @ logger.log project_id:project_id, "flushing project to tpds" - documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') - documentUpdaterHandler.flushProjectToMongo project_id, (error) -> + DocumentUpdaterHandler.flushProjectToMongo project_id, (error) -> return callback(error) if error? ProjectGetter.getProject project_id, {name:true}, (error, project) -> return callback(error) if error? @@ -150,14 +149,14 @@ module.exports = ProjectEntityHandler = else DocstoreManager.getDoc project_id, doc_id, options, callback - addDoc: (project_id, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=> + addDoc: (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=> ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) -> if err? logger.err project_id:project_id, err:err, "error getting project for add doc" return callback(err) - ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, callback + ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, userId, callback - addDocWithProject: (project, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=> + addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=> project_id = project._id logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project" confirmFolder project, folder_id, (folder_id)=> @@ -177,7 +176,14 @@ module.exports = ProjectEntityHandler = rev: 0 }, (err) -> return callback(err) if err? - callback(null, doc, folder_id) + newDocs = [ + doc: doc + path: result?.path?.fileSystem + docLines: docLines.join('\n') + ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) -> + return callback(error) if error? + callback null, doc, folder_id restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) -> # getDoc will return the deleted doc's lines, but we don't actually remove @@ -186,20 +192,20 @@ module.exports = ProjectEntityHandler = return callback(error) if error? ProjectEntityHandler.addDoc project_id, null, name, lines, callback - addFile: (project_id, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)-> + addFile: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)-> ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) -> if err? logger.err project_id:project_id, err:err, "error getting project for add file" return callback(err) - ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, callback + ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, userId, callback - addFileWithProject: (project, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)-> + addFileWithProject: (project, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)-> project_id = project._id logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file" return callback(err) if err? confirmFolder project, folder_id, (folder_id)-> fileRef = new File name : fileName - FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err)-> + FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err, fileStoreUrl)-> if err? logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" return callback(err) @@ -209,27 +215,31 @@ module.exports = ProjectEntityHandler = return callback(err) tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> return callback(err) if err? - callback(null, fileRef, folder_id) + newFiles = [ + file: fileRef + path: result?.path?.fileSystem + url: fileStoreUrl + ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> + return callback(error) if error? + callback null, fileRef, folder_id - replaceFile: (project_id, file_id, fsPath, callback)-> - ProjectGetter.getProject project_id, {name:true}, (err, project) -> + replaceFile: (project_id, file_id, fsPath, userId, callback)-> + self = ProjectEntityHandler + FileStoreHandler.uploadFileFromDisk project_id, file_id, fsPath, (err, fileStoreUrl)-> return callback(err) if err? - findOpts = - project_id:project._id - element_id:file_id - type:"file" - FileStoreHandler.uploadFileFromDisk project._id, file_id, fsPath, (err)-> + ProjectGetter.getProject project_id, {rootFolder: true, name:true}, (err, project) -> return callback(err) if err? # Note there is a potential race condition here (and elsewhere) # If the file tree changes between findElement and the Project.update # then the path to the file element will be out of date. In practice # this is not a problem so long as we do not do anything longer running # between them (like waiting for the file to upload.) - projectLocator.findElement findOpts, (err, fileRef, path)=> + projectLocator.findElement {project:project, element_id: file_id, type: 'file'}, (err, fileRef, path)=> return callback(err) if err? - tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, (error) -> + tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, (err) -> return callback(err) if err? - conditons = _id:project._id + conditions = _id:project._id inc = {} inc["#{path.mongo}.rev"] = 1 set = {} @@ -237,39 +247,43 @@ module.exports = ProjectEntityHandler = update = "$inc": inc "$set": set - Project.update conditons, update, {}, (err, second)-> - callback() + Project.findOneAndUpdate conditions, update, { "new": true}, (err) -> + return callback(err) if err? + newFiles = [ + file: fileRef + path: path.fileSystem + url: fileStoreUrl + ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, callback - copyFileFromExistingProject: (project_id, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)-> - logger.log project_id:project_id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3" - ProjectGetter.getProject project_id, {name:true}, (err, project) -> - if err? - logger.err project_id:project_id, err:err, "error getting project for copy file from existing project" - return callback(err) - ProjectEntityHandler.copyFileFromExistingProjectWithProject project, folder_id, originalProject_id, origonalFileRef, callback - - - copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)-> + copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)-> project_id = project._id - logger.log project_id:project_id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3 with project" + logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project" return callback(err) if err? confirmFolder project, folder_id, (folder_id)=> if !origonalFileRef? - logger.err project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "file trying to copy is null" + logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null" return callback() fileRef = new File name : origonalFileRef.name - FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err)-> + FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)-> if err? - logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error coping file in s3" + logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3" return callback(err) ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=> if err? - logger.err err:err, project_id:project._id, folder_id:folder_id, "error putting element as part of copy" + logger.err { err, project_id, folder_id }, "error putting element as part of copy" return callback(err) - tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) -> + tpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) -> if err? - logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error sending file to tpds worker" - callback(null, fileRef, folder_id) + logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker" + newFiles = [ + file: fileRef + path: result?.path?.fileSystem + url: fileStoreUrl + ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> + return callback(error) if error? + callback null, fileRef, folder_id mkdirp: (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)-> self = @ @@ -381,11 +395,9 @@ module.exports = ProjectEntityHandler = endPath: result.path.fileSystem, rev: entity.rev tpdsUpdateSender.moveEntity opts - self.getAllEntitiesFromProject newProject, (error, newDocs, newFiles - ) => + self.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) => return callback(error) if error? - documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') - documentUpdaterHandler.updateProjectStructure project_id, userId, oldDocs, newDocs, oldFiles, newFiles, callback + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback _checkValidMove: (project, entityType, entityPath, destFolderId, callback = (error) ->) -> return callback() if !entityType.match(/folder/) @@ -442,8 +454,7 @@ module.exports = ProjectEntityHandler = return callback(error) if error? ProjectEntityHandler.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) => return callback(error) if error? - documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') - documentUpdaterHandler.updateProjectStructure project_id, userId, oldDocs, newDocs, oldFiles, newFiles, callback + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback _cleanUpEntity: (project, entity, entityType, callback = (error) ->) -> if(entityType.indexOf("file") != -1) @@ -466,7 +477,7 @@ module.exports = ProjectEntityHandler = unsetRootDocIfRequired (error) -> return callback(error) if error? - require('../../Features/DocumentUpdater/DocumentUpdaterHandler').deleteDoc project_id, doc_id, (error) -> + DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) -> return callback(error) if error? ProjectEntityHandler._insertDeletedDocReference project._id, doc, (error) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee index bce0befe22..010594d16a 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee @@ -28,7 +28,7 @@ module.exports = FileTypeManager.isBinary path, fsPath, (err, isFile)-> return callback(err) if err? if isFile - self.p.processFile project_id, elementId, fsPath, path, source, callback + self.p.processFile project_id, elementId, fsPath, path, source, user_id, callback else self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback @@ -57,9 +57,9 @@ module.exports = if err? logger.err err:err, project_id:project_id, doc_id:doc_id, path:path, "error processing file" return callback(err) - editorController.addDoc project_id, folder._id, fileName, docLines, source, callback + editorController.addDoc project_id, folder._id, fileName, docLines, source, user_id, callback - processFile: (project_id, file_id, fsPath, path, source, callback)-> + processFile: (project_id, file_id, fsPath, path, source, user_id, callback)-> finish = (err)-> logger.log project_id:project_id, file_id:file_id, path:path, "completed processing file update from tpds" callback(err) @@ -69,9 +69,9 @@ module.exports = logger.err err:err, project_id:project_id, file_id:file_id, path:path, "error processing file" return callback(err) else if file_id? - editorController.replaceFile project_id, file_id, fsPath, source, finish + editorController.replaceFile project_id, file_id, fsPath, source, user_id, finish else - editorController.addFile project_id, folder?._id, fileName, fsPath, source, finish + editorController.addFile project_id, folder?._id, fileName, fsPath, source, user_id, finish writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)-> if !file_id? diff --git a/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee b/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee index f491807e9d..0cec7cfc9d 100644 --- a/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee +++ b/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee @@ -28,9 +28,9 @@ module.exports = FileSystemImportManager = if existingDoc? EditorController.setDoc project_id, existingDoc._id, user_id, lines, "upload", callback else - EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback + EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", user_id, callback else - EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback + EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", user_id, callback addFile: (user_id, project_id, folder_id, name, path, replace, callback = (error, file)-> )-> FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)-> @@ -39,7 +39,7 @@ module.exports = FileSystemImportManager = return callback("path is symlink") if !replace - EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback + EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", user_id, callback else ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> return callback(error) if error? @@ -50,9 +50,9 @@ module.exports = FileSystemImportManager = existingFile = fileRef break if existingFile? - EditorController.replaceFile project_id, existingFile._id, path, "upload", callback + EditorController.replaceFile project_id, existingFile._id, path, "upload", user_id, callback else - EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback + EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", user_id, callback addFolder: (user_id, project_id, folder_id, name, path, replace, callback = (error)-> ) -> FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)-> diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index fa9a223ad7..2fdf219962 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -24,6 +24,7 @@ jsPath = ace = PackageVersions.lib('ace') pdfjs = PackageVersions.lib('pdfjs') +fineuploader = PackageVersions.lib('fineuploader') getFileContent = (filePath)-> filePath = Path.join __dirname, "../../../", "public#{filePath}" @@ -37,6 +38,7 @@ getFileContent = (filePath)-> logger.log "Generating file fingerprints..." pathList = [ + ["#{jsPath}libs/#{fineuploader}.js"] ["#{jsPath}libs/require.js"] ["#{jsPath}ide.js"] ["#{jsPath}main.js"] diff --git a/services/web/app/coffee/infrastructure/PackageVersions.coffee b/services/web/app/coffee/infrastructure/PackageVersions.coffee index f8c2011c11..5acdfec666 100644 --- a/services/web/app/coffee/infrastructure/PackageVersions.coffee +++ b/services/web/app/coffee/infrastructure/PackageVersions.coffee @@ -2,6 +2,7 @@ version = { "pdfjs": "1.7.225" "moment": "2.9.0" "ace": "1.2.5" + "fineuploader": "5.15.4" } module.exports = { diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 822923ac08..31afc17d29 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -132,7 +132,8 @@ html(itemscope, itemtype='http://schema.org/Product') // minimal requirejs configuration (can be extended/overridden) window.requirejs = { "paths" : { - "moment": "libs/#{lib('moment')}" + "moment": "libs/#{lib('moment')}", + "fineuploader": "libs/#{lib('fineuploader')}" }, "urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}", "config":{ diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 1e1f2c982b..625bea883d 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -130,7 +130,8 @@ block requirejs "moment": "libs/#{lib('moment')}", "pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf", "pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}", - "ace": "#{lib('ace')}" + "ace": "#{lib('ace')}", + "fineuploader": "libs/#{lib('fineuploader')}" }, "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}", "waitSeconds": 0, diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index dc40d7b9ba..d1c6fa8156 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -118,7 +118,11 @@ div.full-size( h3.popover-title #{translate("link_sharing")} .popover-content p #{translate("try_out_link_sharing")} - img(src="/img/onboarding/linksharing/link-sharing.png" width="100%") + img( + src="/img/onboarding/linksharing/link-sharing.png" + alt="Link sharing demo" + width="100%" + ) p #{translate("try_link_sharing_description")} button.btn.btn-default.btn-block(ng-click="dismiss()") | #{translate("got_it")} diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index f936750a13..5b4934ede2 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -1,5 +1,5 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected': multiSelectedCount > 0 }").full-size - .toolbar.toolbar-small.toolbar-alt(ng-if="permissions.write") + .toolbar.toolbar-filetree(ng-if="permissions.write") a( href, ng-click="openNewDocModal()", diff --git a/services/web/app/views/project/editor/header.pug b/services/web/app/views/project/editor/header.pug index 49361b5a2e..c80c4d7b3d 100644 --- a/services/web/app/views/project/editor/header.pug +++ b/services/web/app/views/project/editor/header.pug @@ -7,9 +7,9 @@ header.toolbar.toolbar-header.toolbar-with-labels( href, ng-click="ui.leftMenuShown = true;", ) - i.fa.fa-fw.fa-bars + i.fa.fa-fw.fa-bars.editor-menu-icon p.toolbar-label #{translate("menu")} - a( + a.toolbar-header-back-projects( href="/project" ) i.fa.fa-fw.fa-level-up diff --git a/services/web/app/views/project/editor/pdf.pug b/services/web/app/views/project/editor/pdf.pug index eb2b4bfefd..a3b3a2e2f9 100644 --- a/services/web/app/views/project/editor/pdf.pug +++ b/services/web/app/views/project/editor/pdf.pug @@ -1,6 +1,6 @@ div.full-size.pdf(ng-controller="PdfController") - .toolbar.toolbar-tall - .btn-group#recompile( + .toolbar.toolbar-pdf + .btn-group.btn-recompile-group#recompile( dropdown, tooltip-html="'"+translate('recompile_pdf')+" ({{modifierKey}} + Enter)'" tooltip-class="keyboard-tooltip" @@ -8,7 +8,7 @@ div.full-size.pdf(ng-controller="PdfController") tooltip-append-to-body="true" tooltip-placement="bottom" ) - a.btn.btn-info( + a.btn.btn-recompile( href, ng-disabled="pdf.compiling", ng-click="recompile()" @@ -19,7 +19,7 @@ div.full-size.pdf(ng-controller="PdfController") | span(ng-show="!pdf.compiling") #{translate("recompile")} span(ng-show="pdf.compiling") #{translate("compiling")}... - a.btn.btn-info.dropdown-toggle( + a.btn.btn-recompile.dropdown-toggle( href, ng-disabled="pdf.compiling", dropdown-toggle @@ -102,6 +102,7 @@ div.full-size.pdf(ng-controller="PdfController") tooltip-placement="bottom" tooltip-append-to-body="true" ) + i.fa.fa-expand i.full-screen a( href, @@ -111,6 +112,7 @@ div.full-size.pdf(ng-controller="PdfController") tooltip-placement="bottom" tooltip-append-to-body="true" ) + i.fa.fa-compress i.split-screen i.split-screen // end of toolbar diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 37638e958f..1e12fb2293 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -313,3 +313,59 @@ script(type="text/ng-template", id="userProfileModalTemplate") .modal-footer button.btn.btn-info(ng-click="done()") #{translate("done")} + +script(type="text/ng-template", id="v1ImportModalTemplate") + .modal-header + button.close(ng-click="dismiss()") × + h3 #{translate("import_project_to_v2")} + + .modal-body.v1-import-wrapper + .v1-import-step-1(ng-show="step === 1") + img.v1-import-img( + src="/img/v1-import/v2-editor.png" + alt="The new V2 Editor." + ) + h2.v1-import-title Try importing your project to V2! + p Some exciting copy about the new features: + ul + li Some stuff + li Some more stuff + li Yet more stuff + + .v1-import-step-2(ng-show="step === 2") + div.v1-import-warning(aria-label="Warning symbol.") + i.fa.fa-exclamation-triangle + h2.v1-import-title #[strong Warning:] Overleaf V2 is in beta + p Once importing your project you will lose access to the some of the features of Overleaf V1. This includes the git bridge, journal integrations, WYSIWYG and linked files. We’re working on bringing these features to V2! + p Once you have imported a project to V2 you #[strong cannot go back to V1]. + p Are you sure you want to import to V2? + + .modal-footer.v1-import-footer + div(ng-show="step === 1") + if settings.overleaf && settings.overleaf.host + a.btn.btn-primary.v1-import-btn( + ng-href=settings.overleaf.host + "/{{project.id}}" + ) #{translate("open_in_v1")} + button.btn.btn-primary.v1-import-btn( + ng-click="moveToConfirmation()" + ) #{translate("import_to_v2")} + div(ng-show="step === 2") + form( + async-form="v1Import", + name="v1ImportForm", + action="{{'/overleaf/project/'+ project.id + '/import'}}" + method="POST" + ng-cloak + ) + input(name='_csrf', type='hidden', value=csrfToken) + form-messages(for="v1ImportForm") + if settings.overleaf && settings.overleaf.host + a.btn.btn-primary.v1-import-btn( + ng-href=settings.overleaf.host + "/{{project.id}}" + ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.success}" + ) #{translate("never_mind_open_in_v1")} + input.btn.btn-primary.v1-import-btn( + type="submit", + value=translate('yes_im_sure') + ng-disabled="v1ImportForm.inflight || v1ImportForm.success" + ) diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index 0db4f49400..6f568a7b40 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -114,9 +114,9 @@ ) #{translate("delete_forever")} .row.row-spaced - if noV1Connection + each warning in warnings .col-xs-12 - .alert.alert-warning No V1 Connection + .alert.alert-warning(role="alert")= warning .col-xs-12 .card.card-thin.project-list-card diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug index ace8a80b9a..61815068f6 100644 --- a/services/web/app/views/project/list/v1-item.pug +++ b/services/web/app/views/project/list/v1-item.pug @@ -5,11 +5,10 @@ tooltip-append-to-body="true" ) span - if settings.overleaf && settings.overleaf.host - a.projectName( - href=settings.overleaf.host + "/{{project.id}}" - stop-propagation="click" - ) {{project.name}} + button.btn.btn-link.v1ProjectName( + ng-click="openV1ImportModal(project)" + stop-propagation="click" + ) {{project.name}} .col-xs-2 span.owner {{ownerName()}} diff --git a/services/web/bin/acceptance_test b/services/web/bin/acceptance_test new file mode 100755 index 0000000000..fd2e5137b5 --- /dev/null +++ b/services/web/bin/acceptance_test @@ -0,0 +1,4 @@ +#!/bin/bash +set -e; +MOCHA="node_modules/.bin/mocha --recursive --reporter spec --timeout 15000" +$MOCHA "$@" diff --git a/services/web/bin/compile_acceptance_tests b/services/web/bin/compile_acceptance_tests new file mode 100755 index 0000000000..d60ba0cc46 --- /dev/null +++ b/services/web/bin/compile_acceptance_tests @@ -0,0 +1,16 @@ +#!/bin/bash +set -e; + +COFFEE=node_modules/.bin/coffee + +echo Compiling test/acceptance/coffee; +$COFFEE -o test/acceptance/js -c test/acceptance/coffee; + +for dir in modules/*; +do + + if [ -d $dir/test/acceptance ]; then + echo Compiling $dir/test/acceptance/coffee; + $COFFEE -o $dir/test/acceptance/js -c $dir/test/acceptance/coffee; + fi +done \ No newline at end of file diff --git a/services/web/bin/compile_app b/services/web/bin/compile_app new file mode 100755 index 0000000000..b218b01a5a --- /dev/null +++ b/services/web/bin/compile_app @@ -0,0 +1,23 @@ +#!/bin/bash +set -e; + +COFFEE=node_modules/.bin/coffee + +echo Compiling app.coffee; +$COFFEE -c app.coffee; + +echo Compiling app/coffee; +$COFFEE -o app/js -c app/coffee; + +for dir in modules/*; +do + if [ -d $dir/app/coffee ]; then + echo Compiling $dir/app/coffee; + $COFFEE -o $dir/app/js -c $dir/app/coffee; + fi + + if [ -e $dir/index.coffee ]; then + echo Compiling $dir/index.coffee; + $COFFEE -c $dir/index.coffee; + fi +done \ No newline at end of file diff --git a/services/web/bin/compile_unit_tests b/services/web/bin/compile_unit_tests new file mode 100755 index 0000000000..780a189bda --- /dev/null +++ b/services/web/bin/compile_unit_tests @@ -0,0 +1,15 @@ +#!/bin/bash +set -e; + +COFFEE=node_modules/.bin/coffee + +echo Compiling test/unit/coffee; +$COFFEE -o test/unit/js -c test/unit/coffee; + +for dir in modules/*; +do + if [ -d $dir/test/unit ]; then + echo Compiling $dir/test/unit/coffee; + $COFFEE -o $dir/test/unit/js -c $dir/test/unit/coffee; + fi +done \ No newline at end of file diff --git a/services/web/bin/generate_volumes_file b/services/web/bin/generate_volumes_file new file mode 100755 index 0000000000..49ee11c508 --- /dev/null +++ b/services/web/bin/generate_volumes_file @@ -0,0 +1,26 @@ +#!/usr/bin/env python2 + +from os import listdir +from os.path import isfile, isdir, join + +volumes = [] + +for module in listdir("modules/"): + if module[0] != '.': + if isfile(join("modules", module, 'index.coffee')): + volumes.append(join("modules", module, 'index.coffee')) + for directory in ['app/coffee', 'app/views', 'public/coffee', 'test/unit/coffee', 'test/acceptance/coffee', 'test/acceptance/config', 'test/acceptance/files']: + if isdir(join("modules", module, directory)): + volumes.append(join("modules", module, directory)) + +volumes_string = map(lambda vol: "- ./" + vol + ":/app/" + vol + ":ro", volumes) +volumes_string = "\n ".join(volumes_string) + +with open("docker-shared.template.yml", "r") as f: + docker_shared_file = f.read() + +docker_shared_file = docker_shared_file.replace("MODULE_VOLUMES", volumes_string) + +with open("docker-shared.yml", "w") as f: + f.write(docker_shared_file) + diff --git a/services/web/bin/unit_test b/services/web/bin/unit_test new file mode 100755 index 0000000000..da13a441fa --- /dev/null +++ b/services/web/bin/unit_test @@ -0,0 +1,14 @@ +#!/bin/bash +set -e; + +MOCHA="node_modules/.bin/mocha --recursive --reporter spec" + +$MOCHA "$@" test/unit/js + +for dir in modules/*; +do + if [ -d $dir/test/unit/js ]; then + $MOCHA "$@" $dir/test/unit/js + fi +done + diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 69bf0a3b7c..65a8bf1e91 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -35,12 +35,12 @@ module.exports = settings = # Databases # --------- mongo: - url : 'mongodb://127.0.0.1/sharelatex' + url : process.env['MONGO_URL'] || "mongodb://127.0.0.1/sharelatex" redis: web: - host: "localhost" - port: "6379" + host: process.env['REDIS_HOST'] || "localhost" + port: process.env['REDIS_PORT'] || "6379" password: "" # websessions: @@ -74,8 +74,8 @@ module.exports = settings = # ] api: - host: "localhost" - port: "6379" + host: process.env['REDIS_HOST'] || "localhost" + port: process.env['REDIS_PORT'] || "6379" password: "" # Service locations @@ -87,6 +87,7 @@ module.exports = settings = internal: web: port: webPort = 3000 + host: process.env['LISTEN_ADDRESS'] or 'localhost' documentupdater: port: docUpdaterPort = 3003 @@ -99,7 +100,7 @@ module.exports = settings = user: httpAuthUser pass: httpAuthPass documentupdater: - url : "http://localhost:#{docUpdaterPort}" + url : "http://#{process.env['DOCUPDATER_HOST'] or 'localhost'}:#{docUpdaterPort}" thirdPartyDataStore: url : "http://localhost:3002" emptyProjectFlushDelayMiliseconds: 5 * seconds @@ -110,10 +111,10 @@ module.exports = settings = trackchanges: url : "http://localhost:3015" project_history: - enabled: false + enabled: process.env.PROJECT_HISTORY_ENABLED == 'true' or false url : "http://localhost:3054" docstore: - url : "http://localhost:3016" + url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016" pubUrl: "http://localhost:3016" chat: url: "http://localhost:3010" diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml new file mode 100644 index 0000000000..ed12abf0be --- /dev/null +++ b/services/web/docker-compose.yml @@ -0,0 +1,37 @@ +version: "2" + +volumes: + node_modules: + +services: + npm: + extends: + file: docker-shared.yml + service: app + command: npm install + + test_unit: + extends: + file: docker-shared.yml + service: app + command: npm run test:unit + + test_acceptance: + extends: + file: docker-shared.yml + service: app + environment: + REDIS_HOST: redis + MONGO_URL: "mongodb://mongo/sharelatex" + SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true' + PROJECT_HISTORY_ENABLED: 'true' + depends_on: + - redis + - mongo + command: npm run start + + redis: + image: redis + + mongo: + image: mongo:3.4.6 diff --git a/services/web/docker-shared.template.yml b/services/web/docker-shared.template.yml new file mode 100644 index 0000000000..d697e59e96 --- /dev/null +++ b/services/web/docker-shared.template.yml @@ -0,0 +1,31 @@ +version: "2" + +# We mount all the directories explicitly so that we are only mounting +# the coffee directories, so that the compiled js is only written inside +# the container, and not back to the local filesystem, where it would be +# root owned, and conflict with working outside of the container. + +services: + app: + image: node:6.9.5 + volumes: + - ./package.json:/app/package.json + - ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json + - node_modules:/app/node_modules + - ./bin:/app/bin + # Copying the whole public dir is fine for now, and needed for + # some unit tests to pass, but we will want to isolate the coffee + # and vendor js files, so that the compiled js files are not written + # back to the local filesystem. + - ./public:/app/public + - ./app.coffee:/app/app.coffee:ro + - ./app/coffee:/app/app/coffee:ro + - ./app/templates:/app/app/templates:ro + - ./app/views:/app/app/views:ro + - ./config:/app/config + - ./test/unit/coffee:/app/test/unit/coffee:ro + - ./test/acceptance/coffee:/app/test/acceptance/coffee:ro + - ./test/acceptance/files:/app/test/acceptance/files:ro + - ./test/smoke/coffee:/app/test/smoke/coffee:ro + MODULE_VOLUMES + working_dir: /app diff --git a/services/web/modules/.gitignore b/services/web/modules/.gitignore index 1d30263cb3..a310279ca9 100644 --- a/services/web/modules/.gitignore +++ b/services/web/modules/.gitignore @@ -1,12 +1,6 @@ -*/app/js -*/test/unit/js -*/index.js -ldap -admin-panel -groovehq -launchpad -learn-wiki -references-search -sharelatex-saml -templates -tpr-webmodule +# Ignore all modules except for a whitelist +* +!dropbox +!github-sync +!public-registration +!.gitignore diff --git a/services/web/package.json b/services/web/package.json index e831d929e1..59087dfa89 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -9,6 +9,17 @@ "directories": { "public": "./public" }, + "scripts": { + "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3000/status) do sleep 1; done", + "test:acceptance:run": "bin/acceptance_test $@", + "test:acceptance:dir": "npm -q run compile:acceptance_tests && npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@", + "test:acceptance": "npm -q run test:acceptance:dir -- $@ test/acceptance/js", + "test:unit": "npm -q run compile:app && npm -q run compile:unit_tests && bin/unit_test $@", + "compile:unit_tests": "bin/compile_unit_tests", + "compile:acceptance_tests": "bin/compile_acceptance_tests", + "compile:app": "bin/compile_app", + "start": "npm -q run compile:app && node app.js" + }, "dependencies": { "archiver": "0.9.0", "async": "0.6.2", @@ -98,6 +109,7 @@ "grunt-postcss": "^0.8.0", "grunt-sed": "^0.1.1", "grunt-shell": "^2.1.0", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "sandboxed-module": "0.2.0", "sinon": "^1.17.0", "timekeeper": "", diff --git a/services/web/public/coffee/directives/fineUpload.coffee b/services/web/public/coffee/directives/fineUpload.coffee index 7fffcc366a..b3d3766ba6 100644 --- a/services/web/public/coffee/directives/fineUpload.coffee +++ b/services/web/public/coffee/directives/fineUpload.coffee @@ -1,6 +1,6 @@ define [ "base" - "libs/fineuploader" + "fineuploader" ], (App, qq) -> App.directive 'fineUpload', ($timeout) -> return { diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index e93c330573..55eb2cb179 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -15,6 +15,7 @@ define [ "ide/metadata/services/metadata" "ide/graphics/services/graphics" "ide/preamble/services/preamble" + "ide/files/services/files" ], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager, LabelsManager) -> EditSession = ace.require('ace/edit_session').EditSession ModeList = ace.require('ace/ext/modelist') @@ -37,7 +38,7 @@ define [ url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}" return url - App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, metadata, graphics, preamble, $http, $q) -> + App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, metadata, graphics, preamble, files, $http, $q) -> monkeyPatchSearch($rootScope, $compile) return { @@ -106,7 +107,7 @@ define [ trackChangesManager = new TrackChangesManager(scope, editor, element) labelsManager = new LabelsManager(scope, editor, element, labels) metadataManager = new MetadataManager(scope, editor, element, metadata) - autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, labelsManager, graphics, preamble) + autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, labelsManager, graphics, preamble, files) # Prevert Ctrl|Cmd-S from triggering save dialog diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 53b3eac261..542b8ae69f 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -10,7 +10,7 @@ define [ aceSnippetManager = ace.require('ace/snippets').snippetManager class AutoCompleteManager - constructor: (@$scope, @editor, @element, @metadataManager, @labelsManager, @graphics, @preamble) -> + constructor: (@$scope, @editor, @element, @metadataManager, @labelsManager, @graphics, @preamble, @files) -> @monkeyPatchAutocomplete() @@ -41,6 +41,8 @@ define [ Graphics = @graphics Preamble = @preamble + Files = @files + GraphicsCompleter = getCompletions: (editor, session, pos, prefix, callback) -> context = Helpers.getContext(editor, pos) @@ -67,6 +69,27 @@ define [ callback null, result metadataManager = @metadataManager + FilesCompleter = + getCompletions: (editor, session, pos, prefix, callback) => + context = Helpers.getContext(editor, pos) + {lineUpToCursor, commandFragment, lineBeyondCursor, needsClosingBrace} = context + if commandFragment + match = commandFragment.match(/^\\(input|include){(\w*)/) + if match + commandName = match[1] + currentArg = match[2] + result = [] + for file in Files.getTeXFiles() + if file.id != @$scope.docId + path = file.path + result.push { + caption: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}", + value: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}", + meta: "file", + score: 50 + } + callback null, result + LabelsCompleter = getCompletions: (editor, session, pos, prefix, callback) -> context = Helpers.getContext(editor, pos) @@ -136,6 +159,7 @@ define [ ReferencesCompleter LabelsCompleter GraphicsCompleter + FilesCompleter ] disable: () -> diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee index d43824b12c..08de22d846 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee @@ -29,7 +29,7 @@ define () -> packageSnippets.push { caption: "\\usepackage{}" - snippet: "\\usepackage{}" + snippet: "\\usepackage{$1}" meta: "pkg" score: 70 } diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee index 362d163196..c9308626bc 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee @@ -22,7 +22,7 @@ define [ end = change.end range = new Range(end.row, 0, end.row, end.column) lineUpToCursor = @editor.getSession().getTextRange range - if lineUpToCursor.trim() == '%' or lineUpToCursor.startsWith '\\' + if lineUpToCursor.trim() == '%' or lineUpToCursor.slice(0, 1) == '\\' range = new Range(end.row, 0, end.row, end.column + 80) lineUpToCursor = @editor.getSession().getTextRange range commandFragment = getLastCommandFragment lineUpToCursor @@ -44,9 +44,9 @@ define [ linesContainLabel or linesContainReqPackage - lastCommandFragmentIsLabel = commandFragment?.startsWith '\\label{' - lastCommandFragmentIsPackage = commandFragment?.startsWith '\\usepackage' - lastCommandFragmentIsReqPack = commandFragment?.startsWith '\\RequirePackage' + lastCommandFragmentIsLabel = commandFragment?.slice(0, 7) == '\\label{' + lastCommandFragmentIsPackage = commandFragment?.slice(0, 11) == '\\usepackage' + lastCommandFragmentIsReqPack = commandFragment?.slice(0, 15) == '\\RequirePackage' lastCommandFragmentIsMeta = lastCommandFragmentIsPackage or lastCommandFragmentIsLabel or diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index 0881e75cc8..a4583a0efd 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -13,15 +13,15 @@ define [ @loadRootFolder() @loadDeletedDocs() @$scope.$emit "file-tree:initialized" - + @$scope.$watch "rootFolder", (rootFolder) => if rootFolder? @recalculateDocList() @_bindToSocketEvents() - + @$scope.multiSelectedCount = 0 - + $(document).on "click", => @clearMultiSelectedEntities() $scope.$digest() @@ -46,7 +46,7 @@ define [ type: "file" } @recalculateDocList() - + @ide.socket.on "reciveNewFolder", (parent_folder_id, folder) => parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder @$scope.$apply () => @@ -85,25 +85,25 @@ define [ @ide.fileTreeManager.forEachEntity (entity) -> entity.selected = false entity.selected = true - + toggleMultiSelectEntity: (entity) -> entity.multiSelected = !entity.multiSelected @$scope.multiSelectedCount = @multiSelectedCount() - + multiSelectedCount: () -> count = 0 @forEachEntity (entity) -> if entity.multiSelected count++ return count - + getMultiSelectedEntities: () -> entities = [] @forEachEntity (e) -> if e.multiSelected entities.push e return entities - + getMultiSelectedEntityChildNodes: () -> entities = @getMultiSelectedEntities() paths = {} @@ -125,13 +125,13 @@ define [ if !prefixes[path]? child_entities.push entity return child_entities - + clearMultiSelectedEntities: () -> return if @$scope.multiSelectedCount == 0 # Be efficient, this is called a lot on 'click' @forEachEntity (entity) -> entity.multiSelected = false @$scope.multiSelectedCount = 0 - + multiSelectSelectedEntity: () -> @findSelectedEntity()?.multiSelected = true @@ -140,7 +140,7 @@ define [ return false if !folder? entity = @_findEntityByPathInFolder(folder, name) return entity? - + findSelectedEntity: () -> selected = null @forEachEntity (entity) -> @@ -178,7 +178,7 @@ define [ parts = path.split("/") name = parts.shift() rest = parts.join("/") - + if name == "." return @_findEntityByPathInFolder(folder, rest) @@ -268,7 +268,7 @@ define [ type: "doc" deleted: true } - + recalculateDocList: () -> @$scope.docs = [] @forEachEntity (entity, parentFolder, path) => @@ -287,7 +287,7 @@ define [ return -1 else return 1 - + getEntityPath: (entity) -> @_getEntityPathInFolder @$scope.rootFolder, entity @@ -349,7 +349,7 @@ define [ } deleteEntity: (entity, callback = (error) ->) -> - # We'll wait for the socket.io notification to + # We'll wait for the socket.io notification to # delete from scope. return @ide.queuedHttp { method: "DELETE" @@ -367,7 +367,7 @@ define [ folder_id: parent_folder.id _csrf: window.csrfToken } - + _isChildFolder: (parent_folder, child_folder) -> parent_path = @getEntityPath(parent_folder) or "" # null if root folder child_path = @getEntityPath(child_folder) or "" # null if root folder diff --git a/services/web/public/coffee/ide/files/services/files.coffee b/services/web/public/coffee/ide/files/services/files.coffee new file mode 100644 index 0000000000..58e0598a3f --- /dev/null +++ b/services/web/public/coffee/ide/files/services/files.coffee @@ -0,0 +1,17 @@ +define [ + "base" +], (App) -> + + App.factory 'files', (ide) -> + + Files = + getTeXFiles: () -> + texFiles = [] + ide.fileTreeManager.forEachEntity (entity, folder, path) -> + if entity.type == 'doc' && entity?.name?.match?(/.*\.(tex|txt|md)/) + cloned = _.clone(entity) + cloned.path = path + texFiles.push cloned + return texFiles + + return Files diff --git a/services/web/public/coffee/libs.coffee b/services/web/public/coffee/libs.coffee index 533891548b..2b0b034fb7 100644 --- a/services/web/public/coffee/libs.coffee +++ b/services/web/public/coffee/libs.coffee @@ -1,12 +1,10 @@ define [ - "moment" "libs/angular-autocomplete/angular-autocomplete" "libs/ui-bootstrap" "libs/ng-context-menu-0.1.4" "libs/underscore-1.3.3" "libs/algolia-2.5.2" "libs/jquery.storage" - "libs/fineuploader" "libs/angular-sanitize-1.2.17" "libs/angular-cookie" "libs/passfield" diff --git a/services/web/public/coffee/main/project-list/modal-controllers.coffee b/services/web/public/coffee/main/project-list/modal-controllers.coffee index 179e0d91ce..609a16cb77 100644 --- a/services/web/public/coffee/main/project-list/modal-controllers.coffee +++ b/services/web/public/coffee/main/project-list/modal-controllers.coffee @@ -123,3 +123,13 @@ define [ $scope.onComplete = (error, name, response) -> if response.project_id? window.location = '/project/' + response.project_id + + App.controller 'V1ImportModalController', ($scope, $modalInstance, project) -> + $scope.project = project + $scope.step = 1 + + $scope.dismiss = () -> + $modalInstance.dismiss('cancel') + + $scope.moveToConfirmation = () -> + $scope.step = 2 diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 1d81504c74..f4a6951e4a 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -442,6 +442,16 @@ define [ path = "/project/#{selected_project_ids[0]}/download/zip" window.location = path + + $scope.openV1ImportModal = (project) -> + $modal.open( + templateUrl: 'v1ImportModalTemplate' + controller: 'V1ImportModalController' + size: 'lg' + windowClass: 'v1-import-modal' + resolve: + project: () -> project + ) if storedUIOpts?.filter? if storedUIOpts.filter == "tag" and storedUIOpts.selectedTagId? diff --git a/services/web/public/img/ol-brand/overleaf-o-grey.svg b/services/web/public/img/ol-brand/overleaf-o-grey.svg index 3b47c37cba..921148814f 100644 --- a/services/web/public/img/ol-brand/overleaf-o-grey.svg +++ b/services/web/public/img/ol-brand/overleaf-o-grey.svg @@ -1,18 +1 @@ - - - + \ No newline at end of file diff --git a/services/web/public/img/ol-brand/overleaf-o-white.svg b/services/web/public/img/ol-brand/overleaf-o-white.svg new file mode 100644 index 0000000000..85188b512c --- /dev/null +++ b/services/web/public/img/ol-brand/overleaf-o-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/web/public/img/ol-brand/overleaf-o.svg b/services/web/public/img/ol-brand/overleaf-o.svg index d95cee9ded..223203dc8f 100644 --- a/services/web/public/img/ol-brand/overleaf-o.svg +++ b/services/web/public/img/ol-brand/overleaf-o.svg @@ -1,10 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/services/web/public/img/v1-import/v2-editor.png b/services/web/public/img/v1-import/v2-editor.png new file mode 100644 index 0000000000..5079d77158 Binary files /dev/null and b/services/web/public/img/v1-import/v2-editor.png differ diff --git a/services/web/public/js/libs/fineuploader-5.15.4.js b/services/web/public/js/libs/fineuploader-5.15.4.js new file mode 100644 index 0000000000..606a2fb70a --- /dev/null +++ b/services/web/public/js/libs/fineuploader-5.15.4.js @@ -0,0 +1,7455 @@ +// Fine Uploader 5.15.4 - MIT licensed. http://fineuploader.com +(function(global) { + var qq = function(element) { + "use strict"; + return { + hide: function() { + element.style.display = "none"; + return this; + }, + attach: function(type, fn) { + if (element.addEventListener) { + element.addEventListener(type, fn, false); + } else if (element.attachEvent) { + element.attachEvent("on" + type, fn); + } + return function() { + qq(element).detach(type, fn); + }; + }, + detach: function(type, fn) { + if (element.removeEventListener) { + element.removeEventListener(type, fn, false); + } else if (element.attachEvent) { + element.detachEvent("on" + type, fn); + } + return this; + }, + contains: function(descendant) { + if (!descendant) { + return false; + } + if (element === descendant) { + return true; + } + if (element.contains) { + return element.contains(descendant); + } else { + return !!(descendant.compareDocumentPosition(element) & 8); + } + }, + insertBefore: function(elementB) { + elementB.parentNode.insertBefore(element, elementB); + return this; + }, + remove: function() { + element.parentNode.removeChild(element); + return this; + }, + css: function(styles) { + if (element.style == null) { + throw new qq.Error("Can't apply style to node as it is not on the HTMLElement prototype chain!"); + } + if (styles.opacity != null) { + if (typeof element.style.opacity !== "string" && typeof element.filters !== "undefined") { + styles.filter = "alpha(opacity=" + Math.round(100 * styles.opacity) + ")"; + } + } + qq.extend(element.style, styles); + return this; + }, + hasClass: function(name, considerParent) { + var re = new RegExp("(^| )" + name + "( |$)"); + return re.test(element.className) || !!(considerParent && re.test(element.parentNode.className)); + }, + addClass: function(name) { + if (!qq(element).hasClass(name)) { + element.className += " " + name; + } + return this; + }, + removeClass: function(name) { + var re = new RegExp("(^| )" + name + "( |$)"); + element.className = element.className.replace(re, " ").replace(/^\s+|\s+$/g, ""); + return this; + }, + getByClass: function(className, first) { + var candidates, result = []; + if (first && element.querySelector) { + return element.querySelector("." + className); + } else if (element.querySelectorAll) { + return element.querySelectorAll("." + className); + } + candidates = element.getElementsByTagName("*"); + qq.each(candidates, function(idx, val) { + if (qq(val).hasClass(className)) { + result.push(val); + } + }); + return first ? result[0] : result; + }, + getFirstByClass: function(className) { + return qq(element).getByClass(className, true); + }, + children: function() { + var children = [], child = element.firstChild; + while (child) { + if (child.nodeType === 1) { + children.push(child); + } + child = child.nextSibling; + } + return children; + }, + setText: function(text) { + element.innerText = text; + element.textContent = text; + return this; + }, + clearText: function() { + return qq(element).setText(""); + }, + hasAttribute: function(attrName) { + var attrVal; + if (element.hasAttribute) { + if (!element.hasAttribute(attrName)) { + return false; + } + return /^false$/i.exec(element.getAttribute(attrName)) == null; + } else { + attrVal = element[attrName]; + if (attrVal === undefined) { + return false; + } + return /^false$/i.exec(attrVal) == null; + } + } + }; + }; + (function() { + "use strict"; + qq.canvasToBlob = function(canvas, mime, quality) { + return qq.dataUriToBlob(canvas.toDataURL(mime, quality)); + }; + qq.dataUriToBlob = function(dataUri) { + var arrayBuffer, byteString, createBlob = function(data, mime) { + var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder, blobBuilder = BlobBuilder && new BlobBuilder(); + if (blobBuilder) { + blobBuilder.append(data); + return blobBuilder.getBlob(mime); + } else { + return new Blob([ data ], { + type: mime + }); + } + }, intArray, mimeString; + if (dataUri.split(",")[0].indexOf("base64") >= 0) { + byteString = atob(dataUri.split(",")[1]); + } else { + byteString = decodeURI(dataUri.split(",")[1]); + } + mimeString = dataUri.split(",")[0].split(":")[1].split(";")[0]; + arrayBuffer = new ArrayBuffer(byteString.length); + intArray = new Uint8Array(arrayBuffer); + qq.each(byteString, function(idx, character) { + intArray[idx] = character.charCodeAt(0); + }); + return createBlob(arrayBuffer, mimeString); + }; + qq.log = function(message, level) { + if (window.console) { + if (!level || level === "info") { + window.console.log(message); + } else { + if (window.console[level]) { + window.console[level](message); + } else { + window.console.log("<" + level + "> " + message); + } + } + } + }; + qq.isObject = function(variable) { + return variable && !variable.nodeType && Object.prototype.toString.call(variable) === "[object Object]"; + }; + qq.isFunction = function(variable) { + return typeof variable === "function"; + }; + qq.isArray = function(value) { + return Object.prototype.toString.call(value) === "[object Array]" || value && window.ArrayBuffer && value.buffer && value.buffer.constructor === ArrayBuffer; + }; + qq.isItemList = function(maybeItemList) { + return Object.prototype.toString.call(maybeItemList) === "[object DataTransferItemList]"; + }; + qq.isNodeList = function(maybeNodeList) { + return Object.prototype.toString.call(maybeNodeList) === "[object NodeList]" || maybeNodeList.item && maybeNodeList.namedItem; + }; + qq.isString = function(maybeString) { + return Object.prototype.toString.call(maybeString) === "[object String]"; + }; + qq.trimStr = function(string) { + if (String.prototype.trim) { + return string.trim(); + } + return string.replace(/^\s+|\s+$/g, ""); + }; + qq.format = function(str) { + var args = Array.prototype.slice.call(arguments, 1), newStr = str, nextIdxToReplace = newStr.indexOf("{}"); + qq.each(args, function(idx, val) { + var strBefore = newStr.substring(0, nextIdxToReplace), strAfter = newStr.substring(nextIdxToReplace + 2); + newStr = strBefore + val + strAfter; + nextIdxToReplace = newStr.indexOf("{}", nextIdxToReplace + val.length); + if (nextIdxToReplace < 0) { + return false; + } + }); + return newStr; + }; + qq.isFile = function(maybeFile) { + return window.File && Object.prototype.toString.call(maybeFile) === "[object File]"; + }; + qq.isFileList = function(maybeFileList) { + return window.FileList && Object.prototype.toString.call(maybeFileList) === "[object FileList]"; + }; + qq.isFileOrInput = function(maybeFileOrInput) { + return qq.isFile(maybeFileOrInput) || qq.isInput(maybeFileOrInput); + }; + qq.isInput = function(maybeInput, notFile) { + var evaluateType = function(type) { + var normalizedType = type.toLowerCase(); + if (notFile) { + return normalizedType !== "file"; + } + return normalizedType === "file"; + }; + if (window.HTMLInputElement) { + if (Object.prototype.toString.call(maybeInput) === "[object HTMLInputElement]") { + if (maybeInput.type && evaluateType(maybeInput.type)) { + return true; + } + } + } + if (maybeInput.tagName) { + if (maybeInput.tagName.toLowerCase() === "input") { + if (maybeInput.type && evaluateType(maybeInput.type)) { + return true; + } + } + } + return false; + }; + qq.isBlob = function(maybeBlob) { + if (window.Blob && Object.prototype.toString.call(maybeBlob) === "[object Blob]") { + return true; + } + }; + qq.isXhrUploadSupported = function() { + var input = document.createElement("input"); + input.type = "file"; + return input.multiple !== undefined && typeof File !== "undefined" && typeof FormData !== "undefined" && typeof qq.createXhrInstance().upload !== "undefined"; + }; + qq.createXhrInstance = function() { + if (window.XMLHttpRequest) { + return new XMLHttpRequest(); + } + try { + return new ActiveXObject("MSXML2.XMLHTTP.3.0"); + } catch (error) { + qq.log("Neither XHR or ActiveX are supported!", "error"); + return null; + } + }; + qq.isFolderDropSupported = function(dataTransfer) { + return dataTransfer.items && dataTransfer.items.length > 0 && dataTransfer.items[0].webkitGetAsEntry; + }; + qq.isFileChunkingSupported = function() { + return !qq.androidStock() && qq.isXhrUploadSupported() && (File.prototype.slice !== undefined || File.prototype.webkitSlice !== undefined || File.prototype.mozSlice !== undefined); + }; + qq.sliceBlob = function(fileOrBlob, start, end) { + var slicer = fileOrBlob.slice || fileOrBlob.mozSlice || fileOrBlob.webkitSlice; + return slicer.call(fileOrBlob, start, end); + }; + qq.arrayBufferToHex = function(buffer) { + var bytesAsHex = "", bytes = new Uint8Array(buffer); + qq.each(bytes, function(idx, byt) { + var byteAsHexStr = byt.toString(16); + if (byteAsHexStr.length < 2) { + byteAsHexStr = "0" + byteAsHexStr; + } + bytesAsHex += byteAsHexStr; + }); + return bytesAsHex; + }; + qq.readBlobToHex = function(blob, startOffset, length) { + var initialBlob = qq.sliceBlob(blob, startOffset, startOffset + length), fileReader = new FileReader(), promise = new qq.Promise(); + fileReader.onload = function() { + promise.success(qq.arrayBufferToHex(fileReader.result)); + }; + fileReader.onerror = promise.failure; + fileReader.readAsArrayBuffer(initialBlob); + return promise; + }; + qq.extend = function(first, second, extendNested) { + qq.each(second, function(prop, val) { + if (extendNested && qq.isObject(val)) { + if (first[prop] === undefined) { + first[prop] = {}; + } + qq.extend(first[prop], val, true); + } else { + first[prop] = val; + } + }); + return first; + }; + qq.override = function(target, sourceFn) { + var super_ = {}, source = sourceFn(super_); + qq.each(source, function(srcPropName, srcPropVal) { + if (target[srcPropName] !== undefined) { + super_[srcPropName] = target[srcPropName]; + } + target[srcPropName] = srcPropVal; + }); + return target; + }; + qq.indexOf = function(arr, elt, from) { + if (arr.indexOf) { + return arr.indexOf(elt, from); + } + from = from || 0; + var len = arr.length; + if (from < 0) { + from += len; + } + for (;from < len; from += 1) { + if (arr.hasOwnProperty(from) && arr[from] === elt) { + return from; + } + } + return -1; + }; + qq.getUniqueId = function() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == "x" ? r : r & 3 | 8; + return v.toString(16); + }); + }; + qq.ie = function() { + return navigator.userAgent.indexOf("MSIE") !== -1 || navigator.userAgent.indexOf("Trident") !== -1; + }; + qq.ie7 = function() { + return navigator.userAgent.indexOf("MSIE 7") !== -1; + }; + qq.ie8 = function() { + return navigator.userAgent.indexOf("MSIE 8") !== -1; + }; + qq.ie10 = function() { + return navigator.userAgent.indexOf("MSIE 10") !== -1; + }; + qq.ie11 = function() { + return qq.ie() && navigator.userAgent.indexOf("rv:11") !== -1; + }; + qq.edge = function() { + return navigator.userAgent.indexOf("Edge") >= 0; + }; + qq.safari = function() { + return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1; + }; + qq.chrome = function() { + return navigator.vendor !== undefined && navigator.vendor.indexOf("Google") !== -1; + }; + qq.opera = function() { + return navigator.vendor !== undefined && navigator.vendor.indexOf("Opera") !== -1; + }; + qq.firefox = function() { + return !qq.edge() && !qq.ie11() && navigator.userAgent.indexOf("Mozilla") !== -1 && navigator.vendor !== undefined && navigator.vendor === ""; + }; + qq.windows = function() { + return navigator.platform === "Win32"; + }; + qq.android = function() { + return navigator.userAgent.toLowerCase().indexOf("android") !== -1; + }; + qq.androidStock = function() { + return qq.android() && navigator.userAgent.toLowerCase().indexOf("chrome") < 0; + }; + qq.ios6 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 6_") !== -1; + }; + qq.ios7 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 7_") !== -1; + }; + qq.ios8 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 8_") !== -1; + }; + qq.ios800 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 8_0 ") !== -1; + }; + qq.ios = function() { + return navigator.userAgent.indexOf("iPad") !== -1 || navigator.userAgent.indexOf("iPod") !== -1 || navigator.userAgent.indexOf("iPhone") !== -1; + }; + qq.iosChrome = function() { + return qq.ios() && navigator.userAgent.indexOf("CriOS") !== -1; + }; + qq.iosSafari = function() { + return qq.ios() && !qq.iosChrome() && navigator.userAgent.indexOf("Safari") !== -1; + }; + qq.iosSafariWebView = function() { + return qq.ios() && !qq.iosChrome() && !qq.iosSafari(); + }; + qq.preventDefault = function(e) { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + }; + qq.toElement = function() { + var div = document.createElement("div"); + return function(html) { + div.innerHTML = html; + var element = div.firstChild; + div.removeChild(element); + return element; + }; + }(); + qq.each = function(iterableItem, callback) { + var keyOrIndex, retVal; + if (iterableItem) { + if (window.Storage && iterableItem.constructor === window.Storage) { + for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { + retVal = callback(iterableItem.key(keyOrIndex), iterableItem.getItem(iterableItem.key(keyOrIndex))); + if (retVal === false) { + break; + } + } + } else if (qq.isArray(iterableItem) || qq.isItemList(iterableItem) || qq.isNodeList(iterableItem)) { + for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { + retVal = callback(keyOrIndex, iterableItem[keyOrIndex]); + if (retVal === false) { + break; + } + } + } else if (qq.isString(iterableItem)) { + for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { + retVal = callback(keyOrIndex, iterableItem.charAt(keyOrIndex)); + if (retVal === false) { + break; + } + } + } else { + for (keyOrIndex in iterableItem) { + if (Object.prototype.hasOwnProperty.call(iterableItem, keyOrIndex)) { + retVal = callback(keyOrIndex, iterableItem[keyOrIndex]); + if (retVal === false) { + break; + } + } + } + } + } + }; + qq.bind = function(oldFunc, context) { + if (qq.isFunction(oldFunc)) { + var args = Array.prototype.slice.call(arguments, 2); + return function() { + var newArgs = qq.extend([], args); + if (arguments.length) { + newArgs = newArgs.concat(Array.prototype.slice.call(arguments)); + } + return oldFunc.apply(context, newArgs); + }; + } + throw new Error("first parameter must be a function!"); + }; + qq.obj2url = function(obj, temp, prefixDone) { + var uristrings = [], prefix = "&", add = function(nextObj, i) { + var nextTemp = temp ? /\[\]$/.test(temp) ? temp : temp + "[" + i + "]" : i; + if (nextTemp !== "undefined" && i !== "undefined") { + uristrings.push(typeof nextObj === "object" ? qq.obj2url(nextObj, nextTemp, true) : Object.prototype.toString.call(nextObj) === "[object Function]" ? encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj()) : encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj)); + } + }; + if (!prefixDone && temp) { + prefix = /\?/.test(temp) ? /\?$/.test(temp) ? "" : "&" : "?"; + uristrings.push(temp); + uristrings.push(qq.obj2url(obj)); + } else if (Object.prototype.toString.call(obj) === "[object Array]" && typeof obj !== "undefined") { + qq.each(obj, function(idx, val) { + add(val, idx); + }); + } else if (typeof obj !== "undefined" && obj !== null && typeof obj === "object") { + qq.each(obj, function(prop, val) { + add(val, prop); + }); + } else { + uristrings.push(encodeURIComponent(temp) + "=" + encodeURIComponent(obj)); + } + if (temp) { + return uristrings.join(prefix); + } else { + return uristrings.join(prefix).replace(/^&/, "").replace(/%20/g, "+"); + } + }; + qq.obj2FormData = function(obj, formData, arrayKeyName) { + if (!formData) { + formData = new FormData(); + } + qq.each(obj, function(key, val) { + key = arrayKeyName ? arrayKeyName + "[" + key + "]" : key; + if (qq.isObject(val)) { + qq.obj2FormData(val, formData, key); + } else if (qq.isFunction(val)) { + formData.append(key, val()); + } else { + formData.append(key, val); + } + }); + return formData; + }; + qq.obj2Inputs = function(obj, form) { + var input; + if (!form) { + form = document.createElement("form"); + } + qq.obj2FormData(obj, { + append: function(key, val) { + input = document.createElement("input"); + input.setAttribute("name", key); + input.setAttribute("value", val); + form.appendChild(input); + } + }); + return form; + }; + qq.parseJson = function(json) { + if (window.JSON && qq.isFunction(JSON.parse)) { + return JSON.parse(json); + } else { + return eval("(" + json + ")"); + } + }; + qq.getExtension = function(filename) { + var extIdx = filename.lastIndexOf(".") + 1; + if (extIdx > 0) { + return filename.substr(extIdx, filename.length - extIdx); + } + }; + qq.getFilename = function(blobOrFileInput) { + if (qq.isInput(blobOrFileInput)) { + return blobOrFileInput.value.replace(/.*(\/|\\)/, ""); + } else if (qq.isFile(blobOrFileInput)) { + if (blobOrFileInput.fileName !== null && blobOrFileInput.fileName !== undefined) { + return blobOrFileInput.fileName; + } + } + return blobOrFileInput.name; + }; + qq.DisposeSupport = function() { + var disposers = []; + return { + dispose: function() { + var disposer; + do { + disposer = disposers.shift(); + if (disposer) { + disposer(); + } + } while (disposer); + }, + attach: function() { + var args = arguments; + this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1))); + }, + addDisposer: function(disposeFunction) { + disposers.push(disposeFunction); + } + }; + }; + })(); + (function() { + "use strict"; + if (typeof define === "function" && define.amd) { + define(function() { + return qq; + }); + } else if (typeof module !== "undefined" && module.exports) { + module.exports = qq; + } else { + global.qq = qq; + } + })(); + (function() { + "use strict"; + qq.Error = function(message) { + this.message = "[Fine Uploader " + qq.version + "] " + message; + }; + qq.Error.prototype = new Error(); + })(); + qq.version = "5.15.4"; + qq.supportedFeatures = function() { + "use strict"; + var supportsUploading, supportsUploadingBlobs, supportsFileDrop, supportsAjaxFileUploading, supportsFolderDrop, supportsChunking, supportsResume, supportsUploadViaPaste, supportsUploadCors, supportsDeleteFileXdr, supportsDeleteFileCorsXhr, supportsDeleteFileCors, supportsFolderSelection, supportsImagePreviews, supportsUploadProgress; + function testSupportsFileInputElement() { + var supported = true, tempInput; + try { + tempInput = document.createElement("input"); + tempInput.type = "file"; + qq(tempInput).hide(); + if (tempInput.disabled) { + supported = false; + } + } catch (ex) { + supported = false; + } + return supported; + } + function isChrome21OrHigher() { + return (qq.chrome() || qq.opera()) && navigator.userAgent.match(/Chrome\/[2][1-9]|Chrome\/[3-9][0-9]/) !== undefined; + } + function isChrome14OrHigher() { + return (qq.chrome() || qq.opera()) && navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined; + } + function isCrossOriginXhrSupported() { + if (window.XMLHttpRequest) { + var xhr = qq.createXhrInstance(); + return xhr.withCredentials !== undefined; + } + return false; + } + function isXdrSupported() { + return window.XDomainRequest !== undefined; + } + function isCrossOriginAjaxSupported() { + if (isCrossOriginXhrSupported()) { + return true; + } + return isXdrSupported(); + } + function isFolderSelectionSupported() { + return document.createElement("input").webkitdirectory !== undefined; + } + function isLocalStorageSupported() { + try { + return !!window.localStorage && qq.isFunction(window.localStorage.setItem); + } catch (error) { + return false; + } + } + function isDragAndDropSupported() { + var span = document.createElement("span"); + return ("draggable" in span || "ondragstart" in span && "ondrop" in span) && !qq.android() && !qq.ios(); + } + supportsUploading = testSupportsFileInputElement(); + supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported(); + supportsUploadingBlobs = supportsAjaxFileUploading && !qq.androidStock(); + supportsFileDrop = supportsAjaxFileUploading && isDragAndDropSupported(); + supportsFolderDrop = supportsFileDrop && isChrome21OrHigher(); + supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported(); + supportsResume = supportsAjaxFileUploading && supportsChunking && isLocalStorageSupported(); + supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher(); + supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading); + supportsDeleteFileCorsXhr = isCrossOriginXhrSupported(); + supportsDeleteFileXdr = isXdrSupported(); + supportsDeleteFileCors = isCrossOriginAjaxSupported(); + supportsFolderSelection = isFolderSelectionSupported(); + supportsImagePreviews = supportsAjaxFileUploading && window.FileReader !== undefined; + supportsUploadProgress = function() { + if (supportsAjaxFileUploading) { + return !qq.androidStock() && !qq.iosChrome(); + } + return false; + }(); + return { + ajaxUploading: supportsAjaxFileUploading, + blobUploading: supportsUploadingBlobs, + canDetermineSize: supportsAjaxFileUploading, + chunking: supportsChunking, + deleteFileCors: supportsDeleteFileCors, + deleteFileCorsXdr: supportsDeleteFileXdr, + deleteFileCorsXhr: supportsDeleteFileCorsXhr, + dialogElement: !!window.HTMLDialogElement, + fileDrop: supportsFileDrop, + folderDrop: supportsFolderDrop, + folderSelection: supportsFolderSelection, + imagePreviews: supportsImagePreviews, + imageValidation: supportsImagePreviews, + itemSizeValidation: supportsAjaxFileUploading, + pause: supportsChunking, + progressBar: supportsUploadProgress, + resume: supportsResume, + scaling: supportsImagePreviews && supportsUploadingBlobs, + tiffPreviews: qq.safari(), + unlimitedScaledImageSize: !qq.ios(), + uploading: supportsUploading, + uploadCors: supportsUploadCors, + uploadCustomHeaders: supportsAjaxFileUploading, + uploadNonMultipart: supportsAjaxFileUploading, + uploadViaPaste: supportsUploadViaPaste + }; + }(); + qq.isGenericPromise = function(maybePromise) { + "use strict"; + return !!(maybePromise && maybePromise.then && qq.isFunction(maybePromise.then)); + }; + qq.Promise = function() { + "use strict"; + var successArgs, failureArgs, successCallbacks = [], failureCallbacks = [], doneCallbacks = [], state = 0; + qq.extend(this, { + then: function(onSuccess, onFailure) { + if (state === 0) { + if (onSuccess) { + successCallbacks.push(onSuccess); + } + if (onFailure) { + failureCallbacks.push(onFailure); + } + } else if (state === -1) { + onFailure && onFailure.apply(null, failureArgs); + } else if (onSuccess) { + onSuccess.apply(null, successArgs); + } + return this; + }, + done: function(callback) { + if (state === 0) { + doneCallbacks.push(callback); + } else { + callback.apply(null, failureArgs === undefined ? successArgs : failureArgs); + } + return this; + }, + success: function() { + state = 1; + successArgs = arguments; + if (successCallbacks.length) { + qq.each(successCallbacks, function(idx, callback) { + callback.apply(null, successArgs); + }); + } + if (doneCallbacks.length) { + qq.each(doneCallbacks, function(idx, callback) { + callback.apply(null, successArgs); + }); + } + return this; + }, + failure: function() { + state = -1; + failureArgs = arguments; + if (failureCallbacks.length) { + qq.each(failureCallbacks, function(idx, callback) { + callback.apply(null, failureArgs); + }); + } + if (doneCallbacks.length) { + qq.each(doneCallbacks, function(idx, callback) { + callback.apply(null, failureArgs); + }); + } + return this; + } + }); + }; + qq.BlobProxy = function(referenceBlob, onCreate) { + "use strict"; + qq.extend(this, { + referenceBlob: referenceBlob, + create: function() { + return onCreate(referenceBlob); + } + }); + }; + qq.UploadButton = function(o) { + "use strict"; + var self = this, disposeSupport = new qq.DisposeSupport(), options = { + acceptFiles: null, + element: null, + focusClass: "qq-upload-button-focus", + folders: false, + hoverClass: "qq-upload-button-hover", + ios8BrowserCrashWorkaround: false, + multiple: false, + name: "qqfile", + onChange: function(input) {}, + title: null + }, input, buttonId; + qq.extend(options, o); + buttonId = qq.getUniqueId(); + function createInput() { + var input = document.createElement("input"); + input.setAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME, buttonId); + input.setAttribute("title", options.title); + self.setMultiple(options.multiple, input); + if (options.folders && qq.supportedFeatures.folderSelection) { + input.setAttribute("webkitdirectory", ""); + } + if (options.acceptFiles) { + input.setAttribute("accept", options.acceptFiles); + } + input.setAttribute("type", "file"); + input.setAttribute("name", options.name); + qq(input).css({ + position: "absolute", + right: 0, + top: 0, + fontFamily: "Arial", + fontSize: qq.ie() && !qq.ie8() ? "3500px" : "118px", + margin: 0, + padding: 0, + cursor: "pointer", + opacity: 0 + }); + !qq.ie7() && qq(input).css({ + height: "100%" + }); + options.element.appendChild(input); + disposeSupport.attach(input, "change", function() { + options.onChange(input); + }); + disposeSupport.attach(input, "mouseover", function() { + qq(options.element).addClass(options.hoverClass); + }); + disposeSupport.attach(input, "mouseout", function() { + qq(options.element).removeClass(options.hoverClass); + }); + disposeSupport.attach(input, "focus", function() { + qq(options.element).addClass(options.focusClass); + }); + disposeSupport.attach(input, "blur", function() { + qq(options.element).removeClass(options.focusClass); + }); + return input; + } + qq(options.element).css({ + position: "relative", + overflow: "hidden", + direction: "ltr" + }); + qq.extend(this, { + getInput: function() { + return input; + }, + getButtonId: function() { + return buttonId; + }, + setMultiple: function(isMultiple, optInput) { + var input = optInput || this.getInput(); + if (options.ios8BrowserCrashWorkaround && qq.ios8() && (qq.iosChrome() || qq.iosSafariWebView())) { + input.setAttribute("multiple", ""); + } else { + if (isMultiple) { + input.setAttribute("multiple", ""); + } else { + input.removeAttribute("multiple"); + } + } + }, + setAcceptFiles: function(acceptFiles) { + if (acceptFiles !== options.acceptFiles) { + input.setAttribute("accept", acceptFiles); + } + }, + reset: function() { + if (input.parentNode) { + qq(input).remove(); + } + qq(options.element).removeClass(options.focusClass); + input = null; + input = createInput(); + } + }); + input = createInput(); + }; + qq.UploadButton.BUTTON_ID_ATTR_NAME = "qq-button-id"; + qq.UploadData = function(uploaderProxy) { + "use strict"; + var data = [], byUuid = {}, byStatus = {}, byProxyGroupId = {}, byBatchId = {}; + function getDataByIds(idOrIds) { + if (qq.isArray(idOrIds)) { + var entries = []; + qq.each(idOrIds, function(idx, id) { + entries.push(data[id]); + }); + return entries; + } + return data[idOrIds]; + } + function getDataByUuids(uuids) { + if (qq.isArray(uuids)) { + var entries = []; + qq.each(uuids, function(idx, uuid) { + entries.push(data[byUuid[uuid]]); + }); + return entries; + } + return data[byUuid[uuids]]; + } + function getDataByStatus(status) { + var statusResults = [], statuses = [].concat(status); + qq.each(statuses, function(index, statusEnum) { + var statusResultIndexes = byStatus[statusEnum]; + if (statusResultIndexes !== undefined) { + qq.each(statusResultIndexes, function(i, dataIndex) { + statusResults.push(data[dataIndex]); + }); + } + }); + return statusResults; + } + qq.extend(this, { + addFile: function(spec) { + var status = spec.status || qq.status.SUBMITTING, id = data.push({ + name: spec.name, + originalName: spec.name, + uuid: spec.uuid, + size: spec.size == null ? -1 : spec.size, + status: status + }) - 1; + if (spec.batchId) { + data[id].batchId = spec.batchId; + if (byBatchId[spec.batchId] === undefined) { + byBatchId[spec.batchId] = []; + } + byBatchId[spec.batchId].push(id); + } + if (spec.proxyGroupId) { + data[id].proxyGroupId = spec.proxyGroupId; + if (byProxyGroupId[spec.proxyGroupId] === undefined) { + byProxyGroupId[spec.proxyGroupId] = []; + } + byProxyGroupId[spec.proxyGroupId].push(id); + } + data[id].id = id; + byUuid[spec.uuid] = id; + if (byStatus[status] === undefined) { + byStatus[status] = []; + } + byStatus[status].push(id); + spec.onBeforeStatusChange && spec.onBeforeStatusChange(id); + uploaderProxy.onStatusChange(id, null, status); + return id; + }, + retrieve: function(optionalFilter) { + if (qq.isObject(optionalFilter) && data.length) { + if (optionalFilter.id !== undefined) { + return getDataByIds(optionalFilter.id); + } else if (optionalFilter.uuid !== undefined) { + return getDataByUuids(optionalFilter.uuid); + } else if (optionalFilter.status) { + return getDataByStatus(optionalFilter.status); + } + } else { + return qq.extend([], data, true); + } + }, + reset: function() { + data = []; + byUuid = {}; + byStatus = {}; + byBatchId = {}; + }, + setStatus: function(id, newStatus) { + var oldStatus = data[id].status, byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], id); + byStatus[oldStatus].splice(byStatusOldStatusIndex, 1); + data[id].status = newStatus; + if (byStatus[newStatus] === undefined) { + byStatus[newStatus] = []; + } + byStatus[newStatus].push(id); + uploaderProxy.onStatusChange(id, oldStatus, newStatus); + }, + uuidChanged: function(id, newUuid) { + var oldUuid = data[id].uuid; + data[id].uuid = newUuid; + byUuid[newUuid] = id; + delete byUuid[oldUuid]; + }, + updateName: function(id, newName) { + data[id].name = newName; + }, + updateSize: function(id, newSize) { + data[id].size = newSize; + }, + setParentId: function(targetId, parentId) { + data[targetId].parentId = parentId; + }, + getIdsInProxyGroup: function(id) { + var proxyGroupId = data[id].proxyGroupId; + if (proxyGroupId) { + return byProxyGroupId[proxyGroupId]; + } + return []; + }, + getIdsInBatch: function(id) { + var batchId = data[id].batchId; + return byBatchId[batchId]; + } + }); + }; + qq.status = { + SUBMITTING: "submitting", + SUBMITTED: "submitted", + REJECTED: "rejected", + QUEUED: "queued", + CANCELED: "canceled", + PAUSED: "paused", + UPLOADING: "uploading", + UPLOAD_RETRYING: "retrying upload", + UPLOAD_SUCCESSFUL: "upload successful", + UPLOAD_FAILED: "upload failed", + DELETE_FAILED: "delete failed", + DELETING: "deleting", + DELETED: "deleted" + }; + (function() { + "use strict"; + qq.basePublicApi = { + addBlobs: function(blobDataOrArray, params, endpoint) { + this.addFiles(blobDataOrArray, params, endpoint); + }, + addInitialFiles: function(cannedFileList) { + var self = this; + qq.each(cannedFileList, function(index, cannedFile) { + self._addCannedFile(cannedFile); + }); + }, + addFiles: function(data, params, endpoint) { + this._maybeHandleIos8SafariWorkaround(); + var batchId = this._storedIds.length === 0 ? qq.getUniqueId() : this._currentBatchId, processBlob = qq.bind(function(blob) { + this._handleNewFile({ + blob: blob, + name: this._options.blobs.defaultName + }, batchId, verifiedFiles); + }, this), processBlobData = qq.bind(function(blobData) { + this._handleNewFile(blobData, batchId, verifiedFiles); + }, this), processCanvas = qq.bind(function(canvas) { + var blob = qq.canvasToBlob(canvas); + this._handleNewFile({ + blob: blob, + name: this._options.blobs.defaultName + ".png" + }, batchId, verifiedFiles); + }, this), processCanvasData = qq.bind(function(canvasData) { + var normalizedQuality = canvasData.quality && canvasData.quality / 100, blob = qq.canvasToBlob(canvasData.canvas, canvasData.type, normalizedQuality); + this._handleNewFile({ + blob: blob, + name: canvasData.name + }, batchId, verifiedFiles); + }, this), processFileOrInput = qq.bind(function(fileOrInput) { + if (qq.isInput(fileOrInput) && qq.supportedFeatures.ajaxUploading) { + var files = Array.prototype.slice.call(fileOrInput.files), self = this; + qq.each(files, function(idx, file) { + self._handleNewFile(file, batchId, verifiedFiles); + }); + } else { + this._handleNewFile(fileOrInput, batchId, verifiedFiles); + } + }, this), normalizeData = function() { + if (qq.isFileList(data)) { + data = Array.prototype.slice.call(data); + } + data = [].concat(data); + }, self = this, verifiedFiles = []; + this._currentBatchId = batchId; + if (data) { + normalizeData(); + qq.each(data, function(idx, fileContainer) { + if (qq.isFileOrInput(fileContainer)) { + processFileOrInput(fileContainer); + } else if (qq.isBlob(fileContainer)) { + processBlob(fileContainer); + } else if (qq.isObject(fileContainer)) { + if (fileContainer.blob && fileContainer.name) { + processBlobData(fileContainer); + } else if (fileContainer.canvas && fileContainer.name) { + processCanvasData(fileContainer); + } + } else if (fileContainer.tagName && fileContainer.tagName.toLowerCase() === "canvas") { + processCanvas(fileContainer); + } else { + self.log(fileContainer + " is not a valid file container! Ignoring!", "warn"); + } + }); + this.log("Received " + verifiedFiles.length + " files."); + this._prepareItemsForUpload(verifiedFiles, params, endpoint); + } + }, + cancel: function(id) { + this._handler.cancel(id); + }, + cancelAll: function() { + var storedIdsCopy = [], self = this; + qq.extend(storedIdsCopy, this._storedIds); + qq.each(storedIdsCopy, function(idx, storedFileId) { + self.cancel(storedFileId); + }); + this._handler.cancelAll(); + }, + clearStoredFiles: function() { + this._storedIds = []; + }, + continueUpload: function(id) { + var uploadData = this._uploadData.retrieve({ + id: id + }); + if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) { + return false; + } + if (uploadData.status === qq.status.PAUSED) { + this.log(qq.format("Paused file ID {} ({}) will be continued. Not paused.", id, this.getName(id))); + this._uploadFile(id); + return true; + } else { + this.log(qq.format("Ignoring continue for file ID {} ({}). Not paused.", id, this.getName(id)), "error"); + } + return false; + }, + deleteFile: function(id) { + return this._onSubmitDelete(id); + }, + doesExist: function(fileOrBlobId) { + return this._handler.isValid(fileOrBlobId); + }, + drawThumbnail: function(fileId, imgOrCanvas, maxSize, fromServer, customResizeFunction) { + var promiseToReturn = new qq.Promise(), fileOrUrl, options; + if (this._imageGenerator) { + fileOrUrl = this._thumbnailUrls[fileId]; + options = { + customResizeFunction: customResizeFunction, + maxSize: maxSize > 0 ? maxSize : null, + scale: maxSize > 0 + }; + if (!fromServer && qq.supportedFeatures.imagePreviews) { + fileOrUrl = this.getFile(fileId); + } + if (fileOrUrl == null) { + promiseToReturn.failure({ + container: imgOrCanvas, + error: "File or URL not found." + }); + } else { + this._imageGenerator.generate(fileOrUrl, imgOrCanvas, options).then(function success(modifiedContainer) { + promiseToReturn.success(modifiedContainer); + }, function failure(container, reason) { + promiseToReturn.failure({ + container: container, + error: reason || "Problem generating thumbnail" + }); + }); + } + } else { + promiseToReturn.failure({ + container: imgOrCanvas, + error: "Missing image generator module" + }); + } + return promiseToReturn; + }, + getButton: function(fileId) { + return this._getButton(this._buttonIdsForFileIds[fileId]); + }, + getEndpoint: function(fileId) { + return this._endpointStore.get(fileId); + }, + getFile: function(fileOrBlobId) { + return this._handler.getFile(fileOrBlobId) || null; + }, + getInProgress: function() { + return this._uploadData.retrieve({ + status: [ qq.status.UPLOADING, qq.status.UPLOAD_RETRYING, qq.status.QUEUED ] + }).length; + }, + getName: function(id) { + return this._uploadData.retrieve({ + id: id + }).name; + }, + getParentId: function(id) { + var uploadDataEntry = this.getUploads({ + id: id + }), parentId = null; + if (uploadDataEntry) { + if (uploadDataEntry.parentId !== undefined) { + parentId = uploadDataEntry.parentId; + } + } + return parentId; + }, + getResumableFilesData: function() { + return this._handler.getResumableFilesData(); + }, + getSize: function(id) { + return this._uploadData.retrieve({ + id: id + }).size; + }, + getNetUploads: function() { + return this._netUploaded; + }, + getRemainingAllowedItems: function() { + var allowedItems = this._currentItemLimit; + if (allowedItems > 0) { + return allowedItems - this._netUploadedOrQueued; + } + return null; + }, + getUploads: function(optionalFilter) { + return this._uploadData.retrieve(optionalFilter); + }, + getUuid: function(id) { + return this._uploadData.retrieve({ + id: id + }).uuid; + }, + log: function(str, level) { + if (this._options.debug && (!level || level === "info")) { + qq.log("[Fine Uploader " + qq.version + "] " + str); + } else if (level && level !== "info") { + qq.log("[Fine Uploader " + qq.version + "] " + str, level); + } + }, + pauseUpload: function(id) { + var uploadData = this._uploadData.retrieve({ + id: id + }); + if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) { + return false; + } + if (qq.indexOf([ qq.status.UPLOADING, qq.status.UPLOAD_RETRYING ], uploadData.status) >= 0) { + if (this._handler.pause(id)) { + this._uploadData.setStatus(id, qq.status.PAUSED); + return true; + } else { + this.log(qq.format("Unable to pause file ID {} ({}).", id, this.getName(id)), "error"); + } + } else { + this.log(qq.format("Ignoring pause for file ID {} ({}). Not in progress.", id, this.getName(id)), "error"); + } + return false; + }, + removeFileRef: function(id) { + this._handler.expunge(id); + }, + reset: function() { + this.log("Resetting uploader..."); + this._handler.reset(); + this._storedIds = []; + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + this._thumbnailUrls = []; + qq.each(this._buttons, function(idx, button) { + button.reset(); + }); + this._paramsStore.reset(); + this._endpointStore.reset(); + this._netUploadedOrQueued = 0; + this._netUploaded = 0; + this._uploadData.reset(); + this._buttonIdsForFileIds = []; + this._pasteHandler && this._pasteHandler.reset(); + this._options.session.refreshOnReset && this._refreshSessionData(); + this._succeededSinceLastAllComplete = []; + this._failedSinceLastAllComplete = []; + this._totalProgress && this._totalProgress.reset(); + }, + retry: function(id) { + return this._manualRetry(id); + }, + scaleImage: function(id, specs) { + var self = this; + return qq.Scaler.prototype.scaleImage(id, specs, { + log: qq.bind(self.log, self), + getFile: qq.bind(self.getFile, self), + uploadData: self._uploadData + }); + }, + setCustomHeaders: function(headers, id) { + this._customHeadersStore.set(headers, id); + }, + setDeleteFileCustomHeaders: function(headers, id) { + this._deleteFileCustomHeadersStore.set(headers, id); + }, + setDeleteFileEndpoint: function(endpoint, id) { + this._deleteFileEndpointStore.set(endpoint, id); + }, + setDeleteFileParams: function(params, id) { + this._deleteFileParamsStore.set(params, id); + }, + setEndpoint: function(endpoint, id) { + this._endpointStore.set(endpoint, id); + }, + setForm: function(elementOrId) { + this._updateFormSupportAndParams(elementOrId); + }, + setItemLimit: function(newItemLimit) { + this._currentItemLimit = newItemLimit; + }, + setName: function(id, newName) { + this._uploadData.updateName(id, newName); + }, + setParams: function(params, id) { + this._paramsStore.set(params, id); + }, + setUuid: function(id, newUuid) { + return this._uploadData.uuidChanged(id, newUuid); + }, + setStatus: function(id, newStatus) { + var fileRecord = this.getUploads({ + id: id + }); + if (!fileRecord) { + throw new qq.Error(id + " is not a valid file ID."); + } + switch (newStatus) { + case qq.status.DELETED: + this._onDeleteComplete(id, null, false); + break; + + case qq.status.DELETE_FAILED: + this._onDeleteComplete(id, null, true); + break; + + default: + var errorMessage = "Method setStatus called on '" + name + "' not implemented yet for " + newStatus; + this.log(errorMessage); + throw new qq.Error(errorMessage); + } + }, + uploadStoredFiles: function() { + if (this._storedIds.length === 0) { + this._itemError("noFilesError"); + } else { + this._uploadStoredFiles(); + } + } + }; + qq.basePrivateApi = { + _addCannedFile: function(sessionData) { + var self = this; + return this._uploadData.addFile({ + uuid: sessionData.uuid, + name: sessionData.name, + size: sessionData.size, + status: qq.status.UPLOAD_SUCCESSFUL, + onBeforeStatusChange: function(id) { + sessionData.deleteFileEndpoint && self.setDeleteFileEndpoint(sessionData.deleteFileEndpoint, id); + sessionData.deleteFileParams && self.setDeleteFileParams(sessionData.deleteFileParams, id); + if (sessionData.thumbnailUrl) { + self._thumbnailUrls[id] = sessionData.thumbnailUrl; + } + self._netUploaded++; + self._netUploadedOrQueued++; + } + }); + }, + _annotateWithButtonId: function(file, associatedInput) { + if (qq.isFile(file)) { + file.qqButtonId = this._getButtonId(associatedInput); + } + }, + _batchError: function(message) { + this._options.callbacks.onError(null, null, message, undefined); + }, + _createDeleteHandler: function() { + var self = this; + return new qq.DeleteFileAjaxRequester({ + method: this._options.deleteFile.method.toUpperCase(), + maxConnections: this._options.maxConnections, + uuidParamName: this._options.request.uuidName, + customHeaders: this._deleteFileCustomHeadersStore, + paramsStore: this._deleteFileParamsStore, + endpointStore: this._deleteFileEndpointStore, + cors: this._options.cors, + log: qq.bind(self.log, self), + onDelete: function(id) { + self._onDelete(id); + self._options.callbacks.onDelete(id); + }, + onDeleteComplete: function(id, xhrOrXdr, isError) { + self._onDeleteComplete(id, xhrOrXdr, isError); + self._options.callbacks.onDeleteComplete(id, xhrOrXdr, isError); + } + }); + }, + _createPasteHandler: function() { + var self = this; + return new qq.PasteSupport({ + targetElement: this._options.paste.targetElement, + callbacks: { + log: qq.bind(self.log, self), + pasteReceived: function(blob) { + self._handleCheckedCallback({ + name: "onPasteReceived", + callback: qq.bind(self._options.callbacks.onPasteReceived, self, blob), + onSuccess: qq.bind(self._handlePasteSuccess, self, blob), + identifier: "pasted image" + }); + } + } + }); + }, + _createStore: function(initialValue, _readOnlyValues_) { + var store = {}, catchall = initialValue, perIdReadOnlyValues = {}, readOnlyValues = _readOnlyValues_, copy = function(orig) { + if (qq.isObject(orig)) { + return qq.extend({}, orig); + } + return orig; + }, getReadOnlyValues = function() { + if (qq.isFunction(readOnlyValues)) { + return readOnlyValues(); + } + return readOnlyValues; + }, includeReadOnlyValues = function(id, existing) { + if (readOnlyValues && qq.isObject(existing)) { + qq.extend(existing, getReadOnlyValues()); + } + if (perIdReadOnlyValues[id]) { + qq.extend(existing, perIdReadOnlyValues[id]); + } + }; + return { + set: function(val, id) { + if (id == null) { + store = {}; + catchall = copy(val); + } else { + store[id] = copy(val); + } + }, + get: function(id) { + var values; + if (id != null && store[id]) { + values = store[id]; + } else { + values = copy(catchall); + } + includeReadOnlyValues(id, values); + return copy(values); + }, + addReadOnly: function(id, values) { + if (qq.isObject(store)) { + if (id === null) { + if (qq.isFunction(values)) { + readOnlyValues = values; + } else { + readOnlyValues = readOnlyValues || {}; + qq.extend(readOnlyValues, values); + } + } else { + perIdReadOnlyValues[id] = perIdReadOnlyValues[id] || {}; + qq.extend(perIdReadOnlyValues[id], values); + } + } + }, + remove: function(fileId) { + return delete store[fileId]; + }, + reset: function() { + store = {}; + perIdReadOnlyValues = {}; + catchall = initialValue; + } + }; + }, + _createUploadDataTracker: function() { + var self = this; + return new qq.UploadData({ + getName: function(id) { + return self.getName(id); + }, + getUuid: function(id) { + return self.getUuid(id); + }, + getSize: function(id) { + return self.getSize(id); + }, + onStatusChange: function(id, oldStatus, newStatus) { + self._onUploadStatusChange(id, oldStatus, newStatus); + self._options.callbacks.onStatusChange(id, oldStatus, newStatus); + self._maybeAllComplete(id, newStatus); + if (self._totalProgress) { + setTimeout(function() { + self._totalProgress.onStatusChange(id, oldStatus, newStatus); + }, 0); + } + } + }); + }, + _createUploadButton: function(spec) { + var self = this, acceptFiles = spec.accept || this._options.validation.acceptFiles, allowedExtensions = spec.allowedExtensions || this._options.validation.allowedExtensions, button; + function allowMultiple() { + if (qq.supportedFeatures.ajaxUploading) { + if (self._options.workarounds.iosEmptyVideos && qq.ios() && !qq.ios6() && self._isAllowedExtension(allowedExtensions, ".mov")) { + return false; + } + if (spec.multiple === undefined) { + return self._options.multiple; + } + return spec.multiple; + } + return false; + } + button = new qq.UploadButton({ + acceptFiles: acceptFiles, + element: spec.element, + focusClass: this._options.classes.buttonFocus, + folders: spec.folders, + hoverClass: this._options.classes.buttonHover, + ios8BrowserCrashWorkaround: this._options.workarounds.ios8BrowserCrash, + multiple: allowMultiple(), + name: this._options.request.inputName, + onChange: function(input) { + self._onInputChange(input); + }, + title: spec.title == null ? this._options.text.fileInputTitle : spec.title + }); + this._disposeSupport.addDisposer(function() { + button.dispose(); + }); + self._buttons.push(button); + return button; + }, + _createUploadHandler: function(additionalOptions, namespace) { + var self = this, lastOnProgress = {}, options = { + debug: this._options.debug, + maxConnections: this._options.maxConnections, + cors: this._options.cors, + paramsStore: this._paramsStore, + endpointStore: this._endpointStore, + chunking: this._options.chunking, + resume: this._options.resume, + blobs: this._options.blobs, + log: qq.bind(self.log, self), + preventRetryParam: this._options.retry.preventRetryResponseProperty, + onProgress: function(id, name, loaded, total) { + if (loaded < 0 || total < 0) { + return; + } + if (lastOnProgress[id]) { + if (lastOnProgress[id].loaded !== loaded || lastOnProgress[id].total !== total) { + self._onProgress(id, name, loaded, total); + self._options.callbacks.onProgress(id, name, loaded, total); + } + } else { + self._onProgress(id, name, loaded, total); + self._options.callbacks.onProgress(id, name, loaded, total); + } + lastOnProgress[id] = { + loaded: loaded, + total: total + }; + }, + onComplete: function(id, name, result, xhr) { + delete lastOnProgress[id]; + var status = self.getUploads({ + id: id + }).status, retVal; + if (status === qq.status.UPLOAD_SUCCESSFUL || status === qq.status.UPLOAD_FAILED) { + return; + } + retVal = self._onComplete(id, name, result, xhr); + if (retVal instanceof qq.Promise) { + retVal.done(function() { + self._options.callbacks.onComplete(id, name, result, xhr); + }); + } else { + self._options.callbacks.onComplete(id, name, result, xhr); + } + }, + onCancel: function(id, name, cancelFinalizationEffort) { + var promise = new qq.Promise(); + self._handleCheckedCallback({ + name: "onCancel", + callback: qq.bind(self._options.callbacks.onCancel, self, id, name), + onFailure: promise.failure, + onSuccess: function() { + cancelFinalizationEffort.then(function() { + self._onCancel(id, name); + }); + promise.success(); + }, + identifier: id + }); + return promise; + }, + onUploadPrep: qq.bind(this._onUploadPrep, this), + onUpload: function(id, name) { + self._onUpload(id, name); + self._options.callbacks.onUpload(id, name); + }, + onUploadChunk: function(id, name, chunkData) { + self._onUploadChunk(id, chunkData); + self._options.callbacks.onUploadChunk(id, name, chunkData); + }, + onUploadChunkSuccess: function(id, chunkData, result, xhr) { + self._options.callbacks.onUploadChunkSuccess.apply(self, arguments); + }, + onResume: function(id, name, chunkData) { + return self._options.callbacks.onResume(id, name, chunkData); + }, + onAutoRetry: function(id, name, responseJSON, xhr) { + return self._onAutoRetry.apply(self, arguments); + }, + onUuidChanged: function(id, newUuid) { + self.log("Server requested UUID change from '" + self.getUuid(id) + "' to '" + newUuid + "'"); + self.setUuid(id, newUuid); + }, + getName: qq.bind(self.getName, self), + getUuid: qq.bind(self.getUuid, self), + getSize: qq.bind(self.getSize, self), + setSize: qq.bind(self._setSize, self), + getDataByUuid: function(uuid) { + return self.getUploads({ + uuid: uuid + }); + }, + isQueued: function(id) { + var status = self.getUploads({ + id: id + }).status; + return status === qq.status.QUEUED || status === qq.status.SUBMITTED || status === qq.status.UPLOAD_RETRYING || status === qq.status.PAUSED; + }, + getIdsInProxyGroup: self._uploadData.getIdsInProxyGroup, + getIdsInBatch: self._uploadData.getIdsInBatch + }; + qq.each(this._options.request, function(prop, val) { + options[prop] = val; + }); + options.customHeaders = this._customHeadersStore; + if (additionalOptions) { + qq.each(additionalOptions, function(key, val) { + options[key] = val; + }); + } + return new qq.UploadHandlerController(options, namespace); + }, + _fileOrBlobRejected: function(id) { + this._netUploadedOrQueued--; + this._uploadData.setStatus(id, qq.status.REJECTED); + }, + _formatSize: function(bytes) { + if (bytes === 0) { + return bytes + this._options.text.sizeSymbols[0]; + } + var i = -1; + do { + bytes = bytes / 1e3; + i++; + } while (bytes > 999); + return Math.max(bytes, .1).toFixed(1) + this._options.text.sizeSymbols[i]; + }, + _generateExtraButtonSpecs: function() { + var self = this; + this._extraButtonSpecs = {}; + qq.each(this._options.extraButtons, function(idx, extraButtonOptionEntry) { + var multiple = extraButtonOptionEntry.multiple, validation = qq.extend({}, self._options.validation, true), extraButtonSpec = qq.extend({}, extraButtonOptionEntry); + if (multiple === undefined) { + multiple = self._options.multiple; + } + if (extraButtonSpec.validation) { + qq.extend(validation, extraButtonOptionEntry.validation, true); + } + qq.extend(extraButtonSpec, { + multiple: multiple, + validation: validation + }, true); + self._initExtraButton(extraButtonSpec); + }); + }, + _getButton: function(buttonId) { + var extraButtonsSpec = this._extraButtonSpecs[buttonId]; + if (extraButtonsSpec) { + return extraButtonsSpec.element; + } else if (buttonId === this._defaultButtonId) { + return this._options.button; + } + }, + _getButtonId: function(buttonOrFileInputOrFile) { + var inputs, fileInput, fileBlobOrInput = buttonOrFileInputOrFile; + if (fileBlobOrInput instanceof qq.BlobProxy) { + fileBlobOrInput = fileBlobOrInput.referenceBlob; + } + if (fileBlobOrInput && !qq.isBlob(fileBlobOrInput)) { + if (qq.isFile(fileBlobOrInput)) { + return fileBlobOrInput.qqButtonId; + } else if (fileBlobOrInput.tagName.toLowerCase() === "input" && fileBlobOrInput.type.toLowerCase() === "file") { + return fileBlobOrInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME); + } + inputs = fileBlobOrInput.getElementsByTagName("input"); + qq.each(inputs, function(idx, input) { + if (input.getAttribute("type") === "file") { + fileInput = input; + return false; + } + }); + if (fileInput) { + return fileInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME); + } + } + }, + _getNotFinished: function() { + return this._uploadData.retrieve({ + status: [ qq.status.UPLOADING, qq.status.UPLOAD_RETRYING, qq.status.QUEUED, qq.status.SUBMITTING, qq.status.SUBMITTED, qq.status.PAUSED ] + }).length; + }, + _getValidationBase: function(buttonId) { + var extraButtonSpec = this._extraButtonSpecs[buttonId]; + return extraButtonSpec ? extraButtonSpec.validation : this._options.validation; + }, + _getValidationDescriptor: function(fileWrapper) { + if (fileWrapper.file instanceof qq.BlobProxy) { + return { + name: qq.getFilename(fileWrapper.file.referenceBlob), + size: fileWrapper.file.referenceBlob.size + }; + } + return { + name: this.getUploads({ + id: fileWrapper.id + }).name, + size: this.getUploads({ + id: fileWrapper.id + }).size + }; + }, + _getValidationDescriptors: function(fileWrappers) { + var self = this, fileDescriptors = []; + qq.each(fileWrappers, function(idx, fileWrapper) { + fileDescriptors.push(self._getValidationDescriptor(fileWrapper)); + }); + return fileDescriptors; + }, + _handleCameraAccess: function() { + if (this._options.camera.ios && qq.ios()) { + var acceptIosCamera = "image/*;capture=camera", button = this._options.camera.button, buttonId = button ? this._getButtonId(button) : this._defaultButtonId, optionRoot = this._options; + if (buttonId && buttonId !== this._defaultButtonId) { + optionRoot = this._extraButtonSpecs[buttonId]; + } + optionRoot.multiple = false; + if (optionRoot.validation.acceptFiles === null) { + optionRoot.validation.acceptFiles = acceptIosCamera; + } else { + optionRoot.validation.acceptFiles += "," + acceptIosCamera; + } + qq.each(this._buttons, function(idx, button) { + if (button.getButtonId() === buttonId) { + button.setMultiple(optionRoot.multiple); + button.setAcceptFiles(optionRoot.acceptFiles); + return false; + } + }); + } + }, + _handleCheckedCallback: function(details) { + var self = this, callbackRetVal = details.callback(); + if (qq.isGenericPromise(callbackRetVal)) { + this.log(details.name + " - waiting for " + details.name + " promise to be fulfilled for " + details.identifier); + return callbackRetVal.then(function(successParam) { + self.log(details.name + " promise success for " + details.identifier); + details.onSuccess(successParam); + }, function() { + if (details.onFailure) { + self.log(details.name + " promise failure for " + details.identifier); + details.onFailure(); + } else { + self.log(details.name + " promise failure for " + details.identifier); + } + }); + } + if (callbackRetVal !== false) { + details.onSuccess(callbackRetVal); + } else { + if (details.onFailure) { + this.log(details.name + " - return value was 'false' for " + details.identifier + ". Invoking failure callback."); + details.onFailure(); + } else { + this.log(details.name + " - return value was 'false' for " + details.identifier + ". Will not proceed."); + } + } + return callbackRetVal; + }, + _handleNewFile: function(file, batchId, newFileWrapperList) { + var self = this, uuid = qq.getUniqueId(), size = -1, name = qq.getFilename(file), actualFile = file.blob || file, handler = this._customNewFileHandler ? this._customNewFileHandler : qq.bind(self._handleNewFileGeneric, self); + if (!qq.isInput(actualFile) && actualFile.size >= 0) { + size = actualFile.size; + } + handler(actualFile, name, uuid, size, newFileWrapperList, batchId, this._options.request.uuidName, { + uploadData: self._uploadData, + paramsStore: self._paramsStore, + addFileToHandler: function(id, file) { + self._handler.add(id, file); + self._netUploadedOrQueued++; + self._trackButton(id); + } + }); + }, + _handleNewFileGeneric: function(file, name, uuid, size, fileList, batchId) { + var id = this._uploadData.addFile({ + uuid: uuid, + name: name, + size: size, + batchId: batchId + }); + this._handler.add(id, file); + this._trackButton(id); + this._netUploadedOrQueued++; + fileList.push({ + id: id, + file: file + }); + }, + _handlePasteSuccess: function(blob, extSuppliedName) { + var extension = blob.type.split("/")[1], name = extSuppliedName; + if (name == null) { + name = this._options.paste.defaultName; + } + name += "." + extension; + this.addFiles({ + name: name, + blob: blob + }); + }, + _handleDeleteSuccess: function(id) { + if (this.getUploads({ + id: id + }).status !== qq.status.DELETED) { + var name = this.getName(id); + this._netUploadedOrQueued--; + this._netUploaded--; + this._handler.expunge(id); + this._uploadData.setStatus(id, qq.status.DELETED); + this.log("Delete request for '" + name + "' has succeeded."); + } + }, + _handleDeleteFailed: function(id, xhrOrXdr) { + var name = this.getName(id); + this._uploadData.setStatus(id, qq.status.DELETE_FAILED); + this.log("Delete request for '" + name + "' has failed.", "error"); + if (!xhrOrXdr || xhrOrXdr.withCredentials === undefined) { + this._options.callbacks.onError(id, name, "Delete request failed", xhrOrXdr); + } else { + this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhrOrXdr.status, xhrOrXdr); + } + }, + _initExtraButton: function(spec) { + var button = this._createUploadButton({ + accept: spec.validation.acceptFiles, + allowedExtensions: spec.validation.allowedExtensions, + element: spec.element, + folders: spec.folders, + multiple: spec.multiple, + title: spec.fileInputTitle + }); + this._extraButtonSpecs[button.getButtonId()] = spec; + }, + _initFormSupportAndParams: function() { + this._formSupport = qq.FormSupport && new qq.FormSupport(this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this)); + if (this._formSupport && this._formSupport.attachedToForm) { + this._paramsStore = this._createStore(this._options.request.params, this._formSupport.getFormInputsAsObject); + this._options.autoUpload = this._formSupport.newAutoUpload; + if (this._formSupport.newEndpoint) { + this._options.request.endpoint = this._formSupport.newEndpoint; + } + } else { + this._paramsStore = this._createStore(this._options.request.params); + } + }, + _isDeletePossible: function() { + if (!qq.DeleteFileAjaxRequester || !this._options.deleteFile.enabled) { + return false; + } + if (this._options.cors.expected) { + if (qq.supportedFeatures.deleteFileCorsXhr) { + return true; + } + if (qq.supportedFeatures.deleteFileCorsXdr && this._options.cors.allowXdr) { + return true; + } + return false; + } + return true; + }, + _isAllowedExtension: function(allowed, fileName) { + var valid = false; + if (!allowed.length) { + return true; + } + qq.each(allowed, function(idx, allowedExt) { + if (qq.isString(allowedExt)) { + var extRegex = new RegExp("\\." + allowedExt + "$", "i"); + if (fileName.match(extRegex) != null) { + valid = true; + return false; + } + } + }); + return valid; + }, + _itemError: function(code, maybeNameOrNames, item) { + var message = this._options.messages[code], allowedExtensions = [], names = [].concat(maybeNameOrNames), name = names[0], buttonId = this._getButtonId(item), validationBase = this._getValidationBase(buttonId), extensionsForMessage, placeholderMatch; + function r(name, replacement) { + message = message.replace(name, replacement); + } + qq.each(validationBase.allowedExtensions, function(idx, allowedExtension) { + if (qq.isString(allowedExtension)) { + allowedExtensions.push(allowedExtension); + } + }); + extensionsForMessage = allowedExtensions.join(", ").toLowerCase(); + r("{file}", this._options.formatFileName(name)); + r("{extensions}", extensionsForMessage); + r("{sizeLimit}", this._formatSize(validationBase.sizeLimit)); + r("{minSizeLimit}", this._formatSize(validationBase.minSizeLimit)); + placeholderMatch = message.match(/(\{\w+\})/g); + if (placeholderMatch !== null) { + qq.each(placeholderMatch, function(idx, placeholder) { + r(placeholder, names[idx]); + }); + } + this._options.callbacks.onError(null, name, message, undefined); + return message; + }, + _manualRetry: function(id, callback) { + if (this._onBeforeManualRetry(id)) { + this._netUploadedOrQueued++; + this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); + if (callback) { + callback(id); + } else { + this._handler.retry(id); + } + return true; + } + }, + _maybeAllComplete: function(id, status) { + var self = this, notFinished = this._getNotFinished(); + if (status === qq.status.UPLOAD_SUCCESSFUL) { + this._succeededSinceLastAllComplete.push(id); + } else if (status === qq.status.UPLOAD_FAILED) { + this._failedSinceLastAllComplete.push(id); + } + if (notFinished === 0 && (this._succeededSinceLastAllComplete.length || this._failedSinceLastAllComplete.length)) { + setTimeout(function() { + self._onAllComplete(self._succeededSinceLastAllComplete, self._failedSinceLastAllComplete); + }, 0); + } + }, + _maybeHandleIos8SafariWorkaround: function() { + var self = this; + if (this._options.workarounds.ios8SafariUploads && qq.ios800() && qq.iosSafari()) { + setTimeout(function() { + window.alert(self._options.messages.unsupportedBrowserIos8Safari); + }, 0); + throw new qq.Error(this._options.messages.unsupportedBrowserIos8Safari); + } + }, + _maybeParseAndSendUploadError: function(id, name, response, xhr) { + if (!response.success) { + if (xhr && xhr.status !== 200 && !response.error) { + this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr); + } else { + var errorReason = response.error ? response.error : this._options.text.defaultResponseError; + this._options.callbacks.onError(id, name, errorReason, xhr); + } + } + }, + _maybeProcessNextItemAfterOnValidateCallback: function(validItem, items, index, params, endpoint) { + var self = this; + if (items.length > index) { + if (validItem || !this._options.validation.stopOnFirstInvalidFile) { + setTimeout(function() { + var validationDescriptor = self._getValidationDescriptor(items[index]), buttonId = self._getButtonId(items[index].file), button = self._getButton(buttonId); + self._handleCheckedCallback({ + name: "onValidate", + callback: qq.bind(self._options.callbacks.onValidate, self, validationDescriptor, button), + onSuccess: qq.bind(self._onValidateCallbackSuccess, self, items, index, params, endpoint), + onFailure: qq.bind(self._onValidateCallbackFailure, self, items, index, params, endpoint), + identifier: "Item '" + validationDescriptor.name + "', size: " + validationDescriptor.size + }); + }, 0); + } else if (!validItem) { + for (;index < items.length; index++) { + self._fileOrBlobRejected(items[index].id); + } + } + } + }, + _onAllComplete: function(successful, failed) { + this._totalProgress && this._totalProgress.onAllComplete(successful, failed, this._preventRetries); + this._options.callbacks.onAllComplete(qq.extend([], successful), qq.extend([], failed)); + this._succeededSinceLastAllComplete = []; + this._failedSinceLastAllComplete = []; + }, + _onAutoRetry: function(id, name, responseJSON, xhr, callback) { + var self = this; + self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty]; + if (self._shouldAutoRetry(id, name, responseJSON)) { + var retryWaitPeriod = self._options.retry.autoAttemptDelay * 1e3; + self._maybeParseAndSendUploadError.apply(self, arguments); + self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id]); + self._onBeforeAutoRetry(id, name); + self._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); + self._retryTimeouts[id] = setTimeout(function() { + self.log("Starting retry for " + name + "..."); + if (callback) { + callback(id); + } else { + self._handler.retry(id); + } + }, retryWaitPeriod); + return true; + } + }, + _onBeforeAutoRetry: function(id, name) { + this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "..."); + }, + _onBeforeManualRetry: function(id) { + var itemLimit = this._currentItemLimit, fileName; + if (this._preventRetries[id]) { + this.log("Retries are forbidden for id " + id, "warn"); + return false; + } else if (this._handler.isValid(id)) { + fileName = this.getName(id); + if (this._options.callbacks.onManualRetry(id, fileName) === false) { + return false; + } + if (itemLimit > 0 && this._netUploadedOrQueued + 1 > itemLimit) { + this._itemError("retryFailTooManyItems"); + return false; + } + this.log("Retrying upload for '" + fileName + "' (id: " + id + ")..."); + return true; + } else { + this.log("'" + id + "' is not a valid file ID", "error"); + return false; + } + }, + _onCancel: function(id, name) { + this._netUploadedOrQueued--; + clearTimeout(this._retryTimeouts[id]); + var storedItemIndex = qq.indexOf(this._storedIds, id); + if (!this._options.autoUpload && storedItemIndex >= 0) { + this._storedIds.splice(storedItemIndex, 1); + } + this._uploadData.setStatus(id, qq.status.CANCELED); + }, + _onComplete: function(id, name, result, xhr) { + if (!result.success) { + this._netUploadedOrQueued--; + this._uploadData.setStatus(id, qq.status.UPLOAD_FAILED); + if (result[this._options.retry.preventRetryResponseProperty] === true) { + this._preventRetries[id] = true; + } + } else { + if (result.thumbnailUrl) { + this._thumbnailUrls[id] = result.thumbnailUrl; + } + this._netUploaded++; + this._uploadData.setStatus(id, qq.status.UPLOAD_SUCCESSFUL); + } + this._maybeParseAndSendUploadError(id, name, result, xhr); + return result.success ? true : false; + }, + _onDelete: function(id) { + this._uploadData.setStatus(id, qq.status.DELETING); + }, + _onDeleteComplete: function(id, xhrOrXdr, isError) { + var name = this.getName(id); + if (isError) { + this._handleDeleteFailed(id, xhrOrXdr); + } else { + this._handleDeleteSuccess(id); + } + }, + _onInputChange: function(input) { + var fileIndex; + if (qq.supportedFeatures.ajaxUploading) { + for (fileIndex = 0; fileIndex < input.files.length; fileIndex++) { + this._annotateWithButtonId(input.files[fileIndex], input); + } + this.addFiles(input.files); + } else if (input.value.length > 0) { + this.addFiles(input); + } + qq.each(this._buttons, function(idx, button) { + button.reset(); + }); + }, + _onProgress: function(id, name, loaded, total) { + this._totalProgress && this._totalProgress.onIndividualProgress(id, loaded, total); + }, + _onSubmit: function(id, name) {}, + _onSubmitCallbackSuccess: function(id, name) { + this._onSubmit.apply(this, arguments); + this._uploadData.setStatus(id, qq.status.SUBMITTED); + this._onSubmitted.apply(this, arguments); + if (this._options.autoUpload) { + this._options.callbacks.onSubmitted.apply(this, arguments); + this._uploadFile(id); + } else { + this._storeForLater(id); + this._options.callbacks.onSubmitted.apply(this, arguments); + } + }, + _onSubmitDelete: function(id, onSuccessCallback, additionalMandatedParams) { + var uuid = this.getUuid(id), adjustedOnSuccessCallback; + if (onSuccessCallback) { + adjustedOnSuccessCallback = qq.bind(onSuccessCallback, this, id, uuid, additionalMandatedParams); + } + if (this._isDeletePossible()) { + this._handleCheckedCallback({ + name: "onSubmitDelete", + callback: qq.bind(this._options.callbacks.onSubmitDelete, this, id), + onSuccess: adjustedOnSuccessCallback || qq.bind(this._deleteHandler.sendDelete, this, id, uuid, additionalMandatedParams), + identifier: id + }); + return true; + } else { + this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " + "due to CORS on a user agent that does not support pre-flighting.", "warn"); + return false; + } + }, + _onSubmitted: function(id) {}, + _onTotalProgress: function(loaded, total) { + this._options.callbacks.onTotalProgress(loaded, total); + }, + _onUploadPrep: function(id) {}, + _onUpload: function(id, name) { + this._uploadData.setStatus(id, qq.status.UPLOADING); + }, + _onUploadChunk: function(id, chunkData) {}, + _onUploadStatusChange: function(id, oldStatus, newStatus) { + if (newStatus === qq.status.PAUSED) { + clearTimeout(this._retryTimeouts[id]); + } + }, + _onValidateBatchCallbackFailure: function(fileWrappers) { + var self = this; + qq.each(fileWrappers, function(idx, fileWrapper) { + self._fileOrBlobRejected(fileWrapper.id); + }); + }, + _onValidateBatchCallbackSuccess: function(validationDescriptors, items, params, endpoint, button) { + var errorMessage, itemLimit = this._currentItemLimit, proposedNetFilesUploadedOrQueued = this._netUploadedOrQueued; + if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) { + if (items.length > 0) { + this._handleCheckedCallback({ + name: "onValidate", + callback: qq.bind(this._options.callbacks.onValidate, this, validationDescriptors[0], button), + onSuccess: qq.bind(this._onValidateCallbackSuccess, this, items, 0, params, endpoint), + onFailure: qq.bind(this._onValidateCallbackFailure, this, items, 0, params, endpoint), + identifier: "Item '" + items[0].file.name + "', size: " + items[0].file.size + }); + } else { + this._itemError("noFilesError"); + } + } else { + this._onValidateBatchCallbackFailure(items); + errorMessage = this._options.messages.tooManyItemsError.replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued).replace(/\{itemLimit\}/g, itemLimit); + this._batchError(errorMessage); + } + }, + _onValidateCallbackFailure: function(items, index, params, endpoint) { + var nextIndex = index + 1; + this._fileOrBlobRejected(items[index].id, items[index].file.name); + this._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint); + }, + _onValidateCallbackSuccess: function(items, index, params, endpoint) { + var self = this, nextIndex = index + 1, validationDescriptor = this._getValidationDescriptor(items[index]); + this._validateFileOrBlobData(items[index], validationDescriptor).then(function() { + self._upload(items[index].id, params, endpoint); + self._maybeProcessNextItemAfterOnValidateCallback(true, items, nextIndex, params, endpoint); + }, function() { + self._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint); + }); + }, + _prepareItemsForUpload: function(items, params, endpoint) { + if (items.length === 0) { + this._itemError("noFilesError"); + return; + } + var validationDescriptors = this._getValidationDescriptors(items), buttonId = this._getButtonId(items[0].file), button = this._getButton(buttonId); + this._handleCheckedCallback({ + name: "onValidateBatch", + callback: qq.bind(this._options.callbacks.onValidateBatch, this, validationDescriptors, button), + onSuccess: qq.bind(this._onValidateBatchCallbackSuccess, this, validationDescriptors, items, params, endpoint, button), + onFailure: qq.bind(this._onValidateBatchCallbackFailure, this, items), + identifier: "batch validation" + }); + }, + _preventLeaveInProgress: function() { + var self = this; + this._disposeSupport.attach(window, "beforeunload", function(e) { + if (self.getInProgress()) { + e = e || window.event; + e.returnValue = self._options.messages.onLeave; + return self._options.messages.onLeave; + } + }); + }, + _refreshSessionData: function() { + var self = this, options = this._options.session; + if (qq.Session && this._options.session.endpoint != null) { + if (!this._session) { + qq.extend(options, { + cors: this._options.cors + }); + options.log = qq.bind(this.log, this); + options.addFileRecord = qq.bind(this._addCannedFile, this); + this._session = new qq.Session(options); + } + setTimeout(function() { + self._session.refresh().then(function(response, xhrOrXdr) { + self._sessionRequestComplete(); + self._options.callbacks.onSessionRequestComplete(response, true, xhrOrXdr); + }, function(response, xhrOrXdr) { + self._options.callbacks.onSessionRequestComplete(response, false, xhrOrXdr); + }); + }, 0); + } + }, + _sessionRequestComplete: function() {}, + _setSize: function(id, newSize) { + this._uploadData.updateSize(id, newSize); + this._totalProgress && this._totalProgress.onNewSize(id); + }, + _shouldAutoRetry: function(id, name, responseJSON) { + var uploadData = this._uploadData.retrieve({ + id: id + }); + if (!this._preventRetries[id] && this._options.retry.enableAuto && uploadData.status !== qq.status.PAUSED) { + if (this._autoRetries[id] === undefined) { + this._autoRetries[id] = 0; + } + if (this._autoRetries[id] < this._options.retry.maxAutoAttempts) { + this._autoRetries[id] += 1; + return true; + } + } + return false; + }, + _storeForLater: function(id) { + this._storedIds.push(id); + }, + _trackButton: function(id) { + var buttonId; + if (qq.supportedFeatures.ajaxUploading) { + buttonId = this._handler.getFile(id).qqButtonId; + } else { + buttonId = this._getButtonId(this._handler.getInput(id)); + } + if (buttonId) { + this._buttonIdsForFileIds[id] = buttonId; + } + }, + _updateFormSupportAndParams: function(formElementOrId) { + this._options.form.element = formElementOrId; + this._formSupport = qq.FormSupport && new qq.FormSupport(this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this)); + if (this._formSupport && this._formSupport.attachedToForm) { + this._paramsStore.addReadOnly(null, this._formSupport.getFormInputsAsObject); + this._options.autoUpload = this._formSupport.newAutoUpload; + if (this._formSupport.newEndpoint) { + this.setEndpoint(this._formSupport.newEndpoint); + } + } + }, + _upload: function(id, params, endpoint) { + var name = this.getName(id); + if (params) { + this.setParams(params, id); + } + if (endpoint) { + this.setEndpoint(endpoint, id); + } + this._handleCheckedCallback({ + name: "onSubmit", + callback: qq.bind(this._options.callbacks.onSubmit, this, id, name), + onSuccess: qq.bind(this._onSubmitCallbackSuccess, this, id, name), + onFailure: qq.bind(this._fileOrBlobRejected, this, id, name), + identifier: id + }); + }, + _uploadFile: function(id) { + if (!this._handler.upload(id)) { + this._uploadData.setStatus(id, qq.status.QUEUED); + } + }, + _uploadStoredFiles: function() { + var idToUpload, stillSubmitting, self = this; + while (this._storedIds.length) { + idToUpload = this._storedIds.shift(); + this._uploadFile(idToUpload); + } + stillSubmitting = this.getUploads({ + status: qq.status.SUBMITTING + }).length; + if (stillSubmitting) { + qq.log("Still waiting for " + stillSubmitting + " files to clear submit queue. Will re-parse stored IDs array shortly."); + setTimeout(function() { + self._uploadStoredFiles(); + }, 1e3); + } + }, + _validateFileOrBlobData: function(fileWrapper, validationDescriptor) { + var self = this, file = function() { + if (fileWrapper.file instanceof qq.BlobProxy) { + return fileWrapper.file.referenceBlob; + } + return fileWrapper.file; + }(), name = validationDescriptor.name, size = validationDescriptor.size, buttonId = this._getButtonId(fileWrapper.file), validationBase = this._getValidationBase(buttonId), validityChecker = new qq.Promise(); + validityChecker.then(function() {}, function() { + self._fileOrBlobRejected(fileWrapper.id, name); + }); + if (qq.isFileOrInput(file) && !this._isAllowedExtension(validationBase.allowedExtensions, name)) { + this._itemError("typeError", name, file); + return validityChecker.failure(); + } + if (!this._options.validation.allowEmpty && size === 0) { + this._itemError("emptyError", name, file); + return validityChecker.failure(); + } + if (size > 0 && validationBase.sizeLimit && size > validationBase.sizeLimit) { + this._itemError("sizeError", name, file); + return validityChecker.failure(); + } + if (size > 0 && size < validationBase.minSizeLimit) { + this._itemError("minSizeError", name, file); + return validityChecker.failure(); + } + if (qq.ImageValidation && qq.supportedFeatures.imagePreviews && qq.isFile(file)) { + new qq.ImageValidation(file, qq.bind(self.log, self)).validate(validationBase.image).then(validityChecker.success, function(errorCode) { + self._itemError(errorCode + "ImageError", name, file); + validityChecker.failure(); + }); + } else { + validityChecker.success(); + } + return validityChecker; + }, + _wrapCallbacks: function() { + var self, safeCallback, prop; + self = this; + safeCallback = function(name, callback, args) { + var errorMsg; + try { + return callback.apply(self, args); + } catch (exception) { + errorMsg = exception.message || exception.toString(); + self.log("Caught exception in '" + name + "' callback - " + errorMsg, "error"); + } + }; + for (prop in this._options.callbacks) { + (function() { + var callbackName, callbackFunc; + callbackName = prop; + callbackFunc = self._options.callbacks[callbackName]; + self._options.callbacks[callbackName] = function() { + return safeCallback(callbackName, callbackFunc, arguments); + }; + })(); + } + } + }; + })(); + (function() { + "use strict"; + qq.FineUploaderBasic = function(o) { + var self = this; + this._options = { + debug: false, + button: null, + multiple: true, + maxConnections: 3, + disableCancelForFormUploads: false, + autoUpload: true, + request: { + customHeaders: {}, + endpoint: "/server/upload", + filenameParam: "qqfilename", + forceMultipart: true, + inputName: "qqfile", + method: "POST", + params: {}, + paramsInBody: true, + totalFileSizeName: "qqtotalfilesize", + uuidName: "qquuid" + }, + validation: { + allowedExtensions: [], + sizeLimit: 0, + minSizeLimit: 0, + itemLimit: 0, + stopOnFirstInvalidFile: true, + acceptFiles: null, + image: { + maxHeight: 0, + maxWidth: 0, + minHeight: 0, + minWidth: 0 + }, + allowEmpty: false + }, + callbacks: { + onSubmit: function(id, name) {}, + onSubmitted: function(id, name) {}, + onComplete: function(id, name, responseJSON, maybeXhr) {}, + onAllComplete: function(successful, failed) {}, + onCancel: function(id, name) {}, + onUpload: function(id, name) {}, + onUploadChunk: function(id, name, chunkData) {}, + onUploadChunkSuccess: function(id, chunkData, responseJSON, xhr) {}, + onResume: function(id, fileName, chunkData) {}, + onProgress: function(id, name, loaded, total) {}, + onTotalProgress: function(loaded, total) {}, + onError: function(id, name, reason, maybeXhrOrXdr) {}, + onAutoRetry: function(id, name, attemptNumber) {}, + onManualRetry: function(id, name) {}, + onValidateBatch: function(fileOrBlobData) {}, + onValidate: function(fileOrBlobData) {}, + onSubmitDelete: function(id) {}, + onDelete: function(id) {}, + onDeleteComplete: function(id, xhrOrXdr, isError) {}, + onPasteReceived: function(blob) {}, + onStatusChange: function(id, oldStatus, newStatus) {}, + onSessionRequestComplete: function(response, success, xhrOrXdr) {} + }, + messages: { + typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.", + sizeError: "{file} is too large, maximum file size is {sizeLimit}.", + minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", + emptyError: "{file} is empty, please select files again without it.", + noFilesError: "No files to upload.", + tooManyItemsError: "Too many items ({netItems}) would be uploaded. Item limit is {itemLimit}.", + maxHeightImageError: "Image is too tall.", + maxWidthImageError: "Image is too wide.", + minHeightImageError: "Image is not tall enough.", + minWidthImageError: "Image is not wide enough.", + retryFailTooManyItems: "Retry failed - you have reached your file limit.", + onLeave: "The files are being uploaded, if you leave now the upload will be canceled.", + unsupportedBrowserIos8Safari: "Unrecoverable error - this browser does not permit file uploading of any kind due to serious bugs in iOS8 Safari. Please use iOS8 Chrome until Apple fixes these issues." + }, + retry: { + enableAuto: false, + maxAutoAttempts: 3, + autoAttemptDelay: 5, + preventRetryResponseProperty: "preventRetry" + }, + classes: { + buttonHover: "qq-upload-button-hover", + buttonFocus: "qq-upload-button-focus" + }, + chunking: { + enabled: false, + concurrent: { + enabled: false + }, + mandatory: false, + paramNames: { + partIndex: "qqpartindex", + partByteOffset: "qqpartbyteoffset", + chunkSize: "qqchunksize", + totalFileSize: "qqtotalfilesize", + totalParts: "qqtotalparts" + }, + partSize: 2e6, + success: { + endpoint: null + } + }, + resume: { + enabled: false, + recordsExpireIn: 7, + paramNames: { + resuming: "qqresume" + } + }, + formatFileName: function(fileOrBlobName) { + return fileOrBlobName; + }, + text: { + defaultResponseError: "Upload failure reason unknown", + fileInputTitle: "file input", + sizeSymbols: [ "kB", "MB", "GB", "TB", "PB", "EB" ] + }, + deleteFile: { + enabled: false, + method: "DELETE", + endpoint: "/server/upload", + customHeaders: {}, + params: {} + }, + cors: { + expected: false, + sendCredentials: false, + allowXdr: false + }, + blobs: { + defaultName: "misc_data" + }, + paste: { + targetElement: null, + defaultName: "pasted_image" + }, + camera: { + ios: false, + button: null + }, + extraButtons: [], + session: { + endpoint: null, + params: {}, + customHeaders: {}, + refreshOnReset: true + }, + form: { + element: "qq-form", + autoUpload: false, + interceptSubmit: true + }, + scaling: { + customResizer: null, + sendOriginal: true, + orient: true, + defaultType: null, + defaultQuality: 80, + failureText: "Failed to scale", + includeExif: false, + sizes: [] + }, + workarounds: { + iosEmptyVideos: true, + ios8SafariUploads: true, + ios8BrowserCrash: false + } + }; + qq.extend(this._options, o, true); + this._buttons = []; + this._extraButtonSpecs = {}; + this._buttonIdsForFileIds = []; + this._wrapCallbacks(); + this._disposeSupport = new qq.DisposeSupport(); + this._storedIds = []; + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + this._thumbnailUrls = []; + this._netUploadedOrQueued = 0; + this._netUploaded = 0; + this._uploadData = this._createUploadDataTracker(); + this._initFormSupportAndParams(); + this._customHeadersStore = this._createStore(this._options.request.customHeaders); + this._deleteFileCustomHeadersStore = this._createStore(this._options.deleteFile.customHeaders); + this._deleteFileParamsStore = this._createStore(this._options.deleteFile.params); + this._endpointStore = this._createStore(this._options.request.endpoint); + this._deleteFileEndpointStore = this._createStore(this._options.deleteFile.endpoint); + this._handler = this._createUploadHandler(); + this._deleteHandler = qq.DeleteFileAjaxRequester && this._createDeleteHandler(); + if (this._options.button) { + this._defaultButtonId = this._createUploadButton({ + element: this._options.button, + title: this._options.text.fileInputTitle + }).getButtonId(); + } + this._generateExtraButtonSpecs(); + this._handleCameraAccess(); + if (this._options.paste.targetElement) { + if (qq.PasteSupport) { + this._pasteHandler = this._createPasteHandler(); + } else { + this.log("Paste support module not found", "error"); + } + } + this._preventLeaveInProgress(); + this._imageGenerator = qq.ImageGenerator && new qq.ImageGenerator(qq.bind(this.log, this)); + this._refreshSessionData(); + this._succeededSinceLastAllComplete = []; + this._failedSinceLastAllComplete = []; + this._scaler = qq.Scaler && new qq.Scaler(this._options.scaling, qq.bind(this.log, this)) || {}; + if (this._scaler.enabled) { + this._customNewFileHandler = qq.bind(this._scaler.handleNewFile, this._scaler); + } + if (qq.TotalProgress && qq.supportedFeatures.progressBar) { + this._totalProgress = new qq.TotalProgress(qq.bind(this._onTotalProgress, this), function(id) { + var entry = self._uploadData.retrieve({ + id: id + }); + return entry && entry.size || 0; + }); + } + this._currentItemLimit = this._options.validation.itemLimit; + }; + qq.FineUploaderBasic.prototype = qq.basePublicApi; + qq.extend(qq.FineUploaderBasic.prototype, qq.basePrivateApi); + })(); + qq.AjaxRequester = function(o) { + "use strict"; + var log, shouldParamsBeInQueryString, queue = [], requestData = {}, options = { + acceptHeader: null, + validMethods: [ "PATCH", "POST", "PUT" ], + method: "POST", + contentType: "application/x-www-form-urlencoded", + maxConnections: 3, + customHeaders: {}, + endpointStore: {}, + paramsStore: {}, + mandatedParams: {}, + allowXRequestedWithAndCacheControl: true, + successfulResponseCodes: { + DELETE: [ 200, 202, 204 ], + PATCH: [ 200, 201, 202, 203, 204 ], + POST: [ 200, 201, 202, 203, 204 ], + PUT: [ 200, 201, 202, 203, 204 ], + GET: [ 200 ] + }, + cors: { + expected: false, + sendCredentials: false + }, + log: function(str, level) {}, + onSend: function(id) {}, + onComplete: function(id, xhrOrXdr, isError) {}, + onProgress: null + }; + qq.extend(options, o); + log = options.log; + if (qq.indexOf(options.validMethods, options.method) < 0) { + throw new Error("'" + options.method + "' is not a supported method for this type of request!"); + } + function isSimpleMethod() { + return qq.indexOf([ "GET", "POST", "HEAD" ], options.method) >= 0; + } + function containsNonSimpleHeaders(headers) { + var containsNonSimple = false; + qq.each(containsNonSimple, function(idx, header) { + if (qq.indexOf([ "Accept", "Accept-Language", "Content-Language", "Content-Type" ], header) < 0) { + containsNonSimple = true; + return false; + } + }); + return containsNonSimple; + } + function isXdr(xhr) { + return options.cors.expected && xhr.withCredentials === undefined; + } + function getCorsAjaxTransport() { + var xhrOrXdr; + if (window.XMLHttpRequest || window.ActiveXObject) { + xhrOrXdr = qq.createXhrInstance(); + if (xhrOrXdr.withCredentials === undefined) { + xhrOrXdr = new XDomainRequest(); + xhrOrXdr.onload = function() {}; + xhrOrXdr.onerror = function() {}; + xhrOrXdr.ontimeout = function() {}; + xhrOrXdr.onprogress = function() {}; + } + } + return xhrOrXdr; + } + function getXhrOrXdr(id, suppliedXhr) { + var xhrOrXdr = requestData[id].xhr; + if (!xhrOrXdr) { + if (suppliedXhr) { + xhrOrXdr = suppliedXhr; + } else { + if (options.cors.expected) { + xhrOrXdr = getCorsAjaxTransport(); + } else { + xhrOrXdr = qq.createXhrInstance(); + } + } + requestData[id].xhr = xhrOrXdr; + } + return xhrOrXdr; + } + function dequeue(id) { + var i = qq.indexOf(queue, id), max = options.maxConnections, nextId; + delete requestData[id]; + queue.splice(i, 1); + if (queue.length >= max && i < max) { + nextId = queue[max - 1]; + sendRequest(nextId); + } + } + function onComplete(id, xdrError) { + var xhr = getXhrOrXdr(id), method = options.method, isError = xdrError === true; + dequeue(id); + if (isError) { + log(method + " request for " + id + " has failed", "error"); + } else if (!isXdr(xhr) && !isResponseSuccessful(xhr.status)) { + isError = true; + log(method + " request for " + id + " has failed - response code " + xhr.status, "error"); + } + options.onComplete(id, xhr, isError); + } + function getParams(id) { + var onDemandParams = requestData[id].additionalParams, mandatedParams = options.mandatedParams, params; + if (options.paramsStore.get) { + params = options.paramsStore.get(id); + } + if (onDemandParams) { + qq.each(onDemandParams, function(name, val) { + params = params || {}; + params[name] = val; + }); + } + if (mandatedParams) { + qq.each(mandatedParams, function(name, val) { + params = params || {}; + params[name] = val; + }); + } + return params; + } + function sendRequest(id, optXhr) { + var xhr = getXhrOrXdr(id, optXhr), method = options.method, params = getParams(id), payload = requestData[id].payload, url; + options.onSend(id); + url = createUrl(id, params, requestData[id].additionalQueryParams); + if (isXdr(xhr)) { + xhr.onload = getXdrLoadHandler(id); + xhr.onerror = getXdrErrorHandler(id); + } else { + xhr.onreadystatechange = getXhrReadyStateChangeHandler(id); + } + registerForUploadProgress(id); + xhr.open(method, url, true); + if (options.cors.expected && options.cors.sendCredentials && !isXdr(xhr)) { + xhr.withCredentials = true; + } + setHeaders(id); + log("Sending " + method + " request for " + id); + if (payload) { + xhr.send(payload); + } else if (shouldParamsBeInQueryString || !params) { + xhr.send(); + } else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/x-www-form-urlencoded") >= 0) { + xhr.send(qq.obj2url(params, "")); + } else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/json") >= 0) { + xhr.send(JSON.stringify(params)); + } else { + xhr.send(params); + } + return xhr; + } + function createUrl(id, params, additionalQueryParams) { + var endpoint = options.endpointStore.get(id), addToPath = requestData[id].addToPath; + if (addToPath != undefined) { + endpoint += "/" + addToPath; + } + if (shouldParamsBeInQueryString && params) { + endpoint = qq.obj2url(params, endpoint); + } + if (additionalQueryParams) { + endpoint = qq.obj2url(additionalQueryParams, endpoint); + } + return endpoint; + } + function getXhrReadyStateChangeHandler(id) { + return function() { + if (getXhrOrXdr(id).readyState === 4) { + onComplete(id); + } + }; + } + function registerForUploadProgress(id) { + var onProgress = options.onProgress; + if (onProgress) { + getXhrOrXdr(id).upload.onprogress = function(e) { + if (e.lengthComputable) { + onProgress(id, e.loaded, e.total); + } + }; + } + } + function getXdrLoadHandler(id) { + return function() { + onComplete(id); + }; + } + function getXdrErrorHandler(id) { + return function() { + onComplete(id, true); + }; + } + function setHeaders(id) { + var xhr = getXhrOrXdr(id), customHeaders = options.customHeaders, onDemandHeaders = requestData[id].additionalHeaders || {}, method = options.method, allHeaders = {}; + if (!isXdr(xhr)) { + options.acceptHeader && xhr.setRequestHeader("Accept", options.acceptHeader); + if (options.allowXRequestedWithAndCacheControl) { + if (!options.cors.expected || (!isSimpleMethod() || containsNonSimpleHeaders(customHeaders))) { + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.setRequestHeader("Cache-Control", "no-cache"); + } + } + if (options.contentType && (method === "POST" || method === "PUT")) { + xhr.setRequestHeader("Content-Type", options.contentType); + } + qq.extend(allHeaders, qq.isFunction(customHeaders) ? customHeaders(id) : customHeaders); + qq.extend(allHeaders, onDemandHeaders); + qq.each(allHeaders, function(name, val) { + xhr.setRequestHeader(name, val); + }); + } + } + function isResponseSuccessful(responseCode) { + return qq.indexOf(options.successfulResponseCodes[options.method], responseCode) >= 0; + } + function prepareToSend(id, optXhr, addToPath, additionalParams, additionalQueryParams, additionalHeaders, payload) { + requestData[id] = { + addToPath: addToPath, + additionalParams: additionalParams, + additionalQueryParams: additionalQueryParams, + additionalHeaders: additionalHeaders, + payload: payload + }; + var len = queue.push(id); + if (len <= options.maxConnections) { + return sendRequest(id, optXhr); + } + } + shouldParamsBeInQueryString = options.method === "GET" || options.method === "DELETE"; + qq.extend(this, { + initTransport: function(id) { + var path, params, headers, payload, cacheBuster, additionalQueryParams; + return { + withPath: function(appendToPath) { + path = appendToPath; + return this; + }, + withParams: function(additionalParams) { + params = additionalParams; + return this; + }, + withQueryParams: function(_additionalQueryParams_) { + additionalQueryParams = _additionalQueryParams_; + return this; + }, + withHeaders: function(additionalHeaders) { + headers = additionalHeaders; + return this; + }, + withPayload: function(thePayload) { + payload = thePayload; + return this; + }, + withCacheBuster: function() { + cacheBuster = true; + return this; + }, + send: function(optXhr) { + if (cacheBuster && qq.indexOf([ "GET", "DELETE" ], options.method) >= 0) { + params.qqtimestamp = new Date().getTime(); + } + return prepareToSend(id, optXhr, path, params, additionalQueryParams, headers, payload); + } + }; + }, + canceled: function(id) { + dequeue(id); + } + }); + }; + qq.UploadHandler = function(spec) { + "use strict"; + var proxy = spec.proxy, fileState = {}, onCancel = proxy.onCancel, getName = proxy.getName; + qq.extend(this, { + add: function(id, fileItem) { + fileState[id] = fileItem; + fileState[id].temp = {}; + }, + cancel: function(id) { + var self = this, cancelFinalizationEffort = new qq.Promise(), onCancelRetVal = onCancel(id, getName(id), cancelFinalizationEffort); + onCancelRetVal.then(function() { + if (self.isValid(id)) { + fileState[id].canceled = true; + self.expunge(id); + } + cancelFinalizationEffort.success(); + }); + }, + expunge: function(id) { + delete fileState[id]; + }, + getThirdPartyFileId: function(id) { + return fileState[id].key; + }, + isValid: function(id) { + return fileState[id] !== undefined; + }, + reset: function() { + fileState = {}; + }, + _getFileState: function(id) { + return fileState[id]; + }, + _setThirdPartyFileId: function(id, thirdPartyFileId) { + fileState[id].key = thirdPartyFileId; + }, + _wasCanceled: function(id) { + return !!fileState[id].canceled; + } + }); + }; + qq.UploadHandlerController = function(o, namespace) { + "use strict"; + var controller = this, chunkingPossible = false, concurrentChunkingPossible = false, chunking, preventRetryResponse, log, handler, options = { + paramsStore: {}, + maxConnections: 3, + chunking: { + enabled: false, + multiple: { + enabled: false + } + }, + log: function(str, level) {}, + onProgress: function(id, fileName, loaded, total) {}, + onComplete: function(id, fileName, response, xhr) {}, + onCancel: function(id, fileName) {}, + onUploadPrep: function(id) {}, + onUpload: function(id, fileName) {}, + onUploadChunk: function(id, fileName, chunkData) {}, + onUploadChunkSuccess: function(id, chunkData, response, xhr) {}, + onAutoRetry: function(id, fileName, response, xhr) {}, + onResume: function(id, fileName, chunkData) {}, + onUuidChanged: function(id, newUuid) {}, + getName: function(id) {}, + setSize: function(id, newSize) {}, + isQueued: function(id) {}, + getIdsInProxyGroup: function(id) {}, + getIdsInBatch: function(id) {} + }, chunked = { + done: function(id, chunkIdx, response, xhr) { + var chunkData = handler._getChunkData(id, chunkIdx); + handler._getFileState(id).attemptingResume = false; + delete handler._getFileState(id).temp.chunkProgress[chunkIdx]; + handler._getFileState(id).loaded += chunkData.size; + options.onUploadChunkSuccess(id, handler._getChunkDataForCallback(chunkData), response, xhr); + }, + finalize: function(id) { + var size = options.getSize(id), name = options.getName(id); + log("All chunks have been uploaded for " + id + " - finalizing...."); + handler.finalizeChunks(id).then(function(response, xhr) { + log("Finalize successful for " + id); + var normaizedResponse = upload.normalizeResponse(response, true); + options.onProgress(id, name, size, size); + handler._maybeDeletePersistedChunkData(id); + upload.cleanup(id, normaizedResponse, xhr); + }, function(response, xhr) { + var normaizedResponse = upload.normalizeResponse(response, false); + log("Problem finalizing chunks for file ID " + id + " - " + normaizedResponse.error, "error"); + if (normaizedResponse.reset) { + chunked.reset(id); + } + if (!options.onAutoRetry(id, name, normaizedResponse, xhr)) { + upload.cleanup(id, normaizedResponse, xhr); + } + }); + }, + handleFailure: function(chunkIdx, id, response, xhr) { + var name = options.getName(id); + log("Chunked upload request failed for " + id + ", chunk " + chunkIdx); + handler.clearCachedChunk(id, chunkIdx); + var responseToReport = upload.normalizeResponse(response, false), inProgressIdx; + if (responseToReport.reset) { + chunked.reset(id); + } else { + inProgressIdx = qq.indexOf(handler._getFileState(id).chunking.inProgress, chunkIdx); + if (inProgressIdx >= 0) { + handler._getFileState(id).chunking.inProgress.splice(inProgressIdx, 1); + handler._getFileState(id).chunking.remaining.unshift(chunkIdx); + } + } + if (!handler._getFileState(id).temp.ignoreFailure) { + if (concurrentChunkingPossible) { + handler._getFileState(id).temp.ignoreFailure = true; + log(qq.format("Going to attempt to abort these chunks: {}. These are currently in-progress: {}.", JSON.stringify(Object.keys(handler._getXhrs(id))), JSON.stringify(handler._getFileState(id).chunking.inProgress))); + qq.each(handler._getXhrs(id), function(ckid, ckXhr) { + log(qq.format("Attempting to abort file {}.{}. XHR readyState {}. ", id, ckid, ckXhr.readyState)); + ckXhr.abort(); + ckXhr._cancelled = true; + }); + handler.moveInProgressToRemaining(id); + connectionManager.free(id, true); + } + if (!options.onAutoRetry(id, name, responseToReport, xhr)) { + upload.cleanup(id, responseToReport, xhr); + } + } + }, + hasMoreParts: function(id) { + return !!handler._getFileState(id).chunking.remaining.length; + }, + nextPart: function(id) { + var nextIdx = handler._getFileState(id).chunking.remaining.shift(); + if (nextIdx >= handler._getTotalChunks(id)) { + nextIdx = null; + } + return nextIdx; + }, + reset: function(id) { + log("Server or callback has ordered chunking effort to be restarted on next attempt for item ID " + id, "error"); + handler._maybeDeletePersistedChunkData(id); + handler.reevaluateChunking(id); + handler._getFileState(id).loaded = 0; + }, + sendNext: function(id) { + var size = options.getSize(id), name = options.getName(id), chunkIdx = chunked.nextPart(id), chunkData = handler._getChunkData(id, chunkIdx), resuming = handler._getFileState(id).attemptingResume, inProgressChunks = handler._getFileState(id).chunking.inProgress || []; + if (handler._getFileState(id).loaded == null) { + handler._getFileState(id).loaded = 0; + } + if (resuming && options.onResume(id, name, chunkData) === false) { + chunked.reset(id); + chunkIdx = chunked.nextPart(id); + chunkData = handler._getChunkData(id, chunkIdx); + resuming = false; + } + if (chunkIdx == null && inProgressChunks.length === 0) { + chunked.finalize(id); + } else { + log(qq.format("Sending chunked upload request for item {}.{}, bytes {}-{} of {}.", id, chunkIdx, chunkData.start + 1, chunkData.end, size)); + options.onUploadChunk(id, name, handler._getChunkDataForCallback(chunkData)); + inProgressChunks.push(chunkIdx); + handler._getFileState(id).chunking.inProgress = inProgressChunks; + if (concurrentChunkingPossible) { + connectionManager.open(id, chunkIdx); + } + if (concurrentChunkingPossible && connectionManager.available() && handler._getFileState(id).chunking.remaining.length) { + chunked.sendNext(id); + } + if (chunkData.blob.size === 0) { + log(qq.format("Chunk {} for file {} will not be uploaded, zero sized chunk.", chunkIdx, id), "error"); + chunked.handleFailure(chunkIdx, id, "File is no longer available", null); + } else { + handler.uploadChunk(id, chunkIdx, resuming).then(function success(response, xhr) { + log("Chunked upload request succeeded for " + id + ", chunk " + chunkIdx); + handler.clearCachedChunk(id, chunkIdx); + var inProgressChunks = handler._getFileState(id).chunking.inProgress || [], responseToReport = upload.normalizeResponse(response, true), inProgressChunkIdx = qq.indexOf(inProgressChunks, chunkIdx); + log(qq.format("Chunk {} for file {} uploaded successfully.", chunkIdx, id)); + chunked.done(id, chunkIdx, responseToReport, xhr); + if (inProgressChunkIdx >= 0) { + inProgressChunks.splice(inProgressChunkIdx, 1); + } + handler._maybePersistChunkedState(id); + if (!chunked.hasMoreParts(id) && inProgressChunks.length === 0) { + chunked.finalize(id); + } else if (chunked.hasMoreParts(id)) { + chunked.sendNext(id); + } else { + log(qq.format("File ID {} has no more chunks to send and these chunk indexes are still marked as in-progress: {}", id, JSON.stringify(inProgressChunks))); + } + }, function failure(response, xhr) { + chunked.handleFailure(chunkIdx, id, response, xhr); + }).done(function() { + handler.clearXhr(id, chunkIdx); + }); + } + } + } + }, connectionManager = { + _open: [], + _openChunks: {}, + _waiting: [], + available: function() { + var max = options.maxConnections, openChunkEntriesCount = 0, openChunksCount = 0; + qq.each(connectionManager._openChunks, function(fileId, openChunkIndexes) { + openChunkEntriesCount++; + openChunksCount += openChunkIndexes.length; + }); + return max - (connectionManager._open.length - openChunkEntriesCount + openChunksCount); + }, + free: function(id, dontAllowNext) { + var allowNext = !dontAllowNext, waitingIndex = qq.indexOf(connectionManager._waiting, id), connectionsIndex = qq.indexOf(connectionManager._open, id), nextId; + delete connectionManager._openChunks[id]; + if (upload.getProxyOrBlob(id) instanceof qq.BlobProxy) { + log("Generated blob upload has ended for " + id + ", disposing generated blob."); + delete handler._getFileState(id).file; + } + if (waitingIndex >= 0) { + connectionManager._waiting.splice(waitingIndex, 1); + } else if (allowNext && connectionsIndex >= 0) { + connectionManager._open.splice(connectionsIndex, 1); + nextId = connectionManager._waiting.shift(); + if (nextId >= 0) { + connectionManager._open.push(nextId); + upload.start(nextId); + } + } + }, + getWaitingOrConnected: function() { + var waitingOrConnected = []; + qq.each(connectionManager._openChunks, function(fileId, chunks) { + if (chunks && chunks.length) { + waitingOrConnected.push(parseInt(fileId)); + } + }); + qq.each(connectionManager._open, function(idx, fileId) { + if (!connectionManager._openChunks[fileId]) { + waitingOrConnected.push(parseInt(fileId)); + } + }); + waitingOrConnected = waitingOrConnected.concat(connectionManager._waiting); + return waitingOrConnected; + }, + isUsingConnection: function(id) { + return qq.indexOf(connectionManager._open, id) >= 0; + }, + open: function(id, chunkIdx) { + if (chunkIdx == null) { + connectionManager._waiting.push(id); + } + if (connectionManager.available()) { + if (chunkIdx == null) { + connectionManager._waiting.pop(); + connectionManager._open.push(id); + } else { + (function() { + var openChunksEntry = connectionManager._openChunks[id] || []; + openChunksEntry.push(chunkIdx); + connectionManager._openChunks[id] = openChunksEntry; + })(); + } + return true; + } + return false; + }, + reset: function() { + connectionManager._waiting = []; + connectionManager._open = []; + } + }, simple = { + send: function(id, name) { + handler._getFileState(id).loaded = 0; + log("Sending simple upload request for " + id); + handler.uploadFile(id).then(function(response, optXhr) { + log("Simple upload request succeeded for " + id); + var responseToReport = upload.normalizeResponse(response, true), size = options.getSize(id); + options.onProgress(id, name, size, size); + upload.maybeNewUuid(id, responseToReport); + upload.cleanup(id, responseToReport, optXhr); + }, function(response, optXhr) { + log("Simple upload request failed for " + id); + var responseToReport = upload.normalizeResponse(response, false); + if (!options.onAutoRetry(id, name, responseToReport, optXhr)) { + upload.cleanup(id, responseToReport, optXhr); + } + }); + } + }, upload = { + cancel: function(id) { + log("Cancelling " + id); + options.paramsStore.remove(id); + connectionManager.free(id); + }, + cleanup: function(id, response, optXhr) { + var name = options.getName(id); + options.onComplete(id, name, response, optXhr); + if (handler._getFileState(id)) { + handler._clearXhrs && handler._clearXhrs(id); + } + connectionManager.free(id); + }, + getProxyOrBlob: function(id) { + return handler.getProxy && handler.getProxy(id) || handler.getFile && handler.getFile(id); + }, + initHandler: function() { + var handlerType = namespace ? qq[namespace] : qq.traditional, handlerModuleSubtype = qq.supportedFeatures.ajaxUploading ? "Xhr" : "Form"; + handler = new handlerType[handlerModuleSubtype + "UploadHandler"](options, { + getDataByUuid: options.getDataByUuid, + getName: options.getName, + getSize: options.getSize, + getUuid: options.getUuid, + log: log, + onCancel: options.onCancel, + onProgress: options.onProgress, + onUuidChanged: options.onUuidChanged + }); + if (handler._removeExpiredChunkingRecords) { + handler._removeExpiredChunkingRecords(); + } + }, + isDeferredEligibleForUpload: function(id) { + return options.isQueued(id); + }, + maybeDefer: function(id, blob) { + if (blob && !handler.getFile(id) && blob instanceof qq.BlobProxy) { + options.onUploadPrep(id); + log("Attempting to generate a blob on-demand for " + id); + blob.create().then(function(generatedBlob) { + log("Generated an on-demand blob for " + id); + handler.updateBlob(id, generatedBlob); + options.setSize(id, generatedBlob.size); + handler.reevaluateChunking(id); + upload.maybeSendDeferredFiles(id); + }, function(errorMessage) { + var errorResponse = {}; + if (errorMessage) { + errorResponse.error = errorMessage; + } + log(qq.format("Failed to generate blob for ID {}. Error message: {}.", id, errorMessage), "error"); + options.onComplete(id, options.getName(id), qq.extend(errorResponse, preventRetryResponse), null); + upload.maybeSendDeferredFiles(id); + connectionManager.free(id); + }); + } else { + return upload.maybeSendDeferredFiles(id); + } + return false; + }, + maybeSendDeferredFiles: function(id) { + var idsInGroup = options.getIdsInProxyGroup(id), uploadedThisId = false; + if (idsInGroup && idsInGroup.length) { + log("Maybe ready to upload proxy group file " + id); + qq.each(idsInGroup, function(idx, idInGroup) { + if (upload.isDeferredEligibleForUpload(idInGroup) && !!handler.getFile(idInGroup)) { + uploadedThisId = idInGroup === id; + upload.now(idInGroup); + } else if (upload.isDeferredEligibleForUpload(idInGroup)) { + return false; + } + }); + } else { + uploadedThisId = true; + upload.now(id); + } + return uploadedThisId; + }, + maybeNewUuid: function(id, response) { + if (response.newUuid !== undefined) { + options.onUuidChanged(id, response.newUuid); + } + }, + normalizeResponse: function(originalResponse, successful) { + var response = originalResponse; + if (!qq.isObject(originalResponse)) { + response = {}; + if (qq.isString(originalResponse) && !successful) { + response.error = originalResponse; + } + } + response.success = successful; + return response; + }, + now: function(id) { + var name = options.getName(id); + if (!controller.isValid(id)) { + throw new qq.Error(id + " is not a valid file ID to upload!"); + } + options.onUpload(id, name); + if (chunkingPossible && handler._shouldChunkThisFile(id)) { + chunked.sendNext(id); + } else { + simple.send(id, name); + } + }, + start: function(id) { + var blobToUpload = upload.getProxyOrBlob(id); + if (blobToUpload) { + return upload.maybeDefer(id, blobToUpload); + } else { + upload.now(id); + return true; + } + } + }; + qq.extend(this, { + add: function(id, file) { + handler.add.apply(this, arguments); + }, + upload: function(id) { + if (connectionManager.open(id)) { + return upload.start(id); + } + return false; + }, + retry: function(id) { + if (concurrentChunkingPossible) { + handler._getFileState(id).temp.ignoreFailure = false; + } + if (connectionManager.isUsingConnection(id)) { + return upload.start(id); + } else { + return controller.upload(id); + } + }, + cancel: function(id) { + var cancelRetVal = handler.cancel(id); + if (qq.isGenericPromise(cancelRetVal)) { + cancelRetVal.then(function() { + upload.cancel(id); + }); + } else if (cancelRetVal !== false) { + upload.cancel(id); + } + }, + cancelAll: function() { + var waitingOrConnected = connectionManager.getWaitingOrConnected(), i; + if (waitingOrConnected.length) { + for (i = waitingOrConnected.length - 1; i >= 0; i--) { + controller.cancel(waitingOrConnected[i]); + } + } + connectionManager.reset(); + }, + getFile: function(id) { + if (handler.getProxy && handler.getProxy(id)) { + return handler.getProxy(id).referenceBlob; + } + return handler.getFile && handler.getFile(id); + }, + isProxied: function(id) { + return !!(handler.getProxy && handler.getProxy(id)); + }, + getInput: function(id) { + if (handler.getInput) { + return handler.getInput(id); + } + }, + reset: function() { + log("Resetting upload handler"); + controller.cancelAll(); + connectionManager.reset(); + handler.reset(); + }, + expunge: function(id) { + if (controller.isValid(id)) { + return handler.expunge(id); + } + }, + isValid: function(id) { + return handler.isValid(id); + }, + getResumableFilesData: function() { + if (handler.getResumableFilesData) { + return handler.getResumableFilesData(); + } + return []; + }, + getThirdPartyFileId: function(id) { + if (controller.isValid(id)) { + return handler.getThirdPartyFileId(id); + } + }, + pause: function(id) { + if (controller.isResumable(id) && handler.pause && controller.isValid(id) && handler.pause(id)) { + connectionManager.free(id); + handler.moveInProgressToRemaining(id); + return true; + } + return false; + }, + isResumable: function(id) { + return !!handler.isResumable && handler.isResumable(id); + } + }); + qq.extend(options, o); + log = options.log; + chunkingPossible = options.chunking.enabled && qq.supportedFeatures.chunking; + concurrentChunkingPossible = chunkingPossible && options.chunking.concurrent.enabled; + preventRetryResponse = function() { + var response = {}; + response[options.preventRetryParam] = true; + return response; + }(); + upload.initHandler(); + }; + qq.WindowReceiveMessage = function(o) { + "use strict"; + var options = { + log: function(message, level) {} + }, callbackWrapperDetachers = {}; + qq.extend(options, o); + qq.extend(this, { + receiveMessage: function(id, callback) { + var onMessageCallbackWrapper = function(event) { + callback(event.data); + }; + if (window.postMessage) { + callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper); + } else { + log("iframe message passing not supported in this browser!", "error"); + } + }, + stopReceivingMessages: function(id) { + if (window.postMessage) { + var detacher = callbackWrapperDetachers[id]; + if (detacher) { + detacher(); + } + } + } + }); + }; + qq.FormUploadHandler = function(spec) { + "use strict"; + var options = spec.options, handler = this, proxy = spec.proxy, formHandlerInstanceId = qq.getUniqueId(), onloadCallbacks = {}, detachLoadEvents = {}, postMessageCallbackTimers = {}, isCors = options.isCors, inputName = options.inputName, getUuid = proxy.getUuid, log = proxy.log, corsMessageReceiver = new qq.WindowReceiveMessage({ + log: log + }); + function expungeFile(id) { + delete detachLoadEvents[id]; + if (isCors) { + clearTimeout(postMessageCallbackTimers[id]); + delete postMessageCallbackTimers[id]; + corsMessageReceiver.stopReceivingMessages(id); + } + var iframe = document.getElementById(handler._getIframeName(id)); + if (iframe) { + iframe.setAttribute("src", "javascript:false;"); + qq(iframe).remove(); + } + } + function getFileIdForIframeName(iframeName) { + return iframeName.split("_")[0]; + } + function initIframeForUpload(name) { + var iframe = qq.toElement(""); + iframe.setAttribute("id", name); + iframe.style.display = "none"; + document.body.appendChild(iframe); + return iframe; + } + function registerPostMessageCallback(iframe, callback) { + var iframeName = iframe.id, fileId = getFileIdForIframeName(iframeName), uuid = getUuid(fileId); + onloadCallbacks[uuid] = callback; + detachLoadEvents[fileId] = qq(iframe).attach("load", function() { + if (handler.getInput(fileId)) { + log("Received iframe load event for CORS upload request (iframe name " + iframeName + ")"); + postMessageCallbackTimers[iframeName] = setTimeout(function() { + var errorMessage = "No valid message received from loaded iframe for iframe name " + iframeName; + log(errorMessage, "error"); + callback({ + error: errorMessage + }); + }, 1e3); + } + }); + corsMessageReceiver.receiveMessage(iframeName, function(message) { + log("Received the following window message: '" + message + "'"); + var fileId = getFileIdForIframeName(iframeName), response = handler._parseJsonResponse(message), uuid = response.uuid, onloadCallback; + if (uuid && onloadCallbacks[uuid]) { + log("Handling response for iframe name " + iframeName); + clearTimeout(postMessageCallbackTimers[iframeName]); + delete postMessageCallbackTimers[iframeName]; + handler._detachLoadEvent(iframeName); + onloadCallback = onloadCallbacks[uuid]; + delete onloadCallbacks[uuid]; + corsMessageReceiver.stopReceivingMessages(iframeName); + onloadCallback(response); + } else if (!uuid) { + log("'" + message + "' does not contain a UUID - ignoring."); + } + }); + } + qq.extend(this, new qq.UploadHandler(spec)); + qq.override(this, function(super_) { + return { + add: function(id, fileInput) { + super_.add(id, { + input: fileInput + }); + fileInput.setAttribute("name", inputName); + if (fileInput.parentNode) { + qq(fileInput).remove(); + } + }, + expunge: function(id) { + expungeFile(id); + super_.expunge(id); + }, + isValid: function(id) { + return super_.isValid(id) && handler._getFileState(id).input !== undefined; + } + }; + }); + qq.extend(this, { + getInput: function(id) { + return handler._getFileState(id).input; + }, + _attachLoadEvent: function(iframe, callback) { + var responseDescriptor; + if (isCors) { + registerPostMessageCallback(iframe, callback); + } else { + detachLoadEvents[iframe.id] = qq(iframe).attach("load", function() { + log("Received response for " + iframe.id); + if (!iframe.parentNode) { + return; + } + try { + if (iframe.contentDocument && iframe.contentDocument.body && iframe.contentDocument.body.innerHTML == "false") { + return; + } + } catch (error) { + log("Error when attempting to access iframe during handling of upload response (" + error.message + ")", "error"); + responseDescriptor = { + success: false + }; + } + callback(responseDescriptor); + }); + } + }, + _createIframe: function(id) { + var iframeName = handler._getIframeName(id); + return initIframeForUpload(iframeName); + }, + _detachLoadEvent: function(id) { + if (detachLoadEvents[id] !== undefined) { + detachLoadEvents[id](); + delete detachLoadEvents[id]; + } + }, + _getIframeName: function(fileId) { + return fileId + "_" + formHandlerInstanceId; + }, + _initFormForUpload: function(spec) { + var method = spec.method, endpoint = spec.endpoint, params = spec.params, paramsInBody = spec.paramsInBody, targetName = spec.targetName, form = qq.toElement("
"), url = endpoint; + if (paramsInBody) { + qq.obj2Inputs(params, form); + } else { + url = qq.obj2url(params, endpoint); + } + form.setAttribute("action", url); + form.setAttribute("target", targetName); + form.style.display = "none"; + document.body.appendChild(form); + return form; + }, + _parseJsonResponse: function(innerHtmlOrMessage) { + var response = {}; + try { + response = qq.parseJson(innerHtmlOrMessage); + } catch (error) { + log("Error when attempting to parse iframe upload response (" + error.message + ")", "error"); + } + return response; + } + }); + }; + qq.XhrUploadHandler = function(spec) { + "use strict"; + var handler = this, namespace = spec.options.namespace, proxy = spec.proxy, chunking = spec.options.chunking, resume = spec.options.resume, chunkFiles = chunking && spec.options.chunking.enabled && qq.supportedFeatures.chunking, resumeEnabled = resume && spec.options.resume.enabled && chunkFiles && qq.supportedFeatures.resume, getName = proxy.getName, getSize = proxy.getSize, getUuid = proxy.getUuid, getEndpoint = proxy.getEndpoint, getDataByUuid = proxy.getDataByUuid, onUuidChanged = proxy.onUuidChanged, onProgress = proxy.onProgress, log = proxy.log; + function abort(id) { + qq.each(handler._getXhrs(id), function(xhrId, xhr) { + var ajaxRequester = handler._getAjaxRequester(id, xhrId); + xhr.onreadystatechange = null; + xhr.upload.onprogress = null; + xhr.abort(); + ajaxRequester && ajaxRequester.canceled && ajaxRequester.canceled(id); + }); + } + qq.extend(this, new qq.UploadHandler(spec)); + qq.override(this, function(super_) { + return { + add: function(id, blobOrProxy) { + if (qq.isFile(blobOrProxy) || qq.isBlob(blobOrProxy)) { + super_.add(id, { + file: blobOrProxy + }); + } else if (blobOrProxy instanceof qq.BlobProxy) { + super_.add(id, { + proxy: blobOrProxy + }); + } else { + throw new Error("Passed obj is not a File, Blob, or proxy"); + } + handler._initTempState(id); + resumeEnabled && handler._maybePrepareForResume(id); + }, + expunge: function(id) { + abort(id); + handler._maybeDeletePersistedChunkData(id); + handler._clearXhrs(id); + super_.expunge(id); + } + }; + }); + qq.extend(this, { + clearCachedChunk: function(id, chunkIdx) { + delete handler._getFileState(id).temp.cachedChunks[chunkIdx]; + }, + clearXhr: function(id, chunkIdx) { + var tempState = handler._getFileState(id).temp; + if (tempState.xhrs) { + delete tempState.xhrs[chunkIdx]; + } + if (tempState.ajaxRequesters) { + delete tempState.ajaxRequesters[chunkIdx]; + } + }, + finalizeChunks: function(id, responseParser) { + var lastChunkIdx = handler._getTotalChunks(id) - 1, xhr = handler._getXhr(id, lastChunkIdx); + if (responseParser) { + return new qq.Promise().success(responseParser(xhr), xhr); + } + return new qq.Promise().success({}, xhr); + }, + getFile: function(id) { + return handler.isValid(id) && handler._getFileState(id).file; + }, + getProxy: function(id) { + return handler.isValid(id) && handler._getFileState(id).proxy; + }, + getResumableFilesData: function() { + var resumableFilesData = []; + handler._iterateResumeRecords(function(key, uploadData) { + handler.moveInProgressToRemaining(null, uploadData.chunking.inProgress, uploadData.chunking.remaining); + var data = { + name: uploadData.name, + remaining: uploadData.chunking.remaining, + size: uploadData.size, + uuid: uploadData.uuid + }; + if (uploadData.key) { + data.key = uploadData.key; + } + resumableFilesData.push(data); + }); + return resumableFilesData; + }, + isResumable: function(id) { + return !!chunking && handler.isValid(id) && !handler._getFileState(id).notResumable; + }, + moveInProgressToRemaining: function(id, optInProgress, optRemaining) { + var inProgress = optInProgress || handler._getFileState(id).chunking.inProgress, remaining = optRemaining || handler._getFileState(id).chunking.remaining; + if (inProgress) { + log(qq.format("Moving these chunks from in-progress {}, to remaining.", JSON.stringify(inProgress))); + inProgress.reverse(); + qq.each(inProgress, function(idx, chunkIdx) { + remaining.unshift(chunkIdx); + }); + inProgress.length = 0; + } + }, + pause: function(id) { + if (handler.isValid(id)) { + log(qq.format("Aborting XHR upload for {} '{}' due to pause instruction.", id, getName(id))); + handler._getFileState(id).paused = true; + abort(id); + return true; + } + }, + reevaluateChunking: function(id) { + if (chunking && handler.isValid(id)) { + var state = handler._getFileState(id), totalChunks, i; + delete state.chunking; + state.chunking = {}; + totalChunks = handler._getTotalChunks(id); + if (totalChunks > 1 || chunking.mandatory) { + state.chunking.enabled = true; + state.chunking.parts = totalChunks; + state.chunking.remaining = []; + for (i = 0; i < totalChunks; i++) { + state.chunking.remaining.push(i); + } + handler._initTempState(id); + } else { + state.chunking.enabled = false; + } + } + }, + updateBlob: function(id, newBlob) { + if (handler.isValid(id)) { + handler._getFileState(id).file = newBlob; + } + }, + _clearXhrs: function(id) { + var tempState = handler._getFileState(id).temp; + qq.each(tempState.ajaxRequesters, function(chunkId) { + delete tempState.ajaxRequesters[chunkId]; + }); + qq.each(tempState.xhrs, function(chunkId) { + delete tempState.xhrs[chunkId]; + }); + }, + _createXhr: function(id, optChunkIdx) { + return handler._registerXhr(id, optChunkIdx, qq.createXhrInstance()); + }, + _getAjaxRequester: function(id, optChunkIdx) { + var chunkIdx = optChunkIdx == null ? -1 : optChunkIdx; + return handler._getFileState(id).temp.ajaxRequesters[chunkIdx]; + }, + _getChunkData: function(id, chunkIndex) { + var chunkSize = chunking.partSize, fileSize = getSize(id), fileOrBlob = handler.getFile(id), startBytes = chunkSize * chunkIndex, endBytes = startBytes + chunkSize >= fileSize ? fileSize : startBytes + chunkSize, totalChunks = handler._getTotalChunks(id), cachedChunks = this._getFileState(id).temp.cachedChunks, blob = cachedChunks[chunkIndex] || qq.sliceBlob(fileOrBlob, startBytes, endBytes); + cachedChunks[chunkIndex] = blob; + return { + part: chunkIndex, + start: startBytes, + end: endBytes, + count: totalChunks, + blob: blob, + size: endBytes - startBytes + }; + }, + _getChunkDataForCallback: function(chunkData) { + return { + partIndex: chunkData.part, + startByte: chunkData.start + 1, + endByte: chunkData.end, + totalParts: chunkData.count + }; + }, + _getLocalStorageId: function(id) { + var formatVersion = "5.0", name = getName(id), size = getSize(id), chunkSize = chunking.partSize, endpoint = getEndpoint(id); + return qq.format("qq{}resume{}-{}-{}-{}-{}", namespace, formatVersion, name, size, chunkSize, endpoint); + }, + _getMimeType: function(id) { + return handler.getFile(id).type; + }, + _getPersistableData: function(id) { + return handler._getFileState(id).chunking; + }, + _getTotalChunks: function(id) { + if (chunking) { + var fileSize = getSize(id), chunkSize = chunking.partSize; + return Math.ceil(fileSize / chunkSize); + } + }, + _getXhr: function(id, optChunkIdx) { + var chunkIdx = optChunkIdx == null ? -1 : optChunkIdx; + return handler._getFileState(id).temp.xhrs[chunkIdx]; + }, + _getXhrs: function(id) { + return handler._getFileState(id).temp.xhrs; + }, + _iterateResumeRecords: function(callback) { + if (resumeEnabled) { + qq.each(localStorage, function(key, item) { + if (key.indexOf(qq.format("qq{}resume", namespace)) === 0) { + var uploadData = JSON.parse(item); + callback(key, uploadData); + } + }); + } + }, + _initTempState: function(id) { + handler._getFileState(id).temp = { + ajaxRequesters: {}, + chunkProgress: {}, + xhrs: {}, + cachedChunks: {} + }; + }, + _markNotResumable: function(id) { + handler._getFileState(id).notResumable = true; + }, + _maybeDeletePersistedChunkData: function(id) { + var localStorageId; + if (resumeEnabled && handler.isResumable(id)) { + localStorageId = handler._getLocalStorageId(id); + if (localStorageId && localStorage.getItem(localStorageId)) { + localStorage.removeItem(localStorageId); + return true; + } + } + return false; + }, + _maybePrepareForResume: function(id) { + var state = handler._getFileState(id), localStorageId, persistedData; + if (resumeEnabled && state.key === undefined) { + localStorageId = handler._getLocalStorageId(id); + persistedData = localStorage.getItem(localStorageId); + if (persistedData) { + persistedData = JSON.parse(persistedData); + if (getDataByUuid(persistedData.uuid)) { + handler._markNotResumable(id); + } else { + log(qq.format("Identified file with ID {} and name of {} as resumable.", id, getName(id))); + onUuidChanged(id, persistedData.uuid); + state.key = persistedData.key; + state.chunking = persistedData.chunking; + state.loaded = persistedData.loaded; + state.attemptingResume = true; + handler.moveInProgressToRemaining(id); + } + } + } + }, + _maybePersistChunkedState: function(id) { + var state = handler._getFileState(id), localStorageId, persistedData; + if (resumeEnabled && handler.isResumable(id)) { + localStorageId = handler._getLocalStorageId(id); + persistedData = { + name: getName(id), + size: getSize(id), + uuid: getUuid(id), + key: state.key, + chunking: state.chunking, + loaded: state.loaded, + lastUpdated: Date.now() + }; + try { + localStorage.setItem(localStorageId, JSON.stringify(persistedData)); + } catch (error) { + log(qq.format("Unable to save resume data for '{}' due to error: '{}'.", id, error.toString()), "warn"); + } + } + }, + _registerProgressHandler: function(id, chunkIdx, chunkSize) { + var xhr = handler._getXhr(id, chunkIdx), name = getName(id), progressCalculator = { + simple: function(loaded, total) { + var fileSize = getSize(id); + if (loaded === total) { + onProgress(id, name, fileSize, fileSize); + } else { + onProgress(id, name, loaded >= fileSize ? fileSize - 1 : loaded, fileSize); + } + }, + chunked: function(loaded, total) { + var chunkProgress = handler._getFileState(id).temp.chunkProgress, totalSuccessfullyLoadedForFile = handler._getFileState(id).loaded, loadedForRequest = loaded, totalForRequest = total, totalFileSize = getSize(id), estActualChunkLoaded = loadedForRequest - (totalForRequest - chunkSize), totalLoadedForFile = totalSuccessfullyLoadedForFile; + chunkProgress[chunkIdx] = estActualChunkLoaded; + qq.each(chunkProgress, function(chunkIdx, chunkLoaded) { + totalLoadedForFile += chunkLoaded; + }); + onProgress(id, name, totalLoadedForFile, totalFileSize); + } + }; + xhr.upload.onprogress = function(e) { + if (e.lengthComputable) { + var type = chunkSize == null ? "simple" : "chunked"; + progressCalculator[type](e.loaded, e.total); + } + }; + }, + _registerXhr: function(id, optChunkIdx, xhr, optAjaxRequester) { + var xhrsId = optChunkIdx == null ? -1 : optChunkIdx, tempState = handler._getFileState(id).temp; + tempState.xhrs = tempState.xhrs || {}; + tempState.ajaxRequesters = tempState.ajaxRequesters || {}; + tempState.xhrs[xhrsId] = xhr; + if (optAjaxRequester) { + tempState.ajaxRequesters[xhrsId] = optAjaxRequester; + } + return xhr; + }, + _removeExpiredChunkingRecords: function() { + var expirationDays = resume.recordsExpireIn; + handler._iterateResumeRecords(function(key, uploadData) { + var expirationDate = new Date(uploadData.lastUpdated); + expirationDate.setDate(expirationDate.getDate() + expirationDays); + if (expirationDate.getTime() <= Date.now()) { + log("Removing expired resume record with key " + key); + localStorage.removeItem(key); + } + }); + }, + _shouldChunkThisFile: function(id) { + var state = handler._getFileState(id); + if (!state.chunking) { + handler.reevaluateChunking(id); + } + return state.chunking.enabled; + } + }); + }; + qq.DeleteFileAjaxRequester = function(o) { + "use strict"; + var requester, options = { + method: "DELETE", + uuidParamName: "qquuid", + endpointStore: {}, + maxConnections: 3, + customHeaders: function(id) { + return {}; + }, + paramsStore: {}, + cors: { + expected: false, + sendCredentials: false + }, + log: function(str, level) {}, + onDelete: function(id) {}, + onDeleteComplete: function(id, xhrOrXdr, isError) {} + }; + qq.extend(options, o); + function getMandatedParams() { + if (options.method.toUpperCase() === "POST") { + return { + _method: "DELETE" + }; + } + return {}; + } + requester = qq.extend(this, new qq.AjaxRequester({ + acceptHeader: "application/json", + validMethods: [ "POST", "DELETE" ], + method: options.method, + endpointStore: options.endpointStore, + paramsStore: options.paramsStore, + mandatedParams: getMandatedParams(), + maxConnections: options.maxConnections, + customHeaders: function(id) { + return options.customHeaders.get(id); + }, + log: options.log, + onSend: options.onDelete, + onComplete: options.onDeleteComplete, + cors: options.cors + })); + qq.extend(this, { + sendDelete: function(id, uuid, additionalMandatedParams) { + var additionalOptions = additionalMandatedParams || {}; + options.log("Submitting delete file request for " + id); + if (options.method === "DELETE") { + requester.initTransport(id).withPath(uuid).withParams(additionalOptions).send(); + } else { + additionalOptions[options.uuidParamName] = uuid; + requester.initTransport(id).withParams(additionalOptions).send(); + } + } + }); + }; + (function() { + function detectSubsampling(img) { + var iw = img.naturalWidth, ih = img.naturalHeight, canvas = document.createElement("canvas"), ctx; + if (iw * ih > 1024 * 1024) { + canvas.width = canvas.height = 1; + ctx = canvas.getContext("2d"); + ctx.drawImage(img, -iw + 1, 0); + return ctx.getImageData(0, 0, 1, 1).data[3] === 0; + } else { + return false; + } + } + function detectVerticalSquash(img, iw, ih) { + var canvas = document.createElement("canvas"), sy = 0, ey = ih, py = ih, ctx, data, alpha, ratio; + canvas.width = 1; + canvas.height = ih; + ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + data = ctx.getImageData(0, 0, 1, ih).data; + while (py > sy) { + alpha = data[(py - 1) * 4 + 3]; + if (alpha === 0) { + ey = py; + } else { + sy = py; + } + py = ey + sy >> 1; + } + ratio = py / ih; + return ratio === 0 ? 1 : ratio; + } + function renderImageToDataURL(img, blob, options, doSquash) { + var canvas = document.createElement("canvas"), mime = options.mime || "image/jpeg", promise = new qq.Promise(); + renderImageToCanvas(img, blob, canvas, options, doSquash).then(function() { + promise.success(canvas.toDataURL(mime, options.quality || .8)); + }); + return promise; + } + function maybeCalculateDownsampledDimensions(spec) { + var maxPixels = 5241e3; + if (!qq.ios()) { + throw new qq.Error("Downsampled dimensions can only be reliably calculated for iOS!"); + } + if (spec.origHeight * spec.origWidth > maxPixels) { + return { + newHeight: Math.round(Math.sqrt(maxPixels * (spec.origHeight / spec.origWidth))), + newWidth: Math.round(Math.sqrt(maxPixels * (spec.origWidth / spec.origHeight))) + }; + } + } + function renderImageToCanvas(img, blob, canvas, options, doSquash) { + var iw = img.naturalWidth, ih = img.naturalHeight, width = options.width, height = options.height, ctx = canvas.getContext("2d"), promise = new qq.Promise(), modifiedDimensions; + ctx.save(); + if (options.resize) { + return renderImageToCanvasWithCustomResizer({ + blob: blob, + canvas: canvas, + image: img, + imageHeight: ih, + imageWidth: iw, + orientation: options.orientation, + resize: options.resize, + targetHeight: height, + targetWidth: width + }); + } + if (!qq.supportedFeatures.unlimitedScaledImageSize) { + modifiedDimensions = maybeCalculateDownsampledDimensions({ + origWidth: width, + origHeight: height + }); + if (modifiedDimensions) { + qq.log(qq.format("Had to reduce dimensions due to device limitations from {}w / {}h to {}w / {}h", width, height, modifiedDimensions.newWidth, modifiedDimensions.newHeight), "warn"); + width = modifiedDimensions.newWidth; + height = modifiedDimensions.newHeight; + } + } + transformCoordinate(canvas, width, height, options.orientation); + if (qq.ios()) { + (function() { + if (detectSubsampling(img)) { + iw /= 2; + ih /= 2; + } + var d = 1024, tmpCanvas = document.createElement("canvas"), vertSquashRatio = doSquash ? detectVerticalSquash(img, iw, ih) : 1, dw = Math.ceil(d * width / iw), dh = Math.ceil(d * height / ih / vertSquashRatio), sy = 0, dy = 0, tmpCtx, sx, dx; + tmpCanvas.width = tmpCanvas.height = d; + tmpCtx = tmpCanvas.getContext("2d"); + while (sy < ih) { + sx = 0; + dx = 0; + while (sx < iw) { + tmpCtx.clearRect(0, 0, d, d); + tmpCtx.drawImage(img, -sx, -sy); + ctx.drawImage(tmpCanvas, 0, 0, d, d, dx, dy, dw, dh); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + })(); + } else { + ctx.drawImage(img, 0, 0, width, height); + } + canvas.qqImageRendered && canvas.qqImageRendered(); + promise.success(); + return promise; + } + function renderImageToCanvasWithCustomResizer(resizeInfo) { + var blob = resizeInfo.blob, image = resizeInfo.image, imageHeight = resizeInfo.imageHeight, imageWidth = resizeInfo.imageWidth, orientation = resizeInfo.orientation, promise = new qq.Promise(), resize = resizeInfo.resize, sourceCanvas = document.createElement("canvas"), sourceCanvasContext = sourceCanvas.getContext("2d"), targetCanvas = resizeInfo.canvas, targetHeight = resizeInfo.targetHeight, targetWidth = resizeInfo.targetWidth; + transformCoordinate(sourceCanvas, imageWidth, imageHeight, orientation); + targetCanvas.height = targetHeight; + targetCanvas.width = targetWidth; + sourceCanvasContext.drawImage(image, 0, 0); + resize({ + blob: blob, + height: targetHeight, + image: image, + sourceCanvas: sourceCanvas, + targetCanvas: targetCanvas, + width: targetWidth + }).then(function success() { + targetCanvas.qqImageRendered && targetCanvas.qqImageRendered(); + promise.success(); + }, promise.failure); + return promise; + } + function transformCoordinate(canvas, width, height, orientation) { + switch (orientation) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + + default: + canvas.width = width; + canvas.height = height; + } + var ctx = canvas.getContext("2d"); + switch (orientation) { + case 2: + ctx.translate(width, 0); + ctx.scale(-1, 1); + break; + + case 3: + ctx.translate(width, height); + ctx.rotate(Math.PI); + break; + + case 4: + ctx.translate(0, height); + ctx.scale(1, -1); + break; + + case 5: + ctx.rotate(.5 * Math.PI); + ctx.scale(1, -1); + break; + + case 6: + ctx.rotate(.5 * Math.PI); + ctx.translate(0, -height); + break; + + case 7: + ctx.rotate(.5 * Math.PI); + ctx.translate(width, -height); + ctx.scale(-1, 1); + break; + + case 8: + ctx.rotate(-.5 * Math.PI); + ctx.translate(-width, 0); + break; + + default: + break; + } + } + function MegaPixImage(srcImage, errorCallback) { + var self = this; + if (window.Blob && srcImage instanceof Blob) { + (function() { + var img = new Image(), URL = window.URL && window.URL.createObjectURL ? window.URL : window.webkitURL && window.webkitURL.createObjectURL ? window.webkitURL : null; + if (!URL) { + throw Error("No createObjectURL function found to create blob url"); + } + img.src = URL.createObjectURL(srcImage); + self.blob = srcImage; + srcImage = img; + })(); + } + if (!srcImage.naturalWidth && !srcImage.naturalHeight) { + srcImage.onload = function() { + var listeners = self.imageLoadListeners; + if (listeners) { + self.imageLoadListeners = null; + setTimeout(function() { + for (var i = 0, len = listeners.length; i < len; i++) { + listeners[i](); + } + }, 0); + } + }; + srcImage.onerror = errorCallback; + this.imageLoadListeners = []; + } + this.srcImage = srcImage; + } + MegaPixImage.prototype.render = function(target, options) { + options = options || {}; + var self = this, imgWidth = this.srcImage.naturalWidth, imgHeight = this.srcImage.naturalHeight, width = options.width, height = options.height, maxWidth = options.maxWidth, maxHeight = options.maxHeight, doSquash = !this.blob || this.blob.type === "image/jpeg", tagName = target.tagName.toLowerCase(), opt; + if (this.imageLoadListeners) { + this.imageLoadListeners.push(function() { + self.render(target, options); + }); + return; + } + if (width && !height) { + height = imgHeight * width / imgWidth << 0; + } else if (height && !width) { + width = imgWidth * height / imgHeight << 0; + } else { + width = imgWidth; + height = imgHeight; + } + if (maxWidth && width > maxWidth) { + width = maxWidth; + height = imgHeight * width / imgWidth << 0; + } + if (maxHeight && height > maxHeight) { + height = maxHeight; + width = imgWidth * height / imgHeight << 0; + } + opt = { + width: width, + height: height + }, qq.each(options, function(optionsKey, optionsValue) { + opt[optionsKey] = optionsValue; + }); + if (tagName === "img") { + (function() { + var oldTargetSrc = target.src; + renderImageToDataURL(self.srcImage, self.blob, opt, doSquash).then(function(dataUri) { + target.src = dataUri; + oldTargetSrc === target.src && target.onload(); + }); + })(); + } else if (tagName === "canvas") { + renderImageToCanvas(this.srcImage, this.blob, target, opt, doSquash); + } + if (typeof this.onrender === "function") { + this.onrender(target); + } + }; + qq.MegaPixImage = MegaPixImage; + })(); + qq.ImageGenerator = function(log) { + "use strict"; + function isImg(el) { + return el.tagName.toLowerCase() === "img"; + } + function isCanvas(el) { + return el.tagName.toLowerCase() === "canvas"; + } + function isImgCorsSupported() { + return new Image().crossOrigin !== undefined; + } + function isCanvasSupported() { + var canvas = document.createElement("canvas"); + return canvas.getContext && canvas.getContext("2d"); + } + function determineMimeOfFileName(nameWithPath) { + var pathSegments = nameWithPath.split("/"), name = pathSegments[pathSegments.length - 1].split("?")[0], extension = qq.getExtension(name); + extension = extension && extension.toLowerCase(); + switch (extension) { + case "jpeg": + case "jpg": + return "image/jpeg"; + + case "png": + return "image/png"; + + case "bmp": + return "image/bmp"; + + case "gif": + return "image/gif"; + + case "tiff": + case "tif": + return "image/tiff"; + } + } + function isCrossOrigin(url) { + var targetAnchor = document.createElement("a"), targetProtocol, targetHostname, targetPort; + targetAnchor.href = url; + targetProtocol = targetAnchor.protocol; + targetPort = targetAnchor.port; + targetHostname = targetAnchor.hostname; + if (targetProtocol.toLowerCase() !== window.location.protocol.toLowerCase()) { + return true; + } + if (targetHostname.toLowerCase() !== window.location.hostname.toLowerCase()) { + return true; + } + if (targetPort !== window.location.port && !qq.ie()) { + return true; + } + return false; + } + function registerImgLoadListeners(img, promise) { + img.onload = function() { + img.onload = null; + img.onerror = null; + promise.success(img); + }; + img.onerror = function() { + img.onload = null; + img.onerror = null; + log("Problem drawing thumbnail!", "error"); + promise.failure(img, "Problem drawing thumbnail!"); + }; + } + function registerCanvasDrawImageListener(canvas, promise) { + canvas.qqImageRendered = function() { + promise.success(canvas); + }; + } + function registerThumbnailRenderedListener(imgOrCanvas, promise) { + var registered = isImg(imgOrCanvas) || isCanvas(imgOrCanvas); + if (isImg(imgOrCanvas)) { + registerImgLoadListeners(imgOrCanvas, promise); + } else if (isCanvas(imgOrCanvas)) { + registerCanvasDrawImageListener(imgOrCanvas, promise); + } else { + promise.failure(imgOrCanvas); + log(qq.format("Element container of type {} is not supported!", imgOrCanvas.tagName), "error"); + } + return registered; + } + function draw(fileOrBlob, container, options) { + var drawPreview = new qq.Promise(), identifier = new qq.Identify(fileOrBlob, log), maxSize = options.maxSize, orient = options.orient == null ? true : options.orient, megapixErrorHandler = function() { + container.onerror = null; + container.onload = null; + log("Could not render preview, file may be too large!", "error"); + drawPreview.failure(container, "Browser cannot render image!"); + }; + identifier.isPreviewable().then(function(mime) { + var dummyExif = { + parse: function() { + return new qq.Promise().success(); + } + }, exif = orient ? new qq.Exif(fileOrBlob, log) : dummyExif, mpImg = new qq.MegaPixImage(fileOrBlob, megapixErrorHandler); + if (registerThumbnailRenderedListener(container, drawPreview)) { + exif.parse().then(function(exif) { + var orientation = exif && exif.Orientation; + mpImg.render(container, { + maxWidth: maxSize, + maxHeight: maxSize, + orientation: orientation, + mime: mime, + resize: options.customResizeFunction + }); + }, function(failureMsg) { + log(qq.format("EXIF data could not be parsed ({}). Assuming orientation = 1.", failureMsg)); + mpImg.render(container, { + maxWidth: maxSize, + maxHeight: maxSize, + mime: mime, + resize: options.customResizeFunction + }); + }); + } + }, function() { + log("Not previewable"); + drawPreview.failure(container, "Not previewable"); + }); + return drawPreview; + } + function drawOnCanvasOrImgFromUrl(url, canvasOrImg, draw, maxSize, customResizeFunction) { + var tempImg = new Image(), tempImgRender = new qq.Promise(); + registerThumbnailRenderedListener(tempImg, tempImgRender); + if (isCrossOrigin(url)) { + tempImg.crossOrigin = "anonymous"; + } + tempImg.src = url; + tempImgRender.then(function rendered() { + registerThumbnailRenderedListener(canvasOrImg, draw); + var mpImg = new qq.MegaPixImage(tempImg); + mpImg.render(canvasOrImg, { + maxWidth: maxSize, + maxHeight: maxSize, + mime: determineMimeOfFileName(url), + resize: customResizeFunction + }); + }, draw.failure); + } + function drawOnImgFromUrlWithCssScaling(url, img, draw, maxSize) { + registerThumbnailRenderedListener(img, draw); + qq(img).css({ + maxWidth: maxSize + "px", + maxHeight: maxSize + "px" + }); + img.src = url; + } + function drawFromUrl(url, container, options) { + var draw = new qq.Promise(), scale = options.scale, maxSize = scale ? options.maxSize : null; + if (scale && isImg(container)) { + if (isCanvasSupported()) { + if (isCrossOrigin(url) && !isImgCorsSupported()) { + drawOnImgFromUrlWithCssScaling(url, container, draw, maxSize); + } else { + drawOnCanvasOrImgFromUrl(url, container, draw, maxSize); + } + } else { + drawOnImgFromUrlWithCssScaling(url, container, draw, maxSize); + } + } else if (isCanvas(container)) { + drawOnCanvasOrImgFromUrl(url, container, draw, maxSize); + } else if (registerThumbnailRenderedListener(container, draw)) { + container.src = url; + } + return draw; + } + qq.extend(this, { + generate: function(fileBlobOrUrl, container, options) { + if (qq.isString(fileBlobOrUrl)) { + log("Attempting to update thumbnail based on server response."); + return drawFromUrl(fileBlobOrUrl, container, options || {}); + } else { + log("Attempting to draw client-side image preview."); + return draw(fileBlobOrUrl, container, options || {}); + } + } + }); + this._testing = {}; + this._testing.isImg = isImg; + this._testing.isCanvas = isCanvas; + this._testing.isCrossOrigin = isCrossOrigin; + this._testing.determineMimeOfFileName = determineMimeOfFileName; + }; + qq.Exif = function(fileOrBlob, log) { + "use strict"; + var TAG_IDS = [ 274 ], TAG_INFO = { + 274: { + name: "Orientation", + bytes: 2 + } + }; + function parseLittleEndian(hex) { + var result = 0, pow = 0; + while (hex.length > 0) { + result += parseInt(hex.substring(0, 2), 16) * Math.pow(2, pow); + hex = hex.substring(2, hex.length); + pow += 8; + } + return result; + } + function seekToApp1(offset, promise) { + var theOffset = offset, thePromise = promise; + if (theOffset === undefined) { + theOffset = 2; + thePromise = new qq.Promise(); + } + qq.readBlobToHex(fileOrBlob, theOffset, 4).then(function(hex) { + var match = /^ffe([0-9])/.exec(hex), segmentLength; + if (match) { + if (match[1] !== "1") { + segmentLength = parseInt(hex.slice(4, 8), 16); + seekToApp1(theOffset + segmentLength + 2, thePromise); + } else { + thePromise.success(theOffset); + } + } else { + thePromise.failure("No EXIF header to be found!"); + } + }); + return thePromise; + } + function getApp1Offset() { + var promise = new qq.Promise(); + qq.readBlobToHex(fileOrBlob, 0, 6).then(function(hex) { + if (hex.indexOf("ffd8") !== 0) { + promise.failure("Not a valid JPEG!"); + } else { + seekToApp1().then(function(offset) { + promise.success(offset); + }, function(error) { + promise.failure(error); + }); + } + }); + return promise; + } + function isLittleEndian(app1Start) { + var promise = new qq.Promise(); + qq.readBlobToHex(fileOrBlob, app1Start + 10, 2).then(function(hex) { + promise.success(hex === "4949"); + }); + return promise; + } + function getDirEntryCount(app1Start, littleEndian) { + var promise = new qq.Promise(); + qq.readBlobToHex(fileOrBlob, app1Start + 18, 2).then(function(hex) { + if (littleEndian) { + return promise.success(parseLittleEndian(hex)); + } else { + promise.success(parseInt(hex, 16)); + } + }); + return promise; + } + function getIfd(app1Start, dirEntries) { + var offset = app1Start + 20, bytes = dirEntries * 12; + return qq.readBlobToHex(fileOrBlob, offset, bytes); + } + function getDirEntries(ifdHex) { + var entries = [], offset = 0; + while (offset + 24 <= ifdHex.length) { + entries.push(ifdHex.slice(offset, offset + 24)); + offset += 24; + } + return entries; + } + function getTagValues(littleEndian, dirEntries) { + var TAG_VAL_OFFSET = 16, tagsToFind = qq.extend([], TAG_IDS), vals = {}; + qq.each(dirEntries, function(idx, entry) { + var idHex = entry.slice(0, 4), id = littleEndian ? parseLittleEndian(idHex) : parseInt(idHex, 16), tagsToFindIdx = tagsToFind.indexOf(id), tagValHex, tagName, tagValLength; + if (tagsToFindIdx >= 0) { + tagName = TAG_INFO[id].name; + tagValLength = TAG_INFO[id].bytes; + tagValHex = entry.slice(TAG_VAL_OFFSET, TAG_VAL_OFFSET + tagValLength * 2); + vals[tagName] = littleEndian ? parseLittleEndian(tagValHex) : parseInt(tagValHex, 16); + tagsToFind.splice(tagsToFindIdx, 1); + } + if (tagsToFind.length === 0) { + return false; + } + }); + return vals; + } + qq.extend(this, { + parse: function() { + var parser = new qq.Promise(), onParseFailure = function(message) { + log(qq.format("EXIF header parse failed: '{}' ", message)); + parser.failure(message); + }; + getApp1Offset().then(function(app1Offset) { + log(qq.format("Moving forward with EXIF header parsing for '{}'", fileOrBlob.name === undefined ? "blob" : fileOrBlob.name)); + isLittleEndian(app1Offset).then(function(littleEndian) { + log(qq.format("EXIF Byte order is {} endian", littleEndian ? "little" : "big")); + getDirEntryCount(app1Offset, littleEndian).then(function(dirEntryCount) { + log(qq.format("Found {} APP1 directory entries", dirEntryCount)); + getIfd(app1Offset, dirEntryCount).then(function(ifdHex) { + var dirEntries = getDirEntries(ifdHex), tagValues = getTagValues(littleEndian, dirEntries); + log("Successfully parsed some EXIF tags"); + parser.success(tagValues); + }, onParseFailure); + }, onParseFailure); + }, onParseFailure); + }, onParseFailure); + return parser; + } + }); + this._testing = {}; + this._testing.parseLittleEndian = parseLittleEndian; + }; + qq.Identify = function(fileOrBlob, log) { + "use strict"; + function isIdentifiable(magicBytes, questionableBytes) { + var identifiable = false, magicBytesEntries = [].concat(magicBytes); + qq.each(magicBytesEntries, function(idx, magicBytesArrayEntry) { + if (questionableBytes.indexOf(magicBytesArrayEntry) === 0) { + identifiable = true; + return false; + } + }); + return identifiable; + } + qq.extend(this, { + isPreviewable: function() { + var self = this, identifier = new qq.Promise(), previewable = false, name = fileOrBlob.name === undefined ? "blob" : fileOrBlob.name; + log(qq.format("Attempting to determine if {} can be rendered in this browser", name)); + log("First pass: check type attribute of blob object."); + if (this.isPreviewableSync()) { + log("Second pass: check for magic bytes in file header."); + qq.readBlobToHex(fileOrBlob, 0, 4).then(function(hex) { + qq.each(self.PREVIEWABLE_MIME_TYPES, function(mime, bytes) { + if (isIdentifiable(bytes, hex)) { + if (mime !== "image/tiff" || qq.supportedFeatures.tiffPreviews) { + previewable = true; + identifier.success(mime); + } + return false; + } + }); + log(qq.format("'{}' is {} able to be rendered in this browser", name, previewable ? "" : "NOT")); + if (!previewable) { + identifier.failure(); + } + }, function() { + log("Error reading file w/ name '" + name + "'. Not able to be rendered in this browser."); + identifier.failure(); + }); + } else { + identifier.failure(); + } + return identifier; + }, + isPreviewableSync: function() { + var fileMime = fileOrBlob.type, isRecognizedImage = qq.indexOf(Object.keys(this.PREVIEWABLE_MIME_TYPES), fileMime) >= 0, previewable = false, name = fileOrBlob.name === undefined ? "blob" : fileOrBlob.name; + if (isRecognizedImage) { + if (fileMime === "image/tiff") { + previewable = qq.supportedFeatures.tiffPreviews; + } else { + previewable = true; + } + } + !previewable && log(name + " is not previewable in this browser per the blob's type attr"); + return previewable; + } + }); + }; + qq.Identify.prototype.PREVIEWABLE_MIME_TYPES = { + "image/jpeg": "ffd8ff", + "image/gif": "474946", + "image/png": "89504e", + "image/bmp": "424d", + "image/tiff": [ "49492a00", "4d4d002a" ] + }; + qq.ImageValidation = function(blob, log) { + "use strict"; + function hasNonZeroLimits(limits) { + var atLeastOne = false; + qq.each(limits, function(limit, value) { + if (value > 0) { + atLeastOne = true; + return false; + } + }); + return atLeastOne; + } + function getWidthHeight() { + var sizeDetermination = new qq.Promise(); + new qq.Identify(blob, log).isPreviewable().then(function() { + var image = new Image(), url = window.URL && window.URL.createObjectURL ? window.URL : window.webkitURL && window.webkitURL.createObjectURL ? window.webkitURL : null; + if (url) { + image.onerror = function() { + log("Cannot determine dimensions for image. May be too large.", "error"); + sizeDetermination.failure(); + }; + image.onload = function() { + sizeDetermination.success({ + width: this.width, + height: this.height + }); + }; + image.src = url.createObjectURL(blob); + } else { + log("No createObjectURL function available to generate image URL!", "error"); + sizeDetermination.failure(); + } + }, sizeDetermination.failure); + return sizeDetermination; + } + function getFailingLimit(limits, dimensions) { + var failingLimit; + qq.each(limits, function(limitName, limitValue) { + if (limitValue > 0) { + var limitMatcher = /(max|min)(Width|Height)/.exec(limitName), dimensionPropName = limitMatcher[2].charAt(0).toLowerCase() + limitMatcher[2].slice(1), actualValue = dimensions[dimensionPropName]; + switch (limitMatcher[1]) { + case "min": + if (actualValue < limitValue) { + failingLimit = limitName; + return false; + } + break; + + case "max": + if (actualValue > limitValue) { + failingLimit = limitName; + return false; + } + break; + } + } + }); + return failingLimit; + } + this.validate = function(limits) { + var validationEffort = new qq.Promise(); + log("Attempting to validate image."); + if (hasNonZeroLimits(limits)) { + getWidthHeight().then(function(dimensions) { + var failingLimit = getFailingLimit(limits, dimensions); + if (failingLimit) { + validationEffort.failure(failingLimit); + } else { + validationEffort.success(); + } + }, validationEffort.success); + } else { + validationEffort.success(); + } + return validationEffort; + }; + }; + qq.Session = function(spec) { + "use strict"; + var options = { + endpoint: null, + params: {}, + customHeaders: {}, + cors: {}, + addFileRecord: function(sessionData) {}, + log: function(message, level) {} + }; + qq.extend(options, spec, true); + function isJsonResponseValid(response) { + if (qq.isArray(response)) { + return true; + } + options.log("Session response is not an array.", "error"); + } + function handleFileItems(fileItems, success, xhrOrXdr, promise) { + var someItemsIgnored = false; + success = success && isJsonResponseValid(fileItems); + if (success) { + qq.each(fileItems, function(idx, fileItem) { + if (fileItem.uuid == null) { + someItemsIgnored = true; + options.log(qq.format("Session response item {} did not include a valid UUID - ignoring.", idx), "error"); + } else if (fileItem.name == null) { + someItemsIgnored = true; + options.log(qq.format("Session response item {} did not include a valid name - ignoring.", idx), "error"); + } else { + try { + options.addFileRecord(fileItem); + return true; + } catch (err) { + someItemsIgnored = true; + options.log(err.message, "error"); + } + } + return false; + }); + } + promise[success && !someItemsIgnored ? "success" : "failure"](fileItems, xhrOrXdr); + } + this.refresh = function() { + var refreshEffort = new qq.Promise(), refreshCompleteCallback = function(response, success, xhrOrXdr) { + handleFileItems(response, success, xhrOrXdr, refreshEffort); + }, requesterOptions = qq.extend({}, options), requester = new qq.SessionAjaxRequester(qq.extend(requesterOptions, { + onComplete: refreshCompleteCallback + })); + requester.queryServer(); + return refreshEffort; + }; + }; + qq.SessionAjaxRequester = function(spec) { + "use strict"; + var requester, options = { + endpoint: null, + customHeaders: {}, + params: {}, + cors: { + expected: false, + sendCredentials: false + }, + onComplete: function(response, success, xhrOrXdr) {}, + log: function(str, level) {} + }; + qq.extend(options, spec); + function onComplete(id, xhrOrXdr, isError) { + var response = null; + if (xhrOrXdr.responseText != null) { + try { + response = qq.parseJson(xhrOrXdr.responseText); + } catch (err) { + options.log("Problem parsing session response: " + err.message, "error"); + isError = true; + } + } + options.onComplete(response, !isError, xhrOrXdr); + } + requester = qq.extend(this, new qq.AjaxRequester({ + acceptHeader: "application/json", + validMethods: [ "GET" ], + method: "GET", + endpointStore: { + get: function() { + return options.endpoint; + } + }, + customHeaders: options.customHeaders, + log: options.log, + onComplete: onComplete, + cors: options.cors + })); + qq.extend(this, { + queryServer: function() { + var params = qq.extend({}, options.params); + options.log("Session query request."); + requester.initTransport("sessionRefresh").withParams(params).withCacheBuster().send(); + } + }); + }; + qq.Scaler = function(spec, log) { + "use strict"; + var self = this, customResizeFunction = spec.customResizer, includeOriginal = spec.sendOriginal, orient = spec.orient, defaultType = spec.defaultType, defaultQuality = spec.defaultQuality / 100, failedToScaleText = spec.failureText, includeExif = spec.includeExif, sizes = this._getSortedSizes(spec.sizes); + qq.extend(this, { + enabled: qq.supportedFeatures.scaling && sizes.length > 0, + getFileRecords: function(originalFileUuid, originalFileName, originalBlobOrBlobData) { + var self = this, records = [], originalBlob = originalBlobOrBlobData.blob ? originalBlobOrBlobData.blob : originalBlobOrBlobData, identifier = new qq.Identify(originalBlob, log); + if (identifier.isPreviewableSync()) { + qq.each(sizes, function(idx, sizeRecord) { + var outputType = self._determineOutputType({ + defaultType: defaultType, + requestedType: sizeRecord.type, + refType: originalBlob.type + }); + records.push({ + uuid: qq.getUniqueId(), + name: self._getName(originalFileName, { + name: sizeRecord.name, + type: outputType, + refType: originalBlob.type + }), + blob: new qq.BlobProxy(originalBlob, qq.bind(self._generateScaledImage, self, { + customResizeFunction: customResizeFunction, + maxSize: sizeRecord.maxSize, + orient: orient, + type: outputType, + quality: defaultQuality, + failedText: failedToScaleText, + includeExif: includeExif, + log: log + })) + }); + }); + records.push({ + uuid: originalFileUuid, + name: originalFileName, + size: originalBlob.size, + blob: includeOriginal ? originalBlob : null + }); + } else { + records.push({ + uuid: originalFileUuid, + name: originalFileName, + size: originalBlob.size, + blob: originalBlob + }); + } + return records; + }, + handleNewFile: function(file, name, uuid, size, fileList, batchId, uuidParamName, api) { + var self = this, buttonId = file.qqButtonId || file.blob && file.blob.qqButtonId, scaledIds = [], originalId = null, addFileToHandler = api.addFileToHandler, uploadData = api.uploadData, paramsStore = api.paramsStore, proxyGroupId = qq.getUniqueId(); + qq.each(self.getFileRecords(uuid, name, file), function(idx, record) { + var blobSize = record.size, id; + if (record.blob instanceof qq.BlobProxy) { + blobSize = -1; + } + id = uploadData.addFile({ + uuid: record.uuid, + name: record.name, + size: blobSize, + batchId: batchId, + proxyGroupId: proxyGroupId + }); + if (record.blob instanceof qq.BlobProxy) { + scaledIds.push(id); + } else { + originalId = id; + } + if (record.blob) { + addFileToHandler(id, record.blob); + fileList.push({ + id: id, + file: record.blob + }); + } else { + uploadData.setStatus(id, qq.status.REJECTED); + } + }); + if (originalId !== null) { + qq.each(scaledIds, function(idx, scaledId) { + var params = { + qqparentuuid: uploadData.retrieve({ + id: originalId + }).uuid, + qqparentsize: uploadData.retrieve({ + id: originalId + }).size + }; + params[uuidParamName] = uploadData.retrieve({ + id: scaledId + }).uuid; + uploadData.setParentId(scaledId, originalId); + paramsStore.addReadOnly(scaledId, params); + }); + if (scaledIds.length) { + (function() { + var param = {}; + param[uuidParamName] = uploadData.retrieve({ + id: originalId + }).uuid; + paramsStore.addReadOnly(originalId, param); + })(); + } + } + } + }); + }; + qq.extend(qq.Scaler.prototype, { + scaleImage: function(id, specs, api) { + "use strict"; + if (!qq.supportedFeatures.scaling) { + throw new qq.Error("Scaling is not supported in this browser!"); + } + var scalingEffort = new qq.Promise(), log = api.log, file = api.getFile(id), uploadData = api.uploadData.retrieve({ + id: id + }), name = uploadData && uploadData.name, uuid = uploadData && uploadData.uuid, scalingOptions = { + customResizer: specs.customResizer, + sendOriginal: false, + orient: specs.orient, + defaultType: specs.type || null, + defaultQuality: specs.quality, + failedToScaleText: "Unable to scale", + sizes: [ { + name: "", + maxSize: specs.maxSize + } ] + }, scaler = new qq.Scaler(scalingOptions, log); + if (!qq.Scaler || !qq.supportedFeatures.imagePreviews || !file) { + scalingEffort.failure(); + log("Could not generate requested scaled image for " + id + ". " + "Scaling is either not possible in this browser, or the file could not be located.", "error"); + } else { + qq.bind(function() { + var record = scaler.getFileRecords(uuid, name, file)[0]; + if (record && record.blob instanceof qq.BlobProxy) { + record.blob.create().then(scalingEffort.success, scalingEffort.failure); + } else { + log(id + " is not a scalable image!", "error"); + scalingEffort.failure(); + } + }, this)(); + } + return scalingEffort; + }, + _determineOutputType: function(spec) { + "use strict"; + var requestedType = spec.requestedType, defaultType = spec.defaultType, referenceType = spec.refType; + if (!defaultType && !requestedType) { + if (referenceType !== "image/jpeg") { + return "image/png"; + } + return referenceType; + } + if (!requestedType) { + return defaultType; + } + if (qq.indexOf(Object.keys(qq.Identify.prototype.PREVIEWABLE_MIME_TYPES), requestedType) >= 0) { + if (requestedType === "image/tiff") { + return qq.supportedFeatures.tiffPreviews ? requestedType : defaultType; + } + return requestedType; + } + return defaultType; + }, + _getName: function(originalName, scaledVersionProperties) { + "use strict"; + var startOfExt = originalName.lastIndexOf("."), versionType = scaledVersionProperties.type || "image/png", referenceType = scaledVersionProperties.refType, scaledName = "", scaledExt = qq.getExtension(originalName), nameAppendage = ""; + if (scaledVersionProperties.name && scaledVersionProperties.name.trim().length) { + nameAppendage = " (" + scaledVersionProperties.name + ")"; + } + if (startOfExt >= 0) { + scaledName = originalName.substr(0, startOfExt); + if (referenceType !== versionType) { + scaledExt = versionType.split("/")[1]; + } + scaledName += nameAppendage + "." + scaledExt; + } else { + scaledName = originalName + nameAppendage; + } + return scaledName; + }, + _getSortedSizes: function(sizes) { + "use strict"; + sizes = qq.extend([], sizes); + return sizes.sort(function(a, b) { + if (a.maxSize > b.maxSize) { + return 1; + } + if (a.maxSize < b.maxSize) { + return -1; + } + return 0; + }); + }, + _generateScaledImage: function(spec, sourceFile) { + "use strict"; + var self = this, customResizeFunction = spec.customResizeFunction, log = spec.log, maxSize = spec.maxSize, orient = spec.orient, type = spec.type, quality = spec.quality, failedText = spec.failedText, includeExif = spec.includeExif && sourceFile.type === "image/jpeg" && type === "image/jpeg", scalingEffort = new qq.Promise(), imageGenerator = new qq.ImageGenerator(log), canvas = document.createElement("canvas"); + log("Attempting to generate scaled version for " + sourceFile.name); + imageGenerator.generate(sourceFile, canvas, { + maxSize: maxSize, + orient: orient, + customResizeFunction: customResizeFunction + }).then(function() { + var scaledImageDataUri = canvas.toDataURL(type, quality), signalSuccess = function() { + log("Success generating scaled version for " + sourceFile.name); + var blob = qq.dataUriToBlob(scaledImageDataUri); + scalingEffort.success(blob); + }; + if (includeExif) { + self._insertExifHeader(sourceFile, scaledImageDataUri, log).then(function(scaledImageDataUriWithExif) { + scaledImageDataUri = scaledImageDataUriWithExif; + signalSuccess(); + }, function() { + log("Problem inserting EXIF header into scaled image. Using scaled image w/out EXIF data.", "error"); + signalSuccess(); + }); + } else { + signalSuccess(); + } + }, function() { + log("Failed attempt to generate scaled version for " + sourceFile.name, "error"); + scalingEffort.failure(failedText); + }); + return scalingEffort; + }, + _insertExifHeader: function(originalImage, scaledImageDataUri, log) { + "use strict"; + var reader = new FileReader(), insertionEffort = new qq.Promise(), originalImageDataUri = ""; + reader.onload = function() { + originalImageDataUri = reader.result; + insertionEffort.success(qq.ExifRestorer.restore(originalImageDataUri, scaledImageDataUri)); + }; + reader.onerror = function() { + log("Problem reading " + originalImage.name + " during attempt to transfer EXIF data to scaled version.", "error"); + insertionEffort.failure(); + }; + reader.readAsDataURL(originalImage); + return insertionEffort; + }, + _dataUriToBlob: function(dataUri) { + "use strict"; + var byteString, mimeString, arrayBuffer, intArray; + if (dataUri.split(",")[0].indexOf("base64") >= 0) { + byteString = atob(dataUri.split(",")[1]); + } else { + byteString = decodeURI(dataUri.split(",")[1]); + } + mimeString = dataUri.split(",")[0].split(":")[1].split(";")[0]; + arrayBuffer = new ArrayBuffer(byteString.length); + intArray = new Uint8Array(arrayBuffer); + qq.each(byteString, function(idx, character) { + intArray[idx] = character.charCodeAt(0); + }); + return this._createBlob(arrayBuffer, mimeString); + }, + _createBlob: function(data, mime) { + "use strict"; + var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder, blobBuilder = BlobBuilder && new BlobBuilder(); + if (blobBuilder) { + blobBuilder.append(data); + return blobBuilder.getBlob(mime); + } else { + return new Blob([ data ], { + type: mime + }); + } + } + }); + qq.ExifRestorer = function() { + var ExifRestorer = {}; + ExifRestorer.KEY_STR = "ABCDEFGHIJKLMNOP" + "QRSTUVWXYZabcdef" + "ghijklmnopqrstuv" + "wxyz0123456789+/" + "="; + ExifRestorer.encode64 = function(input) { + var output = "", chr1, chr2, chr3 = "", enc1, enc2, enc3, enc4 = "", i = 0; + do { + chr1 = input[i++]; + chr2 = input[i++]; + chr3 = input[i++]; + enc1 = chr1 >> 2; + enc2 = (chr1 & 3) << 4 | chr2 >> 4; + enc3 = (chr2 & 15) << 2 | chr3 >> 6; + enc4 = chr3 & 63; + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + output = output + this.KEY_STR.charAt(enc1) + this.KEY_STR.charAt(enc2) + this.KEY_STR.charAt(enc3) + this.KEY_STR.charAt(enc4); + chr1 = chr2 = chr3 = ""; + enc1 = enc2 = enc3 = enc4 = ""; + } while (i < input.length); + return output; + }; + ExifRestorer.restore = function(origFileBase64, resizedFileBase64) { + var expectedBase64Header = "data:image/jpeg;base64,"; + if (!origFileBase64.match(expectedBase64Header)) { + return resizedFileBase64; + } + var rawImage = this.decode64(origFileBase64.replace(expectedBase64Header, "")); + var segments = this.slice2Segments(rawImage); + var image = this.exifManipulation(resizedFileBase64, segments); + return expectedBase64Header + this.encode64(image); + }; + ExifRestorer.exifManipulation = function(resizedFileBase64, segments) { + var exifArray = this.getExifArray(segments), newImageArray = this.insertExif(resizedFileBase64, exifArray), aBuffer = new Uint8Array(newImageArray); + return aBuffer; + }; + ExifRestorer.getExifArray = function(segments) { + var seg; + for (var x = 0; x < segments.length; x++) { + seg = segments[x]; + if (seg[0] == 255 & seg[1] == 225) { + return seg; + } + } + return []; + }; + ExifRestorer.insertExif = function(resizedFileBase64, exifArray) { + var imageData = resizedFileBase64.replace("data:image/jpeg;base64,", ""), buf = this.decode64(imageData), separatePoint = buf.indexOf(255, 3), mae = buf.slice(0, separatePoint), ato = buf.slice(separatePoint), array = mae; + array = array.concat(exifArray); + array = array.concat(ato); + return array; + }; + ExifRestorer.slice2Segments = function(rawImageArray) { + var head = 0, segments = []; + while (1) { + if (rawImageArray[head] == 255 & rawImageArray[head + 1] == 218) { + break; + } + if (rawImageArray[head] == 255 & rawImageArray[head + 1] == 216) { + head += 2; + } else { + var length = rawImageArray[head + 2] * 256 + rawImageArray[head + 3], endPoint = head + length + 2, seg = rawImageArray.slice(head, endPoint); + segments.push(seg); + head = endPoint; + } + if (head > rawImageArray.length) { + break; + } + } + return segments; + }; + ExifRestorer.decode64 = function(input) { + var output = "", chr1, chr2, chr3 = "", enc1, enc2, enc3, enc4 = "", i = 0, buf = []; + var base64test = /[^A-Za-z0-9\+\/\=]/g; + if (base64test.exec(input)) { + throw new Error("There were invalid base64 characters in the input text. " + "Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='"); + } + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + do { + enc1 = this.KEY_STR.indexOf(input.charAt(i++)); + enc2 = this.KEY_STR.indexOf(input.charAt(i++)); + enc3 = this.KEY_STR.indexOf(input.charAt(i++)); + enc4 = this.KEY_STR.indexOf(input.charAt(i++)); + chr1 = enc1 << 2 | enc2 >> 4; + chr2 = (enc2 & 15) << 4 | enc3 >> 2; + chr3 = (enc3 & 3) << 6 | enc4; + buf.push(chr1); + if (enc3 != 64) { + buf.push(chr2); + } + if (enc4 != 64) { + buf.push(chr3); + } + chr1 = chr2 = chr3 = ""; + enc1 = enc2 = enc3 = enc4 = ""; + } while (i < input.length); + return buf; + }; + return ExifRestorer; + }(); + qq.TotalProgress = function(callback, getSize) { + "use strict"; + var perFileProgress = {}, totalLoaded = 0, totalSize = 0, lastLoadedSent = -1, lastTotalSent = -1, callbackProxy = function(loaded, total) { + if (loaded !== lastLoadedSent || total !== lastTotalSent) { + callback(loaded, total); + } + lastLoadedSent = loaded; + lastTotalSent = total; + }, noRetryableFiles = function(failed, retryable) { + var none = true; + qq.each(failed, function(idx, failedId) { + if (qq.indexOf(retryable, failedId) >= 0) { + none = false; + return false; + } + }); + return none; + }, onCancel = function(id) { + updateTotalProgress(id, -1, -1); + delete perFileProgress[id]; + }, onAllComplete = function(successful, failed, retryable) { + if (failed.length === 0 || noRetryableFiles(failed, retryable)) { + callbackProxy(totalSize, totalSize); + this.reset(); + } + }, onNew = function(id) { + var size = getSize(id); + if (size > 0) { + updateTotalProgress(id, 0, size); + perFileProgress[id] = { + loaded: 0, + total: size + }; + } + }, updateTotalProgress = function(id, newLoaded, newTotal) { + var oldLoaded = perFileProgress[id] ? perFileProgress[id].loaded : 0, oldTotal = perFileProgress[id] ? perFileProgress[id].total : 0; + if (newLoaded === -1 && newTotal === -1) { + totalLoaded -= oldLoaded; + totalSize -= oldTotal; + } else { + if (newLoaded) { + totalLoaded += newLoaded - oldLoaded; + } + if (newTotal) { + totalSize += newTotal - oldTotal; + } + } + callbackProxy(totalLoaded, totalSize); + }; + qq.extend(this, { + onAllComplete: onAllComplete, + onStatusChange: function(id, oldStatus, newStatus) { + if (newStatus === qq.status.CANCELED || newStatus === qq.status.REJECTED) { + onCancel(id); + } else if (newStatus === qq.status.SUBMITTING) { + onNew(id); + } + }, + onIndividualProgress: function(id, loaded, total) { + updateTotalProgress(id, loaded, total); + perFileProgress[id] = { + loaded: loaded, + total: total + }; + }, + onNewSize: function(id) { + onNew(id); + }, + reset: function() { + perFileProgress = {}; + totalLoaded = 0; + totalSize = 0; + } + }); + }; + qq.PasteSupport = function(o) { + "use strict"; + var options, detachPasteHandler; + options = { + targetElement: null, + callbacks: { + log: function(message, level) {}, + pasteReceived: function(blob) {} + } + }; + function isImage(item) { + return item.type && item.type.indexOf("image/") === 0; + } + function registerPasteHandler() { + detachPasteHandler = qq(options.targetElement).attach("paste", function(event) { + var clipboardData = event.clipboardData; + if (clipboardData) { + qq.each(clipboardData.items, function(idx, item) { + if (isImage(item)) { + var blob = item.getAsFile(); + options.callbacks.pasteReceived(blob); + } + }); + } + }); + } + function unregisterPasteHandler() { + if (detachPasteHandler) { + detachPasteHandler(); + } + } + qq.extend(options, o); + registerPasteHandler(); + qq.extend(this, { + reset: function() { + unregisterPasteHandler(); + } + }); + }; + qq.FormSupport = function(options, startUpload, log) { + "use strict"; + var self = this, interceptSubmit = options.interceptSubmit, formEl = options.element, autoUpload = options.autoUpload; + qq.extend(this, { + newEndpoint: null, + newAutoUpload: autoUpload, + attachedToForm: false, + getFormInputsAsObject: function() { + if (formEl == null) { + return null; + } + return self._form2Obj(formEl); + } + }); + function determineNewEndpoint(formEl) { + if (formEl.getAttribute("action")) { + self.newEndpoint = formEl.getAttribute("action"); + } + } + function validateForm(formEl, nativeSubmit) { + if (formEl.checkValidity && !formEl.checkValidity()) { + log("Form did not pass validation checks - will not upload.", "error"); + nativeSubmit(); + } else { + return true; + } + } + function maybeUploadOnSubmit(formEl) { + var nativeSubmit = formEl.submit; + qq(formEl).attach("submit", function(event) { + event = event || window.event; + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + validateForm(formEl, nativeSubmit) && startUpload(); + }); + formEl.submit = function() { + validateForm(formEl, nativeSubmit) && startUpload(); + }; + } + function determineFormEl(formEl) { + if (formEl) { + if (qq.isString(formEl)) { + formEl = document.getElementById(formEl); + } + if (formEl) { + log("Attaching to form element."); + determineNewEndpoint(formEl); + interceptSubmit && maybeUploadOnSubmit(formEl); + } + } + return formEl; + } + formEl = determineFormEl(formEl); + this.attachedToForm = !!formEl; + }; + qq.extend(qq.FormSupport.prototype, { + _form2Obj: function(form) { + "use strict"; + var obj = {}, notIrrelevantType = function(type) { + var irrelevantTypes = [ "button", "image", "reset", "submit" ]; + return qq.indexOf(irrelevantTypes, type.toLowerCase()) < 0; + }, radioOrCheckbox = function(type) { + return qq.indexOf([ "checkbox", "radio" ], type.toLowerCase()) >= 0; + }, ignoreValue = function(el) { + if (radioOrCheckbox(el.type) && !el.checked) { + return true; + } + return el.disabled && el.type.toLowerCase() !== "hidden"; + }, selectValue = function(select) { + var value = null; + qq.each(qq(select).children(), function(idx, child) { + if (child.tagName.toLowerCase() === "option" && child.selected) { + value = child.value; + return false; + } + }); + return value; + }; + qq.each(form.elements, function(idx, el) { + if ((qq.isInput(el, true) || el.tagName.toLowerCase() === "textarea") && notIrrelevantType(el.type) && !ignoreValue(el)) { + obj[el.name] = el.value; + } else if (el.tagName.toLowerCase() === "select" && !ignoreValue(el)) { + var value = selectValue(el); + if (value !== null) { + obj[el.name] = value; + } + } + }); + return obj; + } + }); + qq.traditional = qq.traditional || {}; + qq.traditional.FormUploadHandler = function(options, proxy) { + "use strict"; + var handler = this, getName = proxy.getName, getUuid = proxy.getUuid, log = proxy.log; + function getIframeContentJson(id, iframe) { + var response, doc, innerHtml; + try { + doc = iframe.contentDocument || iframe.contentWindow.document; + innerHtml = doc.body.innerHTML; + log("converting iframe's innerHTML to JSON"); + log("innerHTML = " + innerHtml); + if (innerHtml && innerHtml.match(/^ 1 && !options.allowMultipleItems) {
+ options.callbacks.processingDroppedFilesComplete([]);
+ options.callbacks.dropError("tooManyFilesError", "");
+ uploadDropZone.dropDisabled(false);
+ handleDataTransferPromise.failure();
+ } else {
+ droppedFiles = [];
+ if (qq.isFolderDropSupported(dataTransfer)) {
+ qq.each(dataTransfer.items, function(idx, item) {
+ var entry = item.webkitGetAsEntry();
+ if (entry) {
+ if (entry.isFile) {
+ droppedFiles.push(item.getAsFile());
+ } else {
+ pendingFolderPromises.push(traverseFileTree(entry).done(function() {
+ pendingFolderPromises.pop();
+ if (pendingFolderPromises.length === 0) {
+ handleDataTransferPromise.success();
+ }
+ }));
+ }
+ }
+ });
+ } else {
+ droppedFiles = dataTransfer.files;
+ }
+ if (pendingFolderPromises.length === 0) {
+ handleDataTransferPromise.success();
+ }
+ }
+ return handleDataTransferPromise;
+ }
+ function setupDropzone(dropArea) {
+ var dropZone = new qq.UploadDropZone({
+ HIDE_ZONES_EVENT_NAME: HIDE_ZONES_EVENT_NAME,
+ element: dropArea,
+ onEnter: function(e) {
+ qq(dropArea).addClass(options.classes.dropActive);
+ e.stopPropagation();
+ },
+ onLeaveNotDescendants: function(e) {
+ qq(dropArea).removeClass(options.classes.dropActive);
+ },
+ onDrop: function(e) {
+ handleDataTransfer(e.dataTransfer, dropZone).then(function() {
+ uploadDroppedFiles(droppedFiles, dropZone);
+ }, function() {
+ options.callbacks.dropLog("Drop event DataTransfer parsing failed. No files will be uploaded.", "error");
+ });
+ }
+ });
+ disposeSupport.addDisposer(function() {
+ dropZone.dispose();
+ });
+ qq(dropArea).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropArea).hide();
+ uploadDropZones.push(dropZone);
+ return dropZone;
+ }
+ function isFileDrag(dragEvent) {
+ var fileDrag;
+ qq.each(dragEvent.dataTransfer.types, function(key, val) {
+ if (val === "Files") {
+ fileDrag = true;
+ return false;
+ }
+ });
+ return fileDrag;
+ }
+ function leavingDocumentOut(e) {
+ if (qq.safari()) {
+ return e.x < 0 || e.y < 0;
+ }
+ return e.x === 0 && e.y === 0;
+ }
+ function setupDragDrop() {
+ var dropZones = options.dropZoneElements, maybeHideDropZones = function() {
+ setTimeout(function() {
+ qq.each(dropZones, function(idx, dropZone) {
+ qq(dropZone).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropZone).hide();
+ qq(dropZone).removeClass(options.classes.dropActive);
+ });
+ }, 10);
+ };
+ qq.each(dropZones, function(idx, dropZone) {
+ var uploadDropZone = setupDropzone(dropZone);
+ if (dropZones.length && qq.supportedFeatures.fileDrop) {
+ disposeSupport.attach(document, "dragenter", function(e) {
+ if (!uploadDropZone.dropDisabled() && isFileDrag(e)) {
+ qq.each(dropZones, function(idx, dropZone) {
+ if (dropZone instanceof HTMLElement && qq(dropZone).hasAttribute(HIDE_BEFORE_ENTER_ATTR)) {
+ qq(dropZone).css({
+ display: "block"
+ });
+ }
+ });
+ }
+ });
+ }
+ });
+ disposeSupport.attach(document, "dragleave", function(e) {
+ if (leavingDocumentOut(e)) {
+ maybeHideDropZones();
+ }
+ });
+ disposeSupport.attach(qq(document).children()[0], "mouseenter", function(e) {
+ maybeHideDropZones();
+ });
+ disposeSupport.attach(document, "drop", function(e) {
+ if (isFileDrag(e)) {
+ e.preventDefault();
+ maybeHideDropZones();
+ }
+ });
+ disposeSupport.attach(document, HIDE_ZONES_EVENT_NAME, maybeHideDropZones);
+ }
+ setupDragDrop();
+ qq.extend(this, {
+ setupExtraDropzone: function(element) {
+ options.dropZoneElements.push(element);
+ setupDropzone(element);
+ },
+ removeDropzone: function(element) {
+ var i, dzs = options.dropZoneElements;
+ for (i in dzs) {
+ if (dzs[i] === element) {
+ return dzs.splice(i, 1);
+ }
+ }
+ },
+ dispose: function() {
+ disposeSupport.dispose();
+ qq.each(uploadDropZones, function(idx, dropZone) {
+ dropZone.dispose();
+ });
+ }
+ });
+ };
+ qq.DragAndDrop.callbacks = function() {
+ "use strict";
+ return {
+ processingDroppedFiles: function() {},
+ processingDroppedFilesComplete: function(files, targetEl) {},
+ dropError: function(code, errorSpecifics) {
+ qq.log("Drag & drop error code '" + code + " with these specifics: '" + errorSpecifics + "'", "error");
+ },
+ dropLog: function(message, level) {
+ qq.log(message, level);
+ }
+ };
+ };
+ qq.UploadDropZone = function(o) {
+ "use strict";
+ var disposeSupport = new qq.DisposeSupport(), options, element, preventDrop, dropOutsideDisabled;
+ options = {
+ element: null,
+ onEnter: function(e) {},
+ onLeave: function(e) {},
+ onLeaveNotDescendants: function(e) {},
+ onDrop: function(e) {}
+ };
+ qq.extend(options, o);
+ element = options.element;
+ function dragoverShouldBeCanceled() {
+ return qq.safari() || qq.firefox() && qq.windows();
+ }
+ function disableDropOutside(e) {
+ if (!dropOutsideDisabled) {
+ if (dragoverShouldBeCanceled) {
+ disposeSupport.attach(document, "dragover", function(e) {
+ e.preventDefault();
+ });
+ } else {
+ disposeSupport.attach(document, "dragover", function(e) {
+ if (e.dataTransfer) {
+ e.dataTransfer.dropEffect = "none";
+ e.preventDefault();
+ }
+ });
+ }
+ dropOutsideDisabled = true;
+ }
+ }
+ function isValidFileDrag(e) {
+ if (!qq.supportedFeatures.fileDrop) {
+ return false;
+ }
+ var effectTest, dt = e.dataTransfer, isSafari = qq.safari();
+ effectTest = qq.ie() && qq.supportedFeatures.fileDrop ? true : dt.effectAllowed !== "none";
+ return dt && effectTest && (dt.files && dt.files.length || !isSafari && dt.types.contains && dt.types.contains("Files") || dt.types.includes && dt.types.includes("Files"));
+ }
+ function isOrSetDropDisabled(isDisabled) {
+ if (isDisabled !== undefined) {
+ preventDrop = isDisabled;
+ }
+ return preventDrop;
+ }
+ function triggerHidezonesEvent() {
+ var hideZonesEvent;
+ function triggerUsingOldApi() {
+ hideZonesEvent = document.createEvent("Event");
+ hideZonesEvent.initEvent(options.HIDE_ZONES_EVENT_NAME, true, true);
+ }
+ if (window.CustomEvent) {
+ try {
+ hideZonesEvent = new CustomEvent(options.HIDE_ZONES_EVENT_NAME);
+ } catch (err) {
+ triggerUsingOldApi();
+ }
+ } else {
+ triggerUsingOldApi();
+ }
+ document.dispatchEvent(hideZonesEvent);
+ }
+ function attachEvents() {
+ disposeSupport.attach(element, "dragover", function(e) {
+ if (!isValidFileDrag(e)) {
+ return;
+ }
+ var effect = qq.ie() && qq.supportedFeatures.fileDrop ? null : e.dataTransfer.effectAllowed;
+ if (effect === "move" || effect === "linkMove") {
+ e.dataTransfer.dropEffect = "move";
+ } else {
+ e.dataTransfer.dropEffect = "copy";
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ });
+ disposeSupport.attach(element, "dragenter", function(e) {
+ if (!isOrSetDropDisabled()) {
+ if (!isValidFileDrag(e)) {
+ return;
+ }
+ options.onEnter(e);
+ }
+ });
+ disposeSupport.attach(element, "dragleave", function(e) {
+ if (!isValidFileDrag(e)) {
+ return;
+ }
+ options.onLeave(e);
+ var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
+ if (qq(this).contains(relatedTarget)) {
+ return;
+ }
+ options.onLeaveNotDescendants(e);
+ });
+ disposeSupport.attach(element, "drop", function(e) {
+ if (!isOrSetDropDisabled()) {
+ if (!isValidFileDrag(e)) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ options.onDrop(e);
+ triggerHidezonesEvent();
+ }
+ });
+ }
+ disableDropOutside();
+ attachEvents();
+ qq.extend(this, {
+ dropDisabled: function(isDisabled) {
+ return isOrSetDropDisabled(isDisabled);
+ },
+ dispose: function() {
+ disposeSupport.dispose();
+ },
+ getElement: function() {
+ return element;
+ }
+ });
+ this._testing = {};
+ this._testing.isValidFileDrag = isValidFileDrag;
+ };
+ (function() {
+ "use strict";
+ qq.uiPublicApi = {
+ addInitialFiles: function(cannedFileList) {
+ this._parent.prototype.addInitialFiles.apply(this, arguments);
+ this._templating.addCacheToDom();
+ },
+ clearStoredFiles: function() {
+ this._parent.prototype.clearStoredFiles.apply(this, arguments);
+ this._templating.clearFiles();
+ },
+ addExtraDropzone: function(element) {
+ this._dnd && this._dnd.setupExtraDropzone(element);
+ },
+ removeExtraDropzone: function(element) {
+ if (this._dnd) {
+ return this._dnd.removeDropzone(element);
+ }
+ },
+ getItemByFileId: function(id) {
+ if (!this._templating.isHiddenForever(id)) {
+ return this._templating.getFileContainer(id);
+ }
+ },
+ reset: function() {
+ this._parent.prototype.reset.apply(this, arguments);
+ this._templating.reset();
+ if (!this._options.button && this._templating.getButton()) {
+ this._defaultButtonId = this._createUploadButton({
+ element: this._templating.getButton(),
+ title: this._options.text.fileInputTitle
+ }).getButtonId();
+ }
+ if (this._dnd) {
+ this._dnd.dispose();
+ this._dnd = this._setupDragAndDrop();
+ }
+ this._totalFilesInBatch = 0;
+ this._filesInBatchAddedToUi = 0;
+ this._setupClickAndEditEventHandlers();
+ },
+ setName: function(id, newName) {
+ var formattedFilename = this._options.formatFileName(newName);
+ this._parent.prototype.setName.apply(this, arguments);
+ this._templating.updateFilename(id, formattedFilename);
+ },
+ pauseUpload: function(id) {
+ var paused = this._parent.prototype.pauseUpload.apply(this, arguments);
+ paused && this._templating.uploadPaused(id);
+ return paused;
+ },
+ continueUpload: function(id) {
+ var continued = this._parent.prototype.continueUpload.apply(this, arguments);
+ continued && this._templating.uploadContinued(id);
+ return continued;
+ },
+ getId: function(fileContainerOrChildEl) {
+ return this._templating.getFileId(fileContainerOrChildEl);
+ },
+ getDropTarget: function(fileId) {
+ var file = this.getFile(fileId);
+ return file.qqDropTarget;
+ }
+ };
+ qq.uiPrivateApi = {
+ _getButton: function(buttonId) {
+ var button = this._parent.prototype._getButton.apply(this, arguments);
+ if (!button) {
+ if (buttonId === this._defaultButtonId) {
+ button = this._templating.getButton();
+ }
+ }
+ return button;
+ },
+ _removeFileItem: function(fileId) {
+ this._templating.removeFile(fileId);
+ },
+ _setupClickAndEditEventHandlers: function() {
+ this._fileButtonsClickHandler = qq.FileButtonsClickHandler && this._bindFileButtonsClickEvent();
+ this._focusinEventSupported = !qq.firefox();
+ if (this._isEditFilenameEnabled()) {
+ this._filenameClickHandler = this._bindFilenameClickEvent();
+ this._filenameInputFocusInHandler = this._bindFilenameInputFocusInEvent();
+ this._filenameInputFocusHandler = this._bindFilenameInputFocusEvent();
+ }
+ },
+ _setupDragAndDrop: function() {
+ var self = this, dropZoneElements = this._options.dragAndDrop.extraDropzones, templating = this._templating, defaultDropZone = templating.getDropZone();
+ defaultDropZone && dropZoneElements.push(defaultDropZone);
+ return new qq.DragAndDrop({
+ dropZoneElements: dropZoneElements,
+ allowMultipleItems: this._options.multiple,
+ classes: {
+ dropActive: this._options.classes.dropActive
+ },
+ callbacks: {
+ processingDroppedFiles: function() {
+ templating.showDropProcessing();
+ },
+ processingDroppedFilesComplete: function(files, targetEl) {
+ templating.hideDropProcessing();
+ qq.each(files, function(idx, file) {
+ file.qqDropTarget = targetEl;
+ });
+ if (files.length) {
+ self.addFiles(files, null, null);
+ }
+ },
+ dropError: function(code, errorData) {
+ self._itemError(code, errorData);
+ },
+ dropLog: function(message, level) {
+ self.log(message, level);
+ }
+ }
+ });
+ },
+ _bindFileButtonsClickEvent: function() {
+ var self = this;
+ return new qq.FileButtonsClickHandler({
+ templating: this._templating,
+ log: function(message, lvl) {
+ self.log(message, lvl);
+ },
+ onDeleteFile: function(fileId) {
+ self.deleteFile(fileId);
+ },
+ onCancel: function(fileId) {
+ self.cancel(fileId);
+ },
+ onRetry: function(fileId) {
+ self.retry(fileId);
+ },
+ onPause: function(fileId) {
+ self.pauseUpload(fileId);
+ },
+ onContinue: function(fileId) {
+ self.continueUpload(fileId);
+ },
+ onGetName: function(fileId) {
+ return self.getName(fileId);
+ }
+ });
+ },
+ _isEditFilenameEnabled: function() {
+ return this._templating.isEditFilenamePossible() && !this._options.autoUpload && qq.FilenameClickHandler && qq.FilenameInputFocusHandler && qq.FilenameInputFocusHandler;
+ },
+ _filenameEditHandler: function() {
+ var self = this, templating = this._templating;
+ return {
+ templating: templating,
+ log: function(message, lvl) {
+ self.log(message, lvl);
+ },
+ onGetUploadStatus: function(fileId) {
+ return self.getUploads({
+ id: fileId
+ }).status;
+ },
+ onGetName: function(fileId) {
+ return self.getName(fileId);
+ },
+ onSetName: function(id, newName) {
+ self.setName(id, newName);
+ },
+ onEditingStatusChange: function(id, isEditing) {
+ var qqInput = qq(templating.getEditInput(id)), qqFileContainer = qq(templating.getFileContainer(id));
+ if (isEditing) {
+ qqInput.addClass("qq-editing");
+ templating.hideFilename(id);
+ templating.hideEditIcon(id);
+ } else {
+ qqInput.removeClass("qq-editing");
+ templating.showFilename(id);
+ templating.showEditIcon(id);
+ }
+ qqFileContainer.addClass("qq-temp").removeClass("qq-temp");
+ }
+ };
+ },
+ _onUploadStatusChange: function(id, oldStatus, newStatus) {
+ this._parent.prototype._onUploadStatusChange.apply(this, arguments);
+ if (this._isEditFilenameEnabled()) {
+ if (this._templating.getFileContainer(id) && newStatus !== qq.status.SUBMITTED) {
+ this._templating.markFilenameEditable(id);
+ this._templating.hideEditIcon(id);
+ }
+ }
+ if (oldStatus === qq.status.UPLOAD_RETRYING && newStatus === qq.status.UPLOADING) {
+ this._templating.hideRetry(id);
+ this._templating.setStatusText(id);
+ qq(this._templating.getFileContainer(id)).removeClass(this._classes.retrying);
+ } else if (newStatus === qq.status.UPLOAD_FAILED) {
+ this._templating.hidePause(id);
+ }
+ },
+ _bindFilenameInputFocusInEvent: function() {
+ var spec = qq.extend({}, this._filenameEditHandler());
+ return new qq.FilenameInputFocusInHandler(spec);
+ },
+ _bindFilenameInputFocusEvent: function() {
+ var spec = qq.extend({}, this._filenameEditHandler());
+ return new qq.FilenameInputFocusHandler(spec);
+ },
+ _bindFilenameClickEvent: function() {
+ var spec = qq.extend({}, this._filenameEditHandler());
+ return new qq.FilenameClickHandler(spec);
+ },
+ _storeForLater: function(id) {
+ this._parent.prototype._storeForLater.apply(this, arguments);
+ this._templating.hideSpinner(id);
+ },
+ _onAllComplete: function(successful, failed) {
+ this._parent.prototype._onAllComplete.apply(this, arguments);
+ this._templating.resetTotalProgress();
+ },
+ _onSubmit: function(id, name) {
+ var file = this.getFile(id);
+ if (file && file.qqPath && this._options.dragAndDrop.reportDirectoryPaths) {
+ this._paramsStore.addReadOnly(id, {
+ qqpath: file.qqPath
+ });
+ }
+ this._parent.prototype._onSubmit.apply(this, arguments);
+ this._addToList(id, name);
+ },
+ _onSubmitted: function(id) {
+ if (this._isEditFilenameEnabled()) {
+ this._templating.markFilenameEditable(id);
+ this._templating.showEditIcon(id);
+ if (!this._focusinEventSupported) {
+ this._filenameInputFocusHandler.addHandler(this._templating.getEditInput(id));
+ }
+ }
+ },
+ _onProgress: function(id, name, loaded, total) {
+ this._parent.prototype._onProgress.apply(this, arguments);
+ this._templating.updateProgress(id, loaded, total);
+ if (total === 0 || Math.round(loaded / total * 100) === 100) {
+ this._templating.hideCancel(id);
+ this._templating.hidePause(id);
+ this._templating.hideProgress(id);
+ this._templating.setStatusText(id, this._options.text.waitingForResponse);
+ this._displayFileSize(id);
+ } else {
+ this._displayFileSize(id, loaded, total);
+ }
+ },
+ _onTotalProgress: function(loaded, total) {
+ this._parent.prototype._onTotalProgress.apply(this, arguments);
+ this._templating.updateTotalProgress(loaded, total);
+ },
+ _onComplete: function(id, name, result, xhr) {
+ var parentRetVal = this._parent.prototype._onComplete.apply(this, arguments), templating = this._templating, fileContainer = templating.getFileContainer(id), self = this;
+ function completeUpload(result) {
+ if (!fileContainer) {
+ return;
+ }
+ templating.setStatusText(id);
+ qq(fileContainer).removeClass(self._classes.retrying);
+ templating.hideProgress(id);
+ if (self.getUploads({
+ id: id
+ }).status !== qq.status.UPLOAD_FAILED) {
+ templating.hideCancel(id);
+ }
+ templating.hideSpinner(id);
+ if (result.success) {
+ self._markFileAsSuccessful(id);
+ } else {
+ qq(fileContainer).addClass(self._classes.fail);
+ templating.showCancel(id);
+ if (templating.isRetryPossible() && !self._preventRetries[id]) {
+ qq(fileContainer).addClass(self._classes.retryable);
+ templating.showRetry(id);
+ }
+ self._controlFailureTextDisplay(id, result);
+ }
+ }
+ if (parentRetVal instanceof qq.Promise) {
+ parentRetVal.done(function(newResult) {
+ completeUpload(newResult);
+ });
+ } else {
+ completeUpload(result);
+ }
+ return parentRetVal;
+ },
+ _markFileAsSuccessful: function(id) {
+ var templating = this._templating;
+ if (this._isDeletePossible()) {
+ templating.showDeleteButton(id);
+ }
+ qq(templating.getFileContainer(id)).addClass(this._classes.success);
+ this._maybeUpdateThumbnail(id);
+ },
+ _onUploadPrep: function(id) {
+ this._parent.prototype._onUploadPrep.apply(this, arguments);
+ this._templating.showSpinner(id);
+ },
+ _onUpload: function(id, name) {
+ var parentRetVal = this._parent.prototype._onUpload.apply(this, arguments);
+ this._templating.showSpinner(id);
+ return parentRetVal;
+ },
+ _onUploadChunk: function(id, chunkData) {
+ this._parent.prototype._onUploadChunk.apply(this, arguments);
+ if (chunkData.partIndex > 0 && this._handler.isResumable(id)) {
+ this._templating.allowPause(id);
+ }
+ },
+ _onCancel: function(id, name) {
+ this._parent.prototype._onCancel.apply(this, arguments);
+ this._removeFileItem(id);
+ if (this._getNotFinished() === 0) {
+ this._templating.resetTotalProgress();
+ }
+ },
+ _onBeforeAutoRetry: function(id) {
+ var retryNumForDisplay, maxAuto, retryNote;
+ this._parent.prototype._onBeforeAutoRetry.apply(this, arguments);
+ this._showCancelLink(id);
+ if (this._options.retry.showAutoRetryNote) {
+ retryNumForDisplay = this._autoRetries[id];
+ maxAuto = this._options.retry.maxAutoAttempts;
+ retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
+ retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
+ this._templating.setStatusText(id, retryNote);
+ qq(this._templating.getFileContainer(id)).addClass(this._classes.retrying);
+ }
+ },
+ _onBeforeManualRetry: function(id) {
+ if (this._parent.prototype._onBeforeManualRetry.apply(this, arguments)) {
+ this._templating.resetProgress(id);
+ qq(this._templating.getFileContainer(id)).removeClass(this._classes.fail);
+ this._templating.setStatusText(id);
+ this._templating.showSpinner(id);
+ this._showCancelLink(id);
+ return true;
+ } else {
+ qq(this._templating.getFileContainer(id)).addClass(this._classes.retryable);
+ this._templating.showRetry(id);
+ return false;
+ }
+ },
+ _onSubmitDelete: function(id) {
+ var onSuccessCallback = qq.bind(this._onSubmitDeleteSuccess, this);
+ this._parent.prototype._onSubmitDelete.call(this, id, onSuccessCallback);
+ },
+ _onSubmitDeleteSuccess: function(id, uuid, additionalMandatedParams) {
+ if (this._options.deleteFile.forceConfirm) {
+ this._showDeleteConfirm.apply(this, arguments);
+ } else {
+ this._sendDeleteRequest.apply(this, arguments);
+ }
+ },
+ _onDeleteComplete: function(id, xhr, isError) {
+ this._parent.prototype._onDeleteComplete.apply(this, arguments);
+ this._templating.hideSpinner(id);
+ if (isError) {
+ this._templating.setStatusText(id, this._options.deleteFile.deletingFailedText);
+ this._templating.showDeleteButton(id);
+ } else {
+ this._removeFileItem(id);
+ }
+ },
+ _sendDeleteRequest: function(id, uuid, additionalMandatedParams) {
+ this._templating.hideDeleteButton(id);
+ this._templating.showSpinner(id);
+ this._templating.setStatusText(id, this._options.deleteFile.deletingStatusText);
+ this._deleteHandler.sendDelete.apply(this, arguments);
+ },
+ _showDeleteConfirm: function(id, uuid, mandatedParams) {
+ var fileName = this.getName(id), confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName), uuid = this.getUuid(id), deleteRequestArgs = arguments, self = this, retVal;
+ retVal = this._options.showConfirm(confirmMessage);
+ if (qq.isGenericPromise(retVal)) {
+ retVal.then(function() {
+ self._sendDeleteRequest.apply(self, deleteRequestArgs);
+ });
+ } else if (retVal !== false) {
+ self._sendDeleteRequest.apply(self, deleteRequestArgs);
+ }
+ },
+ _addToList: function(id, name, canned) {
+ var prependData, prependIndex = 0, dontDisplay = this._handler.isProxied(id) && this._options.scaling.hideScaled, record;
+ if (this._options.display.prependFiles) {
+ if (this._totalFilesInBatch > 1 && this._filesInBatchAddedToUi > 0) {
+ prependIndex = this._filesInBatchAddedToUi - 1;
+ }
+ prependData = {
+ index: prependIndex
+ };
+ }
+ if (!canned) {
+ if (this._options.disableCancelForFormUploads && !qq.supportedFeatures.ajaxUploading) {
+ this._templating.disableCancel();
+ }
+ if (!this._options.multiple) {
+ record = this.getUploads({
+ id: id
+ });
+ this._handledProxyGroup = this._handledProxyGroup || record.proxyGroupId;
+ if (record.proxyGroupId !== this._handledProxyGroup || !record.proxyGroupId) {
+ this._handler.cancelAll();
+ this._clearList();
+ this._handledProxyGroup = null;
+ }
+ }
+ }
+ if (canned) {
+ this._templating.addFileToCache(id, this._options.formatFileName(name), prependData, dontDisplay);
+ this._templating.updateThumbnail(id, this._thumbnailUrls[id], true, this._options.thumbnails.customResizer);
+ } else {
+ this._templating.addFile(id, this._options.formatFileName(name), prependData, dontDisplay);
+ this._templating.generatePreview(id, this.getFile(id), this._options.thumbnails.customResizer);
+ }
+ this._filesInBatchAddedToUi += 1;
+ if (canned || this._options.display.fileSizeOnSubmit && qq.supportedFeatures.ajaxUploading) {
+ this._displayFileSize(id);
+ }
+ },
+ _clearList: function() {
+ this._templating.clearFiles();
+ this.clearStoredFiles();
+ },
+ _displayFileSize: function(id, loadedSize, totalSize) {
+ var size = this.getSize(id), sizeForDisplay = this._formatSize(size);
+ if (size >= 0) {
+ if (loadedSize !== undefined && totalSize !== undefined) {
+ sizeForDisplay = this._formatProgress(loadedSize, totalSize);
+ }
+ this._templating.updateSize(id, sizeForDisplay);
+ }
+ },
+ _formatProgress: function(uploadedSize, totalSize) {
+ var message = this._options.text.formatProgress;
+ function r(name, replacement) {
+ message = message.replace(name, replacement);
+ }
+ r("{percent}", Math.round(uploadedSize / totalSize * 100));
+ r("{total_size}", this._formatSize(totalSize));
+ return message;
+ },
+ _controlFailureTextDisplay: function(id, response) {
+ var mode, responseProperty, failureReason;
+ mode = this._options.failedUploadTextDisplay.mode;
+ responseProperty = this._options.failedUploadTextDisplay.responseProperty;
+ if (mode === "custom") {
+ failureReason = response[responseProperty];
+ if (!failureReason) {
+ failureReason = this._options.text.failUpload;
+ }
+ this._templating.setStatusText(id, failureReason);
+ if (this._options.failedUploadTextDisplay.enableTooltip) {
+ this._showTooltip(id, failureReason);
+ }
+ } else if (mode === "default") {
+ this._templating.setStatusText(id, this._options.text.failUpload);
+ } else if (mode !== "none") {
+ this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", "warn");
+ }
+ },
+ _showTooltip: function(id, text) {
+ this._templating.getFileContainer(id).title = text;
+ },
+ _showCancelLink: function(id) {
+ if (!this._options.disableCancelForFormUploads || qq.supportedFeatures.ajaxUploading) {
+ this._templating.showCancel(id);
+ }
+ },
+ _itemError: function(code, name, item) {
+ var message = this._parent.prototype._itemError.apply(this, arguments);
+ this._options.showMessage(message);
+ },
+ _batchError: function(message) {
+ this._parent.prototype._batchError.apply(this, arguments);
+ this._options.showMessage(message);
+ },
+ _setupPastePrompt: function() {
+ var self = this;
+ this._options.callbacks.onPasteReceived = function() {
+ var message = self._options.paste.namePromptMessage, defaultVal = self._options.paste.defaultName;
+ return self._options.showPrompt(message, defaultVal);
+ };
+ },
+ _fileOrBlobRejected: function(id, name) {
+ this._totalFilesInBatch -= 1;
+ this._parent.prototype._fileOrBlobRejected.apply(this, arguments);
+ },
+ _prepareItemsForUpload: function(items, params, endpoint) {
+ this._totalFilesInBatch = items.length;
+ this._filesInBatchAddedToUi = 0;
+ this._parent.prototype._prepareItemsForUpload.apply(this, arguments);
+ },
+ _maybeUpdateThumbnail: function(fileId) {
+ var thumbnailUrl = this._thumbnailUrls[fileId], fileStatus = this.getUploads({
+ id: fileId
+ }).status;
+ if (fileStatus !== qq.status.DELETED && (thumbnailUrl || this._options.thumbnails.placeholders.waitUntilResponse || !qq.supportedFeatures.imagePreviews)) {
+ this._templating.updateThumbnail(fileId, thumbnailUrl, this._options.thumbnails.customResizer);
+ }
+ },
+ _addCannedFile: function(sessionData) {
+ var id = this._parent.prototype._addCannedFile.apply(this, arguments);
+ this._addToList(id, this.getName(id), true);
+ this._templating.hideSpinner(id);
+ this._templating.hideCancel(id);
+ this._markFileAsSuccessful(id);
+ return id;
+ },
+ _setSize: function(id, newSize) {
+ this._parent.prototype._setSize.apply(this, arguments);
+ this._templating.updateSize(id, this._formatSize(newSize));
+ },
+ _sessionRequestComplete: function() {
+ this._templating.addCacheToDom();
+ this._parent.prototype._sessionRequestComplete.apply(this, arguments);
+ }
+ };
+ })();
+ qq.FineUploader = function(o, namespace) {
+ "use strict";
+ var self = this;
+ this._parent = namespace ? qq[namespace].FineUploaderBasic : qq.FineUploaderBasic;
+ this._parent.apply(this, arguments);
+ qq.extend(this._options, {
+ element: null,
+ button: null,
+ listElement: null,
+ dragAndDrop: {
+ extraDropzones: [],
+ reportDirectoryPaths: false
+ },
+ text: {
+ formatProgress: "{percent}% of {total_size}",
+ failUpload: "Upload failed",
+ waitingForResponse: "Processing...",
+ paused: "Paused"
+ },
+ template: "qq-template",
+ classes: {
+ retrying: "qq-upload-retrying",
+ retryable: "qq-upload-retryable",
+ success: "qq-upload-success",
+ fail: "qq-upload-fail",
+ editable: "qq-editable",
+ hide: "qq-hide",
+ dropActive: "qq-upload-drop-area-active"
+ },
+ failedUploadTextDisplay: {
+ mode: "default",
+ responseProperty: "error",
+ enableTooltip: true
+ },
+ messages: {
+ tooManyFilesError: "You may only drop one file",
+ unsupportedBrowser: "Unrecoverable error - this browser does not permit file uploading of any kind."
+ },
+ retry: {
+ showAutoRetryNote: true,
+ autoRetryNote: "Retrying {retryNum}/{maxAuto}..."
+ },
+ deleteFile: {
+ forceConfirm: false,
+ confirmMessage: "Are you sure you want to delete {filename}?",
+ deletingStatusText: "Deleting...",
+ deletingFailedText: "Delete failed"
+ },
+ display: {
+ fileSizeOnSubmit: false,
+ prependFiles: false
+ },
+ paste: {
+ promptForName: false,
+ namePromptMessage: "Please name this image"
+ },
+ thumbnails: {
+ customResizer: null,
+ maxCount: 0,
+ placeholders: {
+ waitUntilResponse: false,
+ notAvailablePath: null,
+ waitingPath: null
+ },
+ timeBetweenThumbs: 750
+ },
+ scaling: {
+ hideScaled: false
+ },
+ showMessage: function(message) {
+ if (self._templating.hasDialog("alert")) {
+ return self._templating.showDialog("alert", message);
+ } else {
+ setTimeout(function() {
+ window.alert(message);
+ }, 0);
+ }
+ },
+ showConfirm: function(message) {
+ if (self._templating.hasDialog("confirm")) {
+ return self._templating.showDialog("confirm", message);
+ } else {
+ return window.confirm(message);
+ }
+ },
+ showPrompt: function(message, defaultValue) {
+ if (self._templating.hasDialog("prompt")) {
+ return self._templating.showDialog("prompt", message, defaultValue);
+ } else {
+ return window.prompt(message, defaultValue);
+ }
+ }
+ }, true);
+ qq.extend(this._options, o, true);
+ this._templating = new qq.Templating({
+ log: qq.bind(this.log, this),
+ templateIdOrEl: this._options.template,
+ containerEl: this._options.element,
+ fileContainerEl: this._options.listElement,
+ button: this._options.button,
+ imageGenerator: this._imageGenerator,
+ classes: {
+ hide: this._options.classes.hide,
+ editable: this._options.classes.editable
+ },
+ limits: {
+ maxThumbs: this._options.thumbnails.maxCount,
+ timeBetweenThumbs: this._options.thumbnails.timeBetweenThumbs
+ },
+ placeholders: {
+ waitUntilUpdate: this._options.thumbnails.placeholders.waitUntilResponse,
+ thumbnailNotAvailable: this._options.thumbnails.placeholders.notAvailablePath,
+ waitingForThumbnail: this._options.thumbnails.placeholders.waitingPath
+ },
+ text: this._options.text
+ });
+ if (this._options.workarounds.ios8SafariUploads && qq.ios800() && qq.iosSafari()) {
+ this._templating.renderFailure(this._options.messages.unsupportedBrowserIos8Safari);
+ } else if (!qq.supportedFeatures.uploading || this._options.cors.expected && !qq.supportedFeatures.uploadCors) {
+ this._templating.renderFailure(this._options.messages.unsupportedBrowser);
+ } else {
+ this._wrapCallbacks();
+ this._templating.render();
+ this._classes = this._options.classes;
+ if (!this._options.button && this._templating.getButton()) {
+ this._defaultButtonId = this._createUploadButton({
+ element: this._templating.getButton(),
+ title: this._options.text.fileInputTitle
+ }).getButtonId();
+ }
+ this._setupClickAndEditEventHandlers();
+ if (qq.DragAndDrop && qq.supportedFeatures.fileDrop) {
+ this._dnd = this._setupDragAndDrop();
+ }
+ if (this._options.paste.targetElement && this._options.paste.promptForName) {
+ if (qq.PasteSupport) {
+ this._setupPastePrompt();
+ } else {
+ this.log("Paste support module not found.", "error");
+ }
+ }
+ this._totalFilesInBatch = 0;
+ this._filesInBatchAddedToUi = 0;
+ }
+ };
+ qq.extend(qq.FineUploader.prototype, qq.basePublicApi);
+ qq.extend(qq.FineUploader.prototype, qq.basePrivateApi);
+ qq.extend(qq.FineUploader.prototype, qq.uiPublicApi);
+ qq.extend(qq.FineUploader.prototype, qq.uiPrivateApi);
+ qq.Templating = function(spec) {
+ "use strict";
+ var FILE_ID_ATTR = "qq-file-id", FILE_CLASS_PREFIX = "qq-file-id-", THUMBNAIL_MAX_SIZE_ATTR = "qq-max-size", THUMBNAIL_SERVER_SCALE_ATTR = "qq-server-scale", HIDE_DROPZONE_ATTR = "qq-hide-dropzone", DROPZPONE_TEXT_ATTR = "qq-drop-area-text", IN_PROGRESS_CLASS = "qq-in-progress", HIDDEN_FOREVER_CLASS = "qq-hidden-forever", fileBatch = {
+ content: document.createDocumentFragment(),
+ map: {}
+ }, isCancelDisabled = false, generatedThumbnails = 0, thumbnailQueueMonitorRunning = false, thumbGenerationQueue = [], thumbnailMaxSize = -1, options = {
+ log: null,
+ limits: {
+ maxThumbs: 0,
+ timeBetweenThumbs: 750
+ },
+ templateIdOrEl: "qq-template",
+ containerEl: null,
+ fileContainerEl: null,
+ button: null,
+ imageGenerator: null,
+ classes: {
+ hide: "qq-hide",
+ editable: "qq-editable"
+ },
+ placeholders: {
+ waitUntilUpdate: false,
+ thumbnailNotAvailable: null,
+ waitingForThumbnail: null
+ },
+ text: {
+ paused: "Paused"
+ }
+ }, selectorClasses = {
+ button: "qq-upload-button-selector",
+ alertDialog: "qq-alert-dialog-selector",
+ dialogCancelButton: "qq-cancel-button-selector",
+ confirmDialog: "qq-confirm-dialog-selector",
+ dialogMessage: "qq-dialog-message-selector",
+ dialogOkButton: "qq-ok-button-selector",
+ promptDialog: "qq-prompt-dialog-selector",
+ uploader: "qq-uploader-selector",
+ drop: "qq-upload-drop-area-selector",
+ list: "qq-upload-list-selector",
+ progressBarContainer: "qq-progress-bar-container-selector",
+ progressBar: "qq-progress-bar-selector",
+ totalProgressBarContainer: "qq-total-progress-bar-container-selector",
+ totalProgressBar: "qq-total-progress-bar-selector",
+ file: "qq-upload-file-selector",
+ spinner: "qq-upload-spinner-selector",
+ size: "qq-upload-size-selector",
+ cancel: "qq-upload-cancel-selector",
+ pause: "qq-upload-pause-selector",
+ continueButton: "qq-upload-continue-selector",
+ deleteButton: "qq-upload-delete-selector",
+ retry: "qq-upload-retry-selector",
+ statusText: "qq-upload-status-text-selector",
+ editFilenameInput: "qq-edit-filename-selector",
+ editNameIcon: "qq-edit-filename-icon-selector",
+ dropText: "qq-upload-drop-area-text-selector",
+ dropProcessing: "qq-drop-processing-selector",
+ dropProcessingSpinner: "qq-drop-processing-spinner-selector",
+ thumbnail: "qq-thumbnail-selector"
+ }, previewGeneration = {}, cachedThumbnailNotAvailableImg = new qq.Promise(), cachedWaitingForThumbnailImg = new qq.Promise(), log, isEditElementsExist, isRetryElementExist, templateDom, container, fileList, showThumbnails, serverScale, cacheThumbnailPlaceholders = function() {
+ var notAvailableUrl = options.placeholders.thumbnailNotAvailable, waitingUrl = options.placeholders.waitingForThumbnail, spec = {
+ maxSize: thumbnailMaxSize,
+ scale: serverScale
+ };
+ if (showThumbnails) {
+ if (notAvailableUrl) {
+ options.imageGenerator.generate(notAvailableUrl, new Image(), spec).then(function(updatedImg) {
+ cachedThumbnailNotAvailableImg.success(updatedImg);
+ }, function() {
+ cachedThumbnailNotAvailableImg.failure();
+ log("Problem loading 'not available' placeholder image at " + notAvailableUrl, "error");
+ });
+ } else {
+ cachedThumbnailNotAvailableImg.failure();
+ }
+ if (waitingUrl) {
+ options.imageGenerator.generate(waitingUrl, new Image(), spec).then(function(updatedImg) {
+ cachedWaitingForThumbnailImg.success(updatedImg);
+ }, function() {
+ cachedWaitingForThumbnailImg.failure();
+ log("Problem loading 'waiting for thumbnail' placeholder image at " + waitingUrl, "error");
+ });
+ } else {
+ cachedWaitingForThumbnailImg.failure();
+ }
+ }
+ }, displayWaitingImg = function(thumbnail) {
+ var waitingImgPlacement = new qq.Promise();
+ cachedWaitingForThumbnailImg.then(function(img) {
+ maybeScalePlaceholderViaCss(img, thumbnail);
+ if (!thumbnail.src) {
+ thumbnail.src = img.src;
+ thumbnail.onload = function() {
+ thumbnail.onload = null;
+ show(thumbnail);
+ waitingImgPlacement.success();
+ };
+ } else {
+ waitingImgPlacement.success();
+ }
+ }, function() {
+ hide(thumbnail);
+ waitingImgPlacement.success();
+ });
+ return waitingImgPlacement;
+ }, generateNewPreview = function(id, blob, spec) {
+ var thumbnail = getThumbnail(id);
+ log("Generating new thumbnail for " + id);
+ blob.qqThumbnailId = id;
+ return options.imageGenerator.generate(blob, thumbnail, spec).then(function() {
+ generatedThumbnails++;
+ show(thumbnail);
+ previewGeneration[id].success();
+ }, function() {
+ previewGeneration[id].failure();
+ if (!options.placeholders.waitUntilUpdate) {
+ maybeSetDisplayNotAvailableImg(id, thumbnail);
+ }
+ });
+ }, generateNextQueuedPreview = function() {
+ if (thumbGenerationQueue.length) {
+ thumbnailQueueMonitorRunning = true;
+ var queuedThumbRequest = thumbGenerationQueue.shift();
+ if (queuedThumbRequest.update) {
+ processUpdateQueuedPreviewRequest(queuedThumbRequest);
+ } else {
+ processNewQueuedPreviewRequest(queuedThumbRequest);
+ }
+ } else {
+ thumbnailQueueMonitorRunning = false;
+ }
+ }, getCancel = function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.cancel);
+ }, getContinue = function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.continueButton);
+ }, getDialog = function(type) {
+ return getTemplateEl(container, selectorClasses[type + "Dialog"]);
+ }, getDelete = function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.deleteButton);
+ }, getDropProcessing = function() {
+ return getTemplateEl(container, selectorClasses.dropProcessing);
+ }, getEditIcon = function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.editNameIcon);
+ }, getFile = function(id) {
+ return fileBatch.map[id] || qq(fileList).getFirstByClass(FILE_CLASS_PREFIX + id);
+ }, getFilename = function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.file);
+ }, getPause = function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.pause);
+ }, getProgress = function(id) {
+ if (id == null) {
+ return getTemplateEl(container, selectorClasses.totalProgressBarContainer) || getTemplateEl(container, selectorClasses.totalProgressBar);
+ }
+ return getTemplateEl(getFile(id), selectorClasses.progressBarContainer) || getTemplateEl(getFile(id), selectorClasses.progressBar);
+ }, getRetry = function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.retry);
+ }, getSize = function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.size);
+ }, getSpinner = function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.spinner);
+ }, getTemplateEl = function(context, cssClass) {
+ return context && qq(context).getFirstByClass(cssClass);
+ }, getThumbnail = function(id) {
+ return showThumbnails && getTemplateEl(getFile(id), selectorClasses.thumbnail);
+ }, hide = function(el) {
+ el && qq(el).addClass(options.classes.hide);
+ }, maybeScalePlaceholderViaCss = function(placeholder, thumbnail) {
+ var maxWidth = placeholder.style.maxWidth, maxHeight = placeholder.style.maxHeight;
+ if (maxHeight && maxWidth && !thumbnail.style.maxWidth && !thumbnail.style.maxHeight) {
+ qq(thumbnail).css({
+ maxWidth: maxWidth,
+ maxHeight: maxHeight
+ });
+ }
+ }, maybeSetDisplayNotAvailableImg = function(id, thumbnail) {
+ var previewing = previewGeneration[id] || new qq.Promise().failure(), notAvailableImgPlacement = new qq.Promise();
+ cachedThumbnailNotAvailableImg.then(function(img) {
+ previewing.then(function() {
+ notAvailableImgPlacement.success();
+ }, function() {
+ maybeScalePlaceholderViaCss(img, thumbnail);
+ thumbnail.onload = function() {
+ thumbnail.onload = null;
+ notAvailableImgPlacement.success();
+ };
+ thumbnail.src = img.src;
+ show(thumbnail);
+ });
+ });
+ return notAvailableImgPlacement;
+ }, parseAndGetTemplate = function() {
+ var scriptEl, scriptHtml, fileListNode, tempTemplateEl, fileListEl, defaultButton, dropArea, thumbnail, dropProcessing, dropTextEl, uploaderEl;
+ log("Parsing template");
+ if (options.templateIdOrEl == null) {
+ throw new Error("You MUST specify either a template element or ID!");
+ }
+ if (qq.isString(options.templateIdOrEl)) {
+ scriptEl = document.getElementById(options.templateIdOrEl);
+ if (scriptEl === null) {
+ throw new Error(qq.format("Cannot find template script at ID '{}'!", options.templateIdOrEl));
+ }
+ scriptHtml = scriptEl.innerHTML;
+ } else {
+ if (options.templateIdOrEl.innerHTML === undefined) {
+ throw new Error("You have specified an invalid value for the template option! " + "It must be an ID or an Element.");
+ }
+ scriptHtml = options.templateIdOrEl.innerHTML;
+ }
+ scriptHtml = qq.trimStr(scriptHtml);
+ tempTemplateEl = document.createElement("div");
+ tempTemplateEl.appendChild(qq.toElement(scriptHtml));
+ uploaderEl = qq(tempTemplateEl).getFirstByClass(selectorClasses.uploader);
+ if (options.button) {
+ defaultButton = qq(tempTemplateEl).getFirstByClass(selectorClasses.button);
+ if (defaultButton) {
+ qq(defaultButton).remove();
+ }
+ }
+ if (!qq.DragAndDrop || !qq.supportedFeatures.fileDrop) {
+ dropProcessing = qq(tempTemplateEl).getFirstByClass(selectorClasses.dropProcessing);
+ if (dropProcessing) {
+ qq(dropProcessing).remove();
+ }
+ }
+ dropArea = qq(tempTemplateEl).getFirstByClass(selectorClasses.drop);
+ if (dropArea && !qq.DragAndDrop) {
+ log("DnD module unavailable.", "info");
+ qq(dropArea).remove();
+ }
+ if (!qq.supportedFeatures.fileDrop) {
+ uploaderEl.removeAttribute(DROPZPONE_TEXT_ATTR);
+ if (dropArea && qq(dropArea).hasAttribute(HIDE_DROPZONE_ATTR)) {
+ qq(dropArea).css({
+ display: "none"
+ });
+ }
+ } else if (qq(uploaderEl).hasAttribute(DROPZPONE_TEXT_ATTR) && dropArea) {
+ dropTextEl = qq(dropArea).getFirstByClass(selectorClasses.dropText);
+ dropTextEl && qq(dropTextEl).remove();
+ }
+ thumbnail = qq(tempTemplateEl).getFirstByClass(selectorClasses.thumbnail);
+ if (!showThumbnails) {
+ thumbnail && qq(thumbnail).remove();
+ } else if (thumbnail) {
+ thumbnailMaxSize = parseInt(thumbnail.getAttribute(THUMBNAIL_MAX_SIZE_ATTR));
+ thumbnailMaxSize = thumbnailMaxSize > 0 ? thumbnailMaxSize : null;
+ serverScale = qq(thumbnail).hasAttribute(THUMBNAIL_SERVER_SCALE_ATTR);
+ }
+ showThumbnails = showThumbnails && thumbnail;
+ isEditElementsExist = qq(tempTemplateEl).getByClass(selectorClasses.editFilenameInput).length > 0;
+ isRetryElementExist = qq(tempTemplateEl).getByClass(selectorClasses.retry).length > 0;
+ fileListNode = qq(tempTemplateEl).getFirstByClass(selectorClasses.list);
+ if (fileListNode == null) {
+ throw new Error("Could not find the file list container in the template!");
+ }
+ fileListEl = fileListNode.children[0].cloneNode(true);
+ fileListNode.innerHTML = "";
+ if (tempTemplateEl.getElementsByTagName("DIALOG").length) {
+ document.createElement("dialog");
+ }
+ log("Template parsing complete");
+ return {
+ template: tempTemplateEl,
+ fileTemplate: fileListEl
+ };
+ }, prependFile = function(el, index, fileList) {
+ var parentEl = fileList, beforeEl = parentEl.firstChild;
+ if (index > 0) {
+ beforeEl = qq(parentEl).children()[index].nextSibling;
+ }
+ parentEl.insertBefore(el, beforeEl);
+ }, processNewQueuedPreviewRequest = function(queuedThumbRequest) {
+ var id = queuedThumbRequest.id, optFileOrBlob = queuedThumbRequest.optFileOrBlob, relatedThumbnailId = optFileOrBlob && optFileOrBlob.qqThumbnailId, thumbnail = getThumbnail(id), spec = {
+ customResizeFunction: queuedThumbRequest.customResizeFunction,
+ maxSize: thumbnailMaxSize,
+ orient: true,
+ scale: true
+ };
+ if (qq.supportedFeatures.imagePreviews) {
+ if (thumbnail) {
+ if (options.limits.maxThumbs && options.limits.maxThumbs <= generatedThumbnails) {
+ maybeSetDisplayNotAvailableImg(id, thumbnail);
+ generateNextQueuedPreview();
+ } else {
+ displayWaitingImg(thumbnail).done(function() {
+ previewGeneration[id] = new qq.Promise();
+ previewGeneration[id].done(function() {
+ setTimeout(generateNextQueuedPreview, options.limits.timeBetweenThumbs);
+ });
+ if (relatedThumbnailId != null) {
+ useCachedPreview(id, relatedThumbnailId);
+ } else {
+ generateNewPreview(id, optFileOrBlob, spec);
+ }
+ });
+ }
+ } else {
+ generateNextQueuedPreview();
+ }
+ } else if (thumbnail) {
+ displayWaitingImg(thumbnail);
+ generateNextQueuedPreview();
+ }
+ }, processUpdateQueuedPreviewRequest = function(queuedThumbRequest) {
+ var id = queuedThumbRequest.id, thumbnailUrl = queuedThumbRequest.thumbnailUrl, showWaitingImg = queuedThumbRequest.showWaitingImg, thumbnail = getThumbnail(id), spec = {
+ customResizeFunction: queuedThumbRequest.customResizeFunction,
+ scale: serverScale,
+ maxSize: thumbnailMaxSize
+ };
+ if (thumbnail) {
+ if (thumbnailUrl) {
+ if (options.limits.maxThumbs && options.limits.maxThumbs <= generatedThumbnails) {
+ maybeSetDisplayNotAvailableImg(id, thumbnail);
+ generateNextQueuedPreview();
+ } else {
+ if (showWaitingImg) {
+ displayWaitingImg(thumbnail);
+ }
+ return options.imageGenerator.generate(thumbnailUrl, thumbnail, spec).then(function() {
+ show(thumbnail);
+ generatedThumbnails++;
+ setTimeout(generateNextQueuedPreview, options.limits.timeBetweenThumbs);
+ }, function() {
+ maybeSetDisplayNotAvailableImg(id, thumbnail);
+ setTimeout(generateNextQueuedPreview, options.limits.timeBetweenThumbs);
+ });
+ }
+ } else {
+ maybeSetDisplayNotAvailableImg(id, thumbnail);
+ generateNextQueuedPreview();
+ }
+ }
+ }, setProgressBarWidth = function(id, percent) {
+ var bar = getProgress(id), progressBarSelector = id == null ? selectorClasses.totalProgressBar : selectorClasses.progressBar;
+ if (bar && !qq(bar).hasClass(progressBarSelector)) {
+ bar = qq(bar).getFirstByClass(progressBarSelector);
+ }
+ if (bar) {
+ qq(bar).css({
+ width: percent + "%"
+ });
+ bar.setAttribute("aria-valuenow", percent);
+ }
+ }, show = function(el) {
+ el && qq(el).removeClass(options.classes.hide);
+ }, useCachedPreview = function(targetThumbnailId, cachedThumbnailId) {
+ var targetThumbnail = getThumbnail(targetThumbnailId), cachedThumbnail = getThumbnail(cachedThumbnailId);
+ log(qq.format("ID {} is the same file as ID {}. Will use generated thumbnail from ID {} instead.", targetThumbnailId, cachedThumbnailId, cachedThumbnailId));
+ previewGeneration[cachedThumbnailId].then(function() {
+ generatedThumbnails++;
+ previewGeneration[targetThumbnailId].success();
+ log(qq.format("Now using previously generated thumbnail created for ID {} on ID {}.", cachedThumbnailId, targetThumbnailId));
+ targetThumbnail.src = cachedThumbnail.src;
+ show(targetThumbnail);
+ }, function() {
+ previewGeneration[targetThumbnailId].failure();
+ if (!options.placeholders.waitUntilUpdate) {
+ maybeSetDisplayNotAvailableImg(targetThumbnailId, targetThumbnail);
+ }
+ });
+ };
+ qq.extend(options, spec);
+ log = options.log;
+ if (!qq.supportedFeatures.imagePreviews) {
+ options.limits.timeBetweenThumbs = 0;
+ options.limits.maxThumbs = 0;
+ }
+ container = options.containerEl;
+ showThumbnails = options.imageGenerator !== undefined;
+ templateDom = parseAndGetTemplate();
+ cacheThumbnailPlaceholders();
+ qq.extend(this, {
+ render: function() {
+ log("Rendering template in DOM.");
+ generatedThumbnails = 0;
+ container.appendChild(templateDom.template.cloneNode(true));
+ hide(getDropProcessing());
+ this.hideTotalProgress();
+ fileList = options.fileContainerEl || getTemplateEl(container, selectorClasses.list);
+ log("Template rendering complete");
+ },
+ renderFailure: function(message) {
+ var cantRenderEl = qq.toElement(message);
+ container.innerHTML = "";
+ container.appendChild(cantRenderEl);
+ },
+ reset: function() {
+ this.render();
+ },
+ clearFiles: function() {
+ fileList.innerHTML = "";
+ },
+ disableCancel: function() {
+ isCancelDisabled = true;
+ },
+ addFile: function(id, name, prependInfo, hideForever, batch) {
+ var fileEl = templateDom.fileTemplate.cloneNode(true), fileNameEl = getTemplateEl(fileEl, selectorClasses.file), uploaderEl = getTemplateEl(container, selectorClasses.uploader), fileContainer = batch ? fileBatch.content : fileList, thumb;
+ if (batch) {
+ fileBatch.map[id] = fileEl;
+ }
+ qq(fileEl).addClass(FILE_CLASS_PREFIX + id);
+ uploaderEl.removeAttribute(DROPZPONE_TEXT_ATTR);
+ if (fileNameEl) {
+ qq(fileNameEl).setText(name);
+ fileNameEl.setAttribute("title", name);
+ }
+ fileEl.setAttribute(FILE_ID_ATTR, id);
+ if (prependInfo) {
+ prependFile(fileEl, prependInfo.index, fileContainer);
+ } else {
+ fileContainer.appendChild(fileEl);
+ }
+ if (hideForever) {
+ fileEl.style.display = "none";
+ qq(fileEl).addClass(HIDDEN_FOREVER_CLASS);
+ } else {
+ hide(getProgress(id));
+ hide(getSize(id));
+ hide(getDelete(id));
+ hide(getRetry(id));
+ hide(getPause(id));
+ hide(getContinue(id));
+ if (isCancelDisabled) {
+ this.hideCancel(id);
+ }
+ thumb = getThumbnail(id);
+ if (thumb && !thumb.src) {
+ cachedWaitingForThumbnailImg.then(function(waitingImg) {
+ thumb.src = waitingImg.src;
+ if (waitingImg.style.maxHeight && waitingImg.style.maxWidth) {
+ qq(thumb).css({
+ maxHeight: waitingImg.style.maxHeight,
+ maxWidth: waitingImg.style.maxWidth
+ });
+ }
+ show(thumb);
+ });
+ }
+ }
+ },
+ addFileToCache: function(id, name, prependInfo, hideForever) {
+ this.addFile(id, name, prependInfo, hideForever, true);
+ },
+ addCacheToDom: function() {
+ fileList.appendChild(fileBatch.content);
+ fileBatch.content = document.createDocumentFragment();
+ fileBatch.map = {};
+ },
+ removeFile: function(id) {
+ qq(getFile(id)).remove();
+ },
+ getFileId: function(el) {
+ var currentNode = el;
+ if (currentNode) {
+ while (currentNode.getAttribute(FILE_ID_ATTR) == null) {
+ currentNode = currentNode.parentNode;
+ }
+ return parseInt(currentNode.getAttribute(FILE_ID_ATTR));
+ }
+ },
+ getFileList: function() {
+ return fileList;
+ },
+ markFilenameEditable: function(id) {
+ var filename = getFilename(id);
+ filename && qq(filename).addClass(options.classes.editable);
+ },
+ updateFilename: function(id, name) {
+ var filenameEl = getFilename(id);
+ if (filenameEl) {
+ qq(filenameEl).setText(name);
+ filenameEl.setAttribute("title", name);
+ }
+ },
+ hideFilename: function(id) {
+ hide(getFilename(id));
+ },
+ showFilename: function(id) {
+ show(getFilename(id));
+ },
+ isFileName: function(el) {
+ return qq(el).hasClass(selectorClasses.file);
+ },
+ getButton: function() {
+ return options.button || getTemplateEl(container, selectorClasses.button);
+ },
+ hideDropProcessing: function() {
+ hide(getDropProcessing());
+ },
+ showDropProcessing: function() {
+ show(getDropProcessing());
+ },
+ getDropZone: function() {
+ return getTemplateEl(container, selectorClasses.drop);
+ },
+ isEditFilenamePossible: function() {
+ return isEditElementsExist;
+ },
+ hideRetry: function(id) {
+ hide(getRetry(id));
+ },
+ isRetryPossible: function() {
+ return isRetryElementExist;
+ },
+ showRetry: function(id) {
+ show(getRetry(id));
+ },
+ getFileContainer: function(id) {
+ return getFile(id);
+ },
+ showEditIcon: function(id) {
+ var icon = getEditIcon(id);
+ icon && qq(icon).addClass(options.classes.editable);
+ },
+ isHiddenForever: function(id) {
+ return qq(getFile(id)).hasClass(HIDDEN_FOREVER_CLASS);
+ },
+ hideEditIcon: function(id) {
+ var icon = getEditIcon(id);
+ icon && qq(icon).removeClass(options.classes.editable);
+ },
+ isEditIcon: function(el) {
+ return qq(el).hasClass(selectorClasses.editNameIcon, true);
+ },
+ getEditInput: function(id) {
+ return getTemplateEl(getFile(id), selectorClasses.editFilenameInput);
+ },
+ isEditInput: function(el) {
+ return qq(el).hasClass(selectorClasses.editFilenameInput, true);
+ },
+ updateProgress: function(id, loaded, total) {
+ var bar = getProgress(id), percent;
+ if (bar && total > 0) {
+ percent = Math.round(loaded / total * 100);
+ if (percent === 100) {
+ hide(bar);
+ } else {
+ show(bar);
+ }
+ setProgressBarWidth(id, percent);
+ }
+ },
+ updateTotalProgress: function(loaded, total) {
+ this.updateProgress(null, loaded, total);
+ },
+ hideProgress: function(id) {
+ var bar = getProgress(id);
+ bar && hide(bar);
+ },
+ hideTotalProgress: function() {
+ this.hideProgress();
+ },
+ resetProgress: function(id) {
+ setProgressBarWidth(id, 0);
+ this.hideTotalProgress(id);
+ },
+ resetTotalProgress: function() {
+ this.resetProgress();
+ },
+ showCancel: function(id) {
+ if (!isCancelDisabled) {
+ var cancel = getCancel(id);
+ cancel && qq(cancel).removeClass(options.classes.hide);
+ }
+ },
+ hideCancel: function(id) {
+ hide(getCancel(id));
+ },
+ isCancel: function(el) {
+ return qq(el).hasClass(selectorClasses.cancel, true);
+ },
+ allowPause: function(id) {
+ show(getPause(id));
+ hide(getContinue(id));
+ },
+ uploadPaused: function(id) {
+ this.setStatusText(id, options.text.paused);
+ this.allowContinueButton(id);
+ hide(getSpinner(id));
+ },
+ hidePause: function(id) {
+ hide(getPause(id));
+ },
+ isPause: function(el) {
+ return qq(el).hasClass(selectorClasses.pause, true);
+ },
+ isContinueButton: function(el) {
+ return qq(el).hasClass(selectorClasses.continueButton, true);
+ },
+ allowContinueButton: function(id) {
+ show(getContinue(id));
+ hide(getPause(id));
+ },
+ uploadContinued: function(id) {
+ this.setStatusText(id, "");
+ this.allowPause(id);
+ show(getSpinner(id));
+ },
+ showDeleteButton: function(id) {
+ show(getDelete(id));
+ },
+ hideDeleteButton: function(id) {
+ hide(getDelete(id));
+ },
+ isDeleteButton: function(el) {
+ return qq(el).hasClass(selectorClasses.deleteButton, true);
+ },
+ isRetry: function(el) {
+ return qq(el).hasClass(selectorClasses.retry, true);
+ },
+ updateSize: function(id, text) {
+ var size = getSize(id);
+ if (size) {
+ show(size);
+ qq(size).setText(text);
+ }
+ },
+ setStatusText: function(id, text) {
+ var textEl = getTemplateEl(getFile(id), selectorClasses.statusText);
+ if (textEl) {
+ if (text == null) {
+ qq(textEl).clearText();
+ } else {
+ qq(textEl).setText(text);
+ }
+ }
+ },
+ hideSpinner: function(id) {
+ qq(getFile(id)).removeClass(IN_PROGRESS_CLASS);
+ hide(getSpinner(id));
+ },
+ showSpinner: function(id) {
+ qq(getFile(id)).addClass(IN_PROGRESS_CLASS);
+ show(getSpinner(id));
+ },
+ generatePreview: function(id, optFileOrBlob, customResizeFunction) {
+ if (!this.isHiddenForever(id)) {
+ thumbGenerationQueue.push({
+ id: id,
+ customResizeFunction: customResizeFunction,
+ optFileOrBlob: optFileOrBlob
+ });
+ !thumbnailQueueMonitorRunning && generateNextQueuedPreview();
+ }
+ },
+ updateThumbnail: function(id, thumbnailUrl, showWaitingImg, customResizeFunction) {
+ if (!this.isHiddenForever(id)) {
+ thumbGenerationQueue.push({
+ customResizeFunction: customResizeFunction,
+ update: true,
+ id: id,
+ thumbnailUrl: thumbnailUrl,
+ showWaitingImg: showWaitingImg
+ });
+ !thumbnailQueueMonitorRunning && generateNextQueuedPreview();
+ }
+ },
+ hasDialog: function(type) {
+ return qq.supportedFeatures.dialogElement && !!getDialog(type);
+ },
+ showDialog: function(type, message, defaultValue) {
+ var dialog = getDialog(type), messageEl = getTemplateEl(dialog, selectorClasses.dialogMessage), inputEl = dialog.getElementsByTagName("INPUT")[0], cancelBtn = getTemplateEl(dialog, selectorClasses.dialogCancelButton), okBtn = getTemplateEl(dialog, selectorClasses.dialogOkButton), promise = new qq.Promise(), closeHandler = function() {
+ cancelBtn.removeEventListener("click", cancelClickHandler);
+ okBtn && okBtn.removeEventListener("click", okClickHandler);
+ promise.failure();
+ }, cancelClickHandler = function() {
+ cancelBtn.removeEventListener("click", cancelClickHandler);
+ dialog.close();
+ }, okClickHandler = function() {
+ dialog.removeEventListener("close", closeHandler);
+ okBtn.removeEventListener("click", okClickHandler);
+ dialog.close();
+ promise.success(inputEl && inputEl.value);
+ };
+ dialog.addEventListener("close", closeHandler);
+ cancelBtn.addEventListener("click", cancelClickHandler);
+ okBtn && okBtn.addEventListener("click", okClickHandler);
+ if (inputEl) {
+ inputEl.value = defaultValue;
+ }
+ messageEl.textContent = message;
+ dialog.showModal();
+ return promise;
+ }
+ });
+ };
+ qq.UiEventHandler = function(s, protectedApi) {
+ "use strict";
+ var disposer = new qq.DisposeSupport(), spec = {
+ eventType: "click",
+ attachTo: null,
+ onHandled: function(target, event) {}
+ };
+ qq.extend(this, {
+ addHandler: function(element) {
+ addHandler(element);
+ },
+ dispose: function() {
+ disposer.dispose();
+ }
+ });
+ function addHandler(element) {
+ disposer.attach(element, spec.eventType, function(event) {
+ event = event || window.event;
+ var target = event.target || event.srcElement;
+ spec.onHandled(target, event);
+ });
+ }
+ qq.extend(protectedApi, {
+ getFileIdFromItem: function(item) {
+ return item.qqFileId;
+ },
+ getDisposeSupport: function() {
+ return disposer;
+ }
+ });
+ qq.extend(spec, s);
+ if (spec.attachTo) {
+ addHandler(spec.attachTo);
+ }
+ };
+ qq.FileButtonsClickHandler = function(s) {
+ "use strict";
+ var inheritedInternalApi = {}, spec = {
+ templating: null,
+ log: function(message, lvl) {},
+ onDeleteFile: function(fileId) {},
+ onCancel: function(fileId) {},
+ onRetry: function(fileId) {},
+ onPause: function(fileId) {},
+ onContinue: function(fileId) {},
+ onGetName: function(fileId) {}
+ }, buttonHandlers = {
+ cancel: function(id) {
+ spec.onCancel(id);
+ },
+ retry: function(id) {
+ spec.onRetry(id);
+ },
+ deleteButton: function(id) {
+ spec.onDeleteFile(id);
+ },
+ pause: function(id) {
+ spec.onPause(id);
+ },
+ continueButton: function(id) {
+ spec.onContinue(id);
+ }
+ };
+ function examineEvent(target, event) {
+ qq.each(buttonHandlers, function(buttonType, handler) {
+ var firstLetterCapButtonType = buttonType.charAt(0).toUpperCase() + buttonType.slice(1), fileId;
+ if (spec.templating["is" + firstLetterCapButtonType](target)) {
+ fileId = spec.templating.getFileId(target);
+ qq.preventDefault(event);
+ spec.log(qq.format("Detected valid file button click event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
+ handler(fileId);
+ return false;
+ }
+ });
+ }
+ qq.extend(spec, s);
+ spec.eventType = "click";
+ spec.onHandled = examineEvent;
+ spec.attachTo = spec.templating.getFileList();
+ qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
+ };
+ qq.FilenameClickHandler = function(s) {
+ "use strict";
+ var inheritedInternalApi = {}, spec = {
+ templating: null,
+ log: function(message, lvl) {},
+ classes: {
+ file: "qq-upload-file",
+ editNameIcon: "qq-edit-filename-icon"
+ },
+ onGetUploadStatus: function(fileId) {},
+ onGetName: function(fileId) {}
+ };
+ qq.extend(spec, s);
+ function examineEvent(target, event) {
+ if (spec.templating.isFileName(target) || spec.templating.isEditIcon(target)) {
+ var fileId = spec.templating.getFileId(target), status = spec.onGetUploadStatus(fileId);
+ if (status === qq.status.SUBMITTED) {
+ spec.log(qq.format("Detected valid filename click event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
+ qq.preventDefault(event);
+ inheritedInternalApi.handleFilenameEdit(fileId, target, true);
+ }
+ }
+ }
+ spec.eventType = "click";
+ spec.onHandled = examineEvent;
+ qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
+ };
+ qq.FilenameInputFocusInHandler = function(s, inheritedInternalApi) {
+ "use strict";
+ var spec = {
+ templating: null,
+ onGetUploadStatus: function(fileId) {},
+ log: function(message, lvl) {}
+ };
+ if (!inheritedInternalApi) {
+ inheritedInternalApi = {};
+ }
+ function handleInputFocus(target, event) {
+ if (spec.templating.isEditInput(target)) {
+ var fileId = spec.templating.getFileId(target), status = spec.onGetUploadStatus(fileId);
+ if (status === qq.status.SUBMITTED) {
+ spec.log(qq.format("Detected valid filename input focus event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
+ inheritedInternalApi.handleFilenameEdit(fileId, target);
+ }
+ }
+ }
+ spec.eventType = "focusin";
+ spec.onHandled = handleInputFocus;
+ qq.extend(spec, s);
+ qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
+ };
+ qq.FilenameInputFocusHandler = function(spec) {
+ "use strict";
+ spec.eventType = "focus";
+ spec.attachTo = null;
+ qq.extend(this, new qq.FilenameInputFocusInHandler(spec, {}));
+ };
+ qq.FilenameEditHandler = function(s, inheritedInternalApi) {
+ "use strict";
+ var spec = {
+ templating: null,
+ log: function(message, lvl) {},
+ onGetUploadStatus: function(fileId) {},
+ onGetName: function(fileId) {},
+ onSetName: function(fileId, newName) {},
+ onEditingStatusChange: function(fileId, isEditing) {}
+ };
+ function getFilenameSansExtension(fileId) {
+ var filenameSansExt = spec.onGetName(fileId), extIdx = filenameSansExt.lastIndexOf(".");
+ if (extIdx > 0) {
+ filenameSansExt = filenameSansExt.substr(0, extIdx);
+ }
+ return filenameSansExt;
+ }
+ function getOriginalExtension(fileId) {
+ var origName = spec.onGetName(fileId);
+ return qq.getExtension(origName);
+ }
+ function handleNameUpdate(newFilenameInputEl, fileId) {
+ var newName = newFilenameInputEl.value, origExtension;
+ if (newName !== undefined && qq.trimStr(newName).length > 0) {
+ origExtension = getOriginalExtension(fileId);
+ if (origExtension !== undefined) {
+ newName = newName + "." + origExtension;
+ }
+ spec.onSetName(fileId, newName);
+ }
+ spec.onEditingStatusChange(fileId, false);
+ }
+ function registerInputBlurHandler(inputEl, fileId) {
+ inheritedInternalApi.getDisposeSupport().attach(inputEl, "blur", function() {
+ handleNameUpdate(inputEl, fileId);
+ });
+ }
+ function registerInputEnterKeyHandler(inputEl, fileId) {
+ inheritedInternalApi.getDisposeSupport().attach(inputEl, "keyup", function(event) {
+ var code = event.keyCode || event.which;
+ if (code === 13) {
+ handleNameUpdate(inputEl, fileId);
+ }
+ });
+ }
+ qq.extend(spec, s);
+ spec.attachTo = spec.templating.getFileList();
+ qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
+ qq.extend(inheritedInternalApi, {
+ handleFilenameEdit: function(id, target, focusInput) {
+ var newFilenameInputEl = spec.templating.getEditInput(id);
+ spec.onEditingStatusChange(id, true);
+ newFilenameInputEl.value = getFilenameSansExtension(id);
+ if (focusInput) {
+ newFilenameInputEl.focus();
+ }
+ registerInputBlurHandler(newFilenameInputEl, id);
+ registerInputEnterKeyHandler(newFilenameInputEl, id);
+ }
+ });
+ };
+})(window);
+//# sourceMappingURL=fine-uploader.js.map
diff --git a/services/web/public/js/libs/fineuploader.js b/services/web/public/js/libs/fineuploader.js
index 606a2fb70a..b109b8a1c1 100644
--- a/services/web/public/js/libs/fineuploader.js
+++ b/services/web/public/js/libs/fineuploader.js
@@ -1,7455 +1,3761 @@
-// Fine Uploader 5.15.4 - MIT licensed. http://fineuploader.com
-(function(global) {
- var qq = function(element) {
- "use strict";
- return {
- hide: function() {
- element.style.display = "none";
- return this;
- },
- attach: function(type, fn) {
- if (element.addEventListener) {
- element.addEventListener(type, fn, false);
- } else if (element.attachEvent) {
- element.attachEvent("on" + type, fn);
- }
- return function() {
- qq(element).detach(type, fn);
- };
- },
- detach: function(type, fn) {
- if (element.removeEventListener) {
- element.removeEventListener(type, fn, false);
- } else if (element.attachEvent) {
- element.detachEvent("on" + type, fn);
- }
- return this;
- },
- contains: function(descendant) {
- if (!descendant) {
- return false;
- }
- if (element === descendant) {
- return true;
- }
- if (element.contains) {
- return element.contains(descendant);
- } else {
- return !!(descendant.compareDocumentPosition(element) & 8);
- }
- },
- insertBefore: function(elementB) {
- elementB.parentNode.insertBefore(element, elementB);
- return this;
- },
- remove: function() {
- element.parentNode.removeChild(element);
- return this;
- },
- css: function(styles) {
- if (element.style == null) {
- throw new qq.Error("Can't apply style to node as it is not on the HTMLElement prototype chain!");
- }
- if (styles.opacity != null) {
- if (typeof element.style.opacity !== "string" && typeof element.filters !== "undefined") {
- styles.filter = "alpha(opacity=" + Math.round(100 * styles.opacity) + ")";
- }
- }
- qq.extend(element.style, styles);
- return this;
- },
- hasClass: function(name, considerParent) {
- var re = new RegExp("(^| )" + name + "( |$)");
- return re.test(element.className) || !!(considerParent && re.test(element.parentNode.className));
- },
- addClass: function(name) {
- if (!qq(element).hasClass(name)) {
- element.className += " " + name;
- }
- return this;
- },
- removeClass: function(name) {
- var re = new RegExp("(^| )" + name + "( |$)");
- element.className = element.className.replace(re, " ").replace(/^\s+|\s+$/g, "");
- return this;
- },
- getByClass: function(className, first) {
- var candidates, result = [];
- if (first && element.querySelector) {
- return element.querySelector("." + className);
- } else if (element.querySelectorAll) {
- return element.querySelectorAll("." + className);
- }
- candidates = element.getElementsByTagName("*");
- qq.each(candidates, function(idx, val) {
- if (qq(val).hasClass(className)) {
- result.push(val);
- }
- });
- return first ? result[0] : result;
- },
- getFirstByClass: function(className) {
- return qq(element).getByClass(className, true);
- },
- children: function() {
- var children = [], child = element.firstChild;
- while (child) {
- if (child.nodeType === 1) {
- children.push(child);
- }
- child = child.nextSibling;
- }
- return children;
- },
- setText: function(text) {
- element.innerText = text;
- element.textContent = text;
- return this;
- },
- clearText: function() {
- return qq(element).setText("");
- },
- hasAttribute: function(attrName) {
- var attrVal;
- if (element.hasAttribute) {
- if (!element.hasAttribute(attrName)) {
- return false;
- }
- return /^false$/i.exec(element.getAttribute(attrName)) == null;
- } else {
- attrVal = element[attrName];
- if (attrVal === undefined) {
- return false;
- }
- return /^false$/i.exec(attrVal) == null;
- }
+/**
+ * http://github.com/Valums-File-Uploader/file-uploader
+ *
+ * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers.
+ *
+ * Original version: 1.0 © 2010 Andrew Valums ( andrew(at)valums.com )
+ * Current Maintainer (2.0+): © 2012, Ray Nicholus ( fineuploader(at)garstasio.com )
+ *
+ * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
+ */
+/*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob*/
+var qq = function(element) {
+ "use strict";
+
+ return {
+ hide: function() {
+ element.style.display = 'none';
+ return this;
+ },
+
+ /** Returns the function which detaches attached event */
+ attach: function(type, fn) {
+ if (element.addEventListener){
+ element.addEventListener(type, fn, false);
+ } else if (element.attachEvent){
+ element.attachEvent('on' + type, fn);
}
- };
- };
- (function() {
- "use strict";
- qq.canvasToBlob = function(canvas, mime, quality) {
- return qq.dataUriToBlob(canvas.toDataURL(mime, quality));
- };
- qq.dataUriToBlob = function(dataUri) {
- var arrayBuffer, byteString, createBlob = function(data, mime) {
- var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder, blobBuilder = BlobBuilder && new BlobBuilder();
- if (blobBuilder) {
- blobBuilder.append(data);
- return blobBuilder.getBlob(mime);
- } else {
- return new Blob([ data ], {
- type: mime
- });
- }
- }, intArray, mimeString;
- if (dataUri.split(",")[0].indexOf("base64") >= 0) {
- byteString = atob(dataUri.split(",")[1]);
- } else {
- byteString = decodeURI(dataUri.split(",")[1]);
- }
- mimeString = dataUri.split(",")[0].split(":")[1].split(";")[0];
- arrayBuffer = new ArrayBuffer(byteString.length);
- intArray = new Uint8Array(arrayBuffer);
- qq.each(byteString, function(idx, character) {
- intArray[idx] = character.charCodeAt(0);
- });
- return createBlob(arrayBuffer, mimeString);
- };
- qq.log = function(message, level) {
- if (window.console) {
- if (!level || level === "info") {
- window.console.log(message);
- } else {
- if (window.console[level]) {
- window.console[level](message);
- } else {
- window.console.log("<" + level + "> " + message);
- }
- }
- }
- };
- qq.isObject = function(variable) {
- return variable && !variable.nodeType && Object.prototype.toString.call(variable) === "[object Object]";
- };
- qq.isFunction = function(variable) {
- return typeof variable === "function";
- };
- qq.isArray = function(value) {
- return Object.prototype.toString.call(value) === "[object Array]" || value && window.ArrayBuffer && value.buffer && value.buffer.constructor === ArrayBuffer;
- };
- qq.isItemList = function(maybeItemList) {
- return Object.prototype.toString.call(maybeItemList) === "[object DataTransferItemList]";
- };
- qq.isNodeList = function(maybeNodeList) {
- return Object.prototype.toString.call(maybeNodeList) === "[object NodeList]" || maybeNodeList.item && maybeNodeList.namedItem;
- };
- qq.isString = function(maybeString) {
- return Object.prototype.toString.call(maybeString) === "[object String]";
- };
- qq.trimStr = function(string) {
- if (String.prototype.trim) {
- return string.trim();
- }
- return string.replace(/^\s+|\s+$/g, "");
- };
- qq.format = function(str) {
- var args = Array.prototype.slice.call(arguments, 1), newStr = str, nextIdxToReplace = newStr.indexOf("{}");
- qq.each(args, function(idx, val) {
- var strBefore = newStr.substring(0, nextIdxToReplace), strAfter = newStr.substring(nextIdxToReplace + 2);
- newStr = strBefore + val + strAfter;
- nextIdxToReplace = newStr.indexOf("{}", nextIdxToReplace + val.length);
- if (nextIdxToReplace < 0) {
- return false;
- }
- });
- return newStr;
- };
- qq.isFile = function(maybeFile) {
- return window.File && Object.prototype.toString.call(maybeFile) === "[object File]";
- };
- qq.isFileList = function(maybeFileList) {
- return window.FileList && Object.prototype.toString.call(maybeFileList) === "[object FileList]";
- };
- qq.isFileOrInput = function(maybeFileOrInput) {
- return qq.isFile(maybeFileOrInput) || qq.isInput(maybeFileOrInput);
- };
- qq.isInput = function(maybeInput, notFile) {
- var evaluateType = function(type) {
- var normalizedType = type.toLowerCase();
- if (notFile) {
- return normalizedType !== "file";
- }
- return normalizedType === "file";
+ return function() {
+ qq(element).detach(type, fn);
};
- if (window.HTMLInputElement) {
- if (Object.prototype.toString.call(maybeInput) === "[object HTMLInputElement]") {
- if (maybeInput.type && evaluateType(maybeInput.type)) {
- return true;
- }
- }
+ },
+
+ detach: function(type, fn) {
+ if (element.removeEventListener){
+ element.removeEventListener(type, fn, false);
+ } else if (element.attachEvent){
+ element.detachEvent('on' + type, fn);
}
- if (maybeInput.tagName) {
- if (maybeInput.tagName.toLowerCase() === "input") {
- if (maybeInput.type && evaluateType(maybeInput.type)) {
- return true;
- }
- }
- }
- return false;
- };
- qq.isBlob = function(maybeBlob) {
- if (window.Blob && Object.prototype.toString.call(maybeBlob) === "[object Blob]") {
+ return this;
+ },
+
+ contains: function(descendant) {
+ // compareposition returns false in this case
+ if (element === descendant) {
return true;
}
- };
- qq.isXhrUploadSupported = function() {
- var input = document.createElement("input");
- input.type = "file";
- return input.multiple !== undefined && typeof File !== "undefined" && typeof FormData !== "undefined" && typeof qq.createXhrInstance().upload !== "undefined";
- };
- qq.createXhrInstance = function() {
- if (window.XMLHttpRequest) {
- return new XMLHttpRequest();
- }
- try {
- return new ActiveXObject("MSXML2.XMLHTTP.3.0");
- } catch (error) {
- qq.log("Neither XHR or ActiveX are supported!", "error");
- return null;
- }
- };
- qq.isFolderDropSupported = function(dataTransfer) {
- return dataTransfer.items && dataTransfer.items.length > 0 && dataTransfer.items[0].webkitGetAsEntry;
- };
- qq.isFileChunkingSupported = function() {
- return !qq.androidStock() && qq.isXhrUploadSupported() && (File.prototype.slice !== undefined || File.prototype.webkitSlice !== undefined || File.prototype.mozSlice !== undefined);
- };
- qq.sliceBlob = function(fileOrBlob, start, end) {
- var slicer = fileOrBlob.slice || fileOrBlob.mozSlice || fileOrBlob.webkitSlice;
- return slicer.call(fileOrBlob, start, end);
- };
- qq.arrayBufferToHex = function(buffer) {
- var bytesAsHex = "", bytes = new Uint8Array(buffer);
- qq.each(bytes, function(idx, byt) {
- var byteAsHexStr = byt.toString(16);
- if (byteAsHexStr.length < 2) {
- byteAsHexStr = "0" + byteAsHexStr;
- }
- bytesAsHex += byteAsHexStr;
- });
- return bytesAsHex;
- };
- qq.readBlobToHex = function(blob, startOffset, length) {
- var initialBlob = qq.sliceBlob(blob, startOffset, startOffset + length), fileReader = new FileReader(), promise = new qq.Promise();
- fileReader.onload = function() {
- promise.success(qq.arrayBufferToHex(fileReader.result));
- };
- fileReader.onerror = promise.failure;
- fileReader.readAsArrayBuffer(initialBlob);
- return promise;
- };
- qq.extend = function(first, second, extendNested) {
- qq.each(second, function(prop, val) {
- if (extendNested && qq.isObject(val)) {
- if (first[prop] === undefined) {
- first[prop] = {};
- }
- qq.extend(first[prop], val, true);
- } else {
- first[prop] = val;
- }
- });
- return first;
- };
- qq.override = function(target, sourceFn) {
- var super_ = {}, source = sourceFn(super_);
- qq.each(source, function(srcPropName, srcPropVal) {
- if (target[srcPropName] !== undefined) {
- super_[srcPropName] = target[srcPropName];
- }
- target[srcPropName] = srcPropVal;
- });
- return target;
- };
- qq.indexOf = function(arr, elt, from) {
- if (arr.indexOf) {
- return arr.indexOf(elt, from);
- }
- from = from || 0;
- var len = arr.length;
- if (from < 0) {
- from += len;
- }
- for (;from < len; from += 1) {
- if (arr.hasOwnProperty(from) && arr[from] === elt) {
- return from;
- }
- }
- return -1;
- };
- qq.getUniqueId = function() {
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
- var r = Math.random() * 16 | 0, v = c == "x" ? r : r & 3 | 8;
- return v.toString(16);
- });
- };
- qq.ie = function() {
- return navigator.userAgent.indexOf("MSIE") !== -1 || navigator.userAgent.indexOf("Trident") !== -1;
- };
- qq.ie7 = function() {
- return navigator.userAgent.indexOf("MSIE 7") !== -1;
- };
- qq.ie8 = function() {
- return navigator.userAgent.indexOf("MSIE 8") !== -1;
- };
- qq.ie10 = function() {
- return navigator.userAgent.indexOf("MSIE 10") !== -1;
- };
- qq.ie11 = function() {
- return qq.ie() && navigator.userAgent.indexOf("rv:11") !== -1;
- };
- qq.edge = function() {
- return navigator.userAgent.indexOf("Edge") >= 0;
- };
- qq.safari = function() {
- return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1;
- };
- qq.chrome = function() {
- return navigator.vendor !== undefined && navigator.vendor.indexOf("Google") !== -1;
- };
- qq.opera = function() {
- return navigator.vendor !== undefined && navigator.vendor.indexOf("Opera") !== -1;
- };
- qq.firefox = function() {
- return !qq.edge() && !qq.ie11() && navigator.userAgent.indexOf("Mozilla") !== -1 && navigator.vendor !== undefined && navigator.vendor === "";
- };
- qq.windows = function() {
- return navigator.platform === "Win32";
- };
- qq.android = function() {
- return navigator.userAgent.toLowerCase().indexOf("android") !== -1;
- };
- qq.androidStock = function() {
- return qq.android() && navigator.userAgent.toLowerCase().indexOf("chrome") < 0;
- };
- qq.ios6 = function() {
- return qq.ios() && navigator.userAgent.indexOf(" OS 6_") !== -1;
- };
- qq.ios7 = function() {
- return qq.ios() && navigator.userAgent.indexOf(" OS 7_") !== -1;
- };
- qq.ios8 = function() {
- return qq.ios() && navigator.userAgent.indexOf(" OS 8_") !== -1;
- };
- qq.ios800 = function() {
- return qq.ios() && navigator.userAgent.indexOf(" OS 8_0 ") !== -1;
- };
- qq.ios = function() {
- return navigator.userAgent.indexOf("iPad") !== -1 || navigator.userAgent.indexOf("iPod") !== -1 || navigator.userAgent.indexOf("iPhone") !== -1;
- };
- qq.iosChrome = function() {
- return qq.ios() && navigator.userAgent.indexOf("CriOS") !== -1;
- };
- qq.iosSafari = function() {
- return qq.ios() && !qq.iosChrome() && navigator.userAgent.indexOf("Safari") !== -1;
- };
- qq.iosSafariWebView = function() {
- return qq.ios() && !qq.iosChrome() && !qq.iosSafari();
- };
- qq.preventDefault = function(e) {
- if (e.preventDefault) {
- e.preventDefault();
+
+ if (element.contains){
+ return element.contains(descendant);
} else {
- e.returnValue = false;
+ /*jslint bitwise: true*/
+ return !!(descendant.compareDocumentPosition(element) & 8);
}
- };
- qq.toElement = function() {
- var div = document.createElement("div");
- return function(html) {
- div.innerHTML = html;
- var element = div.firstChild;
- div.removeChild(element);
- return element;
- };
- }();
- qq.each = function(iterableItem, callback) {
- var keyOrIndex, retVal;
- if (iterableItem) {
- if (window.Storage && iterableItem.constructor === window.Storage) {
- for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) {
- retVal = callback(iterableItem.key(keyOrIndex), iterableItem.getItem(iterableItem.key(keyOrIndex)));
- if (retVal === false) {
- break;
- }
- }
- } else if (qq.isArray(iterableItem) || qq.isItemList(iterableItem) || qq.isNodeList(iterableItem)) {
- for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) {
- retVal = callback(keyOrIndex, iterableItem[keyOrIndex]);
- if (retVal === false) {
- break;
- }
- }
- } else if (qq.isString(iterableItem)) {
- for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) {
- retVal = callback(keyOrIndex, iterableItem.charAt(keyOrIndex));
- if (retVal === false) {
- break;
- }
- }
- } else {
- for (keyOrIndex in iterableItem) {
- if (Object.prototype.hasOwnProperty.call(iterableItem, keyOrIndex)) {
- retVal = callback(keyOrIndex, iterableItem[keyOrIndex]);
- if (retVal === false) {
- break;
- }
- }
- }
+ },
+
+ /**
+ * Insert this element before elementB.
+ */
+ insertBefore: function(elementB) {
+ elementB.parentNode.insertBefore(element, elementB);
+ return this;
+ },
+
+ remove: function() {
+ element.parentNode.removeChild(element);
+ return this;
+ },
+
+ /**
+ * Sets styles for an element.
+ * Fixes opacity in IE6-8.
+ */
+ css: function(styles) {
+ if (styles.opacity !== null){
+ if (typeof element.style.opacity !== 'string' && typeof(element.filters) !== 'undefined'){
+ styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
}
}
- };
- qq.bind = function(oldFunc, context) {
- if (qq.isFunction(oldFunc)) {
- var args = Array.prototype.slice.call(arguments, 2);
- return function() {
- var newArgs = qq.extend([], args);
- if (arguments.length) {
- newArgs = newArgs.concat(Array.prototype.slice.call(arguments));
- }
- return oldFunc.apply(context, newArgs);
- };
+ qq.extend(element.style, styles);
+
+ return this;
+ },
+
+ hasClass: function(name) {
+ var re = new RegExp('(^| )' + name + '( |$)');
+ return re.test(element.className);
+ },
+
+ addClass: function(name) {
+ if (!qq(element).hasClass(name)){
+ element.className += ' ' + name;
}
- throw new Error("first parameter must be a function!");
- };
- qq.obj2url = function(obj, temp, prefixDone) {
- var uristrings = [], prefix = "&", add = function(nextObj, i) {
- var nextTemp = temp ? /\[\]$/.test(temp) ? temp : temp + "[" + i + "]" : i;
- if (nextTemp !== "undefined" && i !== "undefined") {
- uristrings.push(typeof nextObj === "object" ? qq.obj2url(nextObj, nextTemp, true) : Object.prototype.toString.call(nextObj) === "[object Function]" ? encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj()) : encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj));
- }
- };
- if (!prefixDone && temp) {
- prefix = /\?/.test(temp) ? /\?$/.test(temp) ? "" : "&" : "?";
- uristrings.push(temp);
- uristrings.push(qq.obj2url(obj));
- } else if (Object.prototype.toString.call(obj) === "[object Array]" && typeof obj !== "undefined") {
- qq.each(obj, function(idx, val) {
- add(val, idx);
- });
- } else if (typeof obj !== "undefined" && obj !== null && typeof obj === "object") {
- qq.each(obj, function(prop, val) {
- add(val, prop);
- });
- } else {
- uristrings.push(encodeURIComponent(temp) + "=" + encodeURIComponent(obj));
+ return this;
+ },
+
+ removeClass: function(name) {
+ var re = new RegExp('(^| )' + name + '( |$)');
+ element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
+ return this;
+ },
+
+ getByClass: function(className) {
+ var candidates,
+ result = [];
+
+ if (element.querySelectorAll){
+ return element.querySelectorAll('.' + className);
}
- if (temp) {
- return uristrings.join(prefix);
- } else {
- return uristrings.join(prefix).replace(/^&/, "").replace(/%20/g, "+");
- }
- };
- qq.obj2FormData = function(obj, formData, arrayKeyName) {
- if (!formData) {
- formData = new FormData();
- }
- qq.each(obj, function(key, val) {
- key = arrayKeyName ? arrayKeyName + "[" + key + "]" : key;
- if (qq.isObject(val)) {
- qq.obj2FormData(val, formData, key);
- } else if (qq.isFunction(val)) {
- formData.append(key, val());
- } else {
- formData.append(key, val);
+
+ candidates = element.getElementsByTagName("*");
+
+ qq.each(candidates, function(idx, val) {
+ if (qq(val).hasClass(className)){
+ result.push(val);
}
});
- return formData;
- };
- qq.obj2Inputs = function(obj, form) {
- var input;
- if (!form) {
- form = document.createElement("form");
- }
- qq.obj2FormData(obj, {
- append: function(key, val) {
- input = document.createElement("input");
- input.setAttribute("name", key);
- input.setAttribute("value", val);
- form.appendChild(input);
+ return result;
+ },
+
+ children: function() {
+ var children = [],
+ child = element.firstChild;
+
+ while (child){
+ if (child.nodeType === 1){
+ children.push(child);
}
- });
- return form;
- };
- qq.parseJson = function(json) {
- if (window.JSON && qq.isFunction(JSON.parse)) {
- return JSON.parse(json);
- } else {
- return eval("(" + json + ")");
+ child = child.nextSibling;
}
- };
- qq.getExtension = function(filename) {
- var extIdx = filename.lastIndexOf(".") + 1;
- if (extIdx > 0) {
- return filename.substr(extIdx, filename.length - extIdx);
+
+ return children;
+ },
+
+ setText: function(text) {
+ element.innerText = text;
+ element.textContent = text;
+ return this;
+ },
+
+ clearText: function() {
+ return qq(element).setText("");
+ }
+ };
+};
+
+qq.log = function(message, level) {
+ "use strict";
+
+ if (window.console) {
+ if (!level || level === 'info') {
+ window.console.log(message);
+ }
+ else
+ {
+ if (window.console[level]) {
+ window.console[level](message);
}
- };
- qq.getFilename = function(blobOrFileInput) {
- if (qq.isInput(blobOrFileInput)) {
- return blobOrFileInput.value.replace(/.*(\/|\\)/, "");
- } else if (qq.isFile(blobOrFileInput)) {
- if (blobOrFileInput.fileName !== null && blobOrFileInput.fileName !== undefined) {
- return blobOrFileInput.fileName;
- }
+ else {
+ window.console.log('<' + level + '> ' + message);
}
- return blobOrFileInput.name;
- };
- qq.DisposeSupport = function() {
- var disposers = [];
- return {
- dispose: function() {
- var disposer;
- do {
- disposer = disposers.shift();
- if (disposer) {
- disposer();
- }
- } while (disposer);
- },
- attach: function() {
- var args = arguments;
- this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
- },
- addDisposer: function(disposeFunction) {
- disposers.push(disposeFunction);
- }
- };
- };
- })();
- (function() {
- "use strict";
- if (typeof define === "function" && define.amd) {
- define(function() {
- return qq;
- });
- } else if (typeof module !== "undefined" && module.exports) {
- module.exports = qq;
- } else {
- global.qq = qq;
}
- })();
- (function() {
- "use strict";
- qq.Error = function(message) {
- this.message = "[Fine Uploader " + qq.version + "] " + message;
- };
- qq.Error.prototype = new Error();
- })();
- qq.version = "5.15.4";
- qq.supportedFeatures = function() {
- "use strict";
- var supportsUploading, supportsUploadingBlobs, supportsFileDrop, supportsAjaxFileUploading, supportsFolderDrop, supportsChunking, supportsResume, supportsUploadViaPaste, supportsUploadCors, supportsDeleteFileXdr, supportsDeleteFileCorsXhr, supportsDeleteFileCors, supportsFolderSelection, supportsImagePreviews, supportsUploadProgress;
- function testSupportsFileInputElement() {
- var supported = true, tempInput;
- try {
- tempInput = document.createElement("input");
- tempInput.type = "file";
- qq(tempInput).hide();
- if (tempInput.disabled) {
- supported = false;
- }
- } catch (ex) {
- supported = false;
- }
- return supported;
- }
- function isChrome21OrHigher() {
- return (qq.chrome() || qq.opera()) && navigator.userAgent.match(/Chrome\/[2][1-9]|Chrome\/[3-9][0-9]/) !== undefined;
- }
- function isChrome14OrHigher() {
- return (qq.chrome() || qq.opera()) && navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined;
- }
- function isCrossOriginXhrSupported() {
- if (window.XMLHttpRequest) {
- var xhr = qq.createXhrInstance();
- return xhr.withCredentials !== undefined;
- }
- return false;
- }
- function isXdrSupported() {
- return window.XDomainRequest !== undefined;
- }
- function isCrossOriginAjaxSupported() {
- if (isCrossOriginXhrSupported()) {
+ }
+};
+
+qq.isObject = function(variable) {
+ "use strict";
+ return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object;
+};
+
+qq.isFunction = function(variable) {
+ "use strict";
+ return typeof(variable) === "function";
+};
+
+qq.trimStr = function(string) {
+ if (String.prototype.trim) {
+ return string.trim();
+ }
+
+ return string.replace(/^\s+|\s+$/g,'');
+};
+
+qq.isFileOrInput = function(maybeFileOrInput) {
+ "use strict";
+ if (qq.isBlob(maybeFileOrInput) && window.File && maybeFileOrInput instanceof File) {
+ return true;
+ }
+ else if (window.HTMLInputElement) {
+ if (maybeFileOrInput instanceof HTMLInputElement) {
+ if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
return true;
}
- return isXdrSupported();
}
- function isFolderSelectionSupported() {
- return document.createElement("input").webkitdirectory !== undefined;
- }
- function isLocalStorageSupported() {
- try {
- return !!window.localStorage && qq.isFunction(window.localStorage.setItem);
- } catch (error) {
- return false;
+ }
+ else if (maybeFileOrInput.tagName) {
+ if (maybeFileOrInput.tagName.toLowerCase() === 'input') {
+ if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') {
+ return true;
}
}
- function isDragAndDropSupported() {
- var span = document.createElement("span");
- return ("draggable" in span || "ondragstart" in span && "ondrop" in span) && !qq.android() && !qq.ios();
- }
- supportsUploading = testSupportsFileInputElement();
- supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported();
- supportsUploadingBlobs = supportsAjaxFileUploading && !qq.androidStock();
- supportsFileDrop = supportsAjaxFileUploading && isDragAndDropSupported();
- supportsFolderDrop = supportsFileDrop && isChrome21OrHigher();
- supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported();
- supportsResume = supportsAjaxFileUploading && supportsChunking && isLocalStorageSupported();
- supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher();
- supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading);
- supportsDeleteFileCorsXhr = isCrossOriginXhrSupported();
- supportsDeleteFileXdr = isXdrSupported();
- supportsDeleteFileCors = isCrossOriginAjaxSupported();
- supportsFolderSelection = isFolderSelectionSupported();
- supportsImagePreviews = supportsAjaxFileUploading && window.FileReader !== undefined;
- supportsUploadProgress = function() {
- if (supportsAjaxFileUploading) {
- return !qq.androidStock() && !qq.iosChrome();
+ }
+
+ return false;
+};
+
+qq.isBlob = function(maybeBlob) {
+ "use strict";
+ return window.Blob && maybeBlob instanceof Blob;
+};
+
+qq.isXhrUploadSupported = function() {
+ "use strict";
+ var input = document.createElement('input');
+ input.type = 'file';
+
+ return (
+ input.multiple !== undefined &&
+ typeof File !== "undefined" &&
+ typeof FormData !== "undefined" &&
+ typeof (new XMLHttpRequest()).upload !== "undefined" );
+};
+
+qq.isFolderDropSupported = function(dataTransfer) {
+ "use strict";
+ return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry);
+};
+
+qq.isFileChunkingSupported = function() {
+ "use strict";
+ return !qq.android() && //android's impl of Blob.slice is broken
+ qq.isXhrUploadSupported() &&
+ (File.prototype.slice || File.prototype.webkitSlice || File.prototype.mozSlice);
+};
+
+qq.extend = function (first, second, extendNested) {
+ "use strict";
+ qq.each(second, function(prop, val) {
+ if (extendNested && qq.isObject(val)) {
+ if (first[prop] === undefined) {
+ first[prop] = {};
+ }
+ qq.extend(first[prop], val, true);
+ }
+ else {
+ first[prop] = val;
+ }
+ });
+};
+
+/**
+ * Searches for a given element in the array, returns -1 if it is not present.
+ * @param {Number} [from] The index at which to begin the search
+ */
+qq.indexOf = function(arr, elt, from){
+ "use strict";
+
+ if (arr.indexOf) {
+ return arr.indexOf(elt, from);
+ }
+
+ from = from || 0;
+ var len = arr.length;
+
+ if (from < 0) {
+ from += len;
+ }
+
+ for (; from < len; from+=1){
+ if (arr.hasOwnProperty(from) && arr[from] === elt){
+ return from;
+ }
+ }
+ return -1;
+};
+
+//this is a version 4 UUID
+qq.getUniqueId = function(){
+ "use strict";
+
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ /*jslint eqeq: true, bitwise: true*/
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+ return v.toString(16);
+ });
+};
+
+//
+// Browsers and platforms detection
+
+qq.ie = function(){
+ "use strict";
+ return navigator.userAgent.indexOf('MSIE') !== -1;
+};
+qq.ie10 = function(){
+ "use strict";
+ return navigator.userAgent.indexOf('MSIE 10') !== -1;
+};
+qq.safari = function(){
+ "use strict";
+ return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1;
+};
+qq.chrome = function(){
+ "use strict";
+ return navigator.vendor !== undefined && navigator.vendor.indexOf('Google') !== -1;
+};
+qq.firefox = function(){
+ "use strict";
+ return (navigator.userAgent.indexOf('Mozilla') !== -1 && navigator.vendor !== undefined && navigator.vendor === '');
+};
+qq.windows = function(){
+ "use strict";
+ return navigator.platform === "Win32";
+};
+qq.android = function(){
+ "use strict";
+ return navigator.userAgent.toLowerCase().indexOf('android') !== -1;
+};
+
+//
+// Events
+
+qq.preventDefault = function(e){
+ "use strict";
+ if (e.preventDefault){
+ e.preventDefault();
+ } else{
+ e.returnValue = false;
+ }
+};
+
+/**
+ * Creates and returns element from html string
+ * Uses innerHTML to create an element
+ */
+qq.toElement = (function(){
+ "use strict";
+ var div = document.createElement('div');
+ return function(html){
+ div.innerHTML = html;
+ var element = div.firstChild;
+ div.removeChild(element);
+ return element;
+ };
+}());
+
+//key and value are passed to callback for each item in the object or array
+qq.each = function(obj, callback) {
+ "use strict";
+ var key, retVal;
+ if (obj) {
+ for (key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ retVal = callback(key, obj[key]);
+ if (retVal === false) {
+ break;
+ }
+ }
+ }
+ }
+};
+
+/**
+ * obj2url() takes a json-object as argument and generates
+ * a querystring. pretty much like jQuery.param()
+ *
+ * how to use:
+ *
+ * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
+ *
+ * will result in:
+ *
+ * `http://any.url/upload?otherParam=value&a=b&c=d`
+ *
+ * @param Object JSON-Object
+ * @param String current querystring-part
+ * @return String encoded querystring
+ */
+qq.obj2url = function(obj, temp, prefixDone){
+ "use strict";
+ /*jshint laxbreak: true*/
+ var i, len,
+ uristrings = [],
+ prefix = '&',
+ add = function(nextObj, i){
+ var nextTemp = temp
+ ? (/\[\]$/.test(temp)) // prevent double-encoding
+ ? temp
+ : temp+'['+i+']'
+ : i;
+ if ((nextTemp !== 'undefined') && (i !== 'undefined')) {
+ uristrings.push(
+ (typeof nextObj === 'object')
+ ? qq.obj2url(nextObj, nextTemp, true)
+ : (Object.prototype.toString.call(nextObj) === '[object Function]')
+ ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
+ : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
+ );
}
- return false;
- }();
- return {
- ajaxUploading: supportsAjaxFileUploading,
- blobUploading: supportsUploadingBlobs,
- canDetermineSize: supportsAjaxFileUploading,
- chunking: supportsChunking,
- deleteFileCors: supportsDeleteFileCors,
- deleteFileCorsXdr: supportsDeleteFileXdr,
- deleteFileCorsXhr: supportsDeleteFileCorsXhr,
- dialogElement: !!window.HTMLDialogElement,
- fileDrop: supportsFileDrop,
- folderDrop: supportsFolderDrop,
- folderSelection: supportsFolderSelection,
- imagePreviews: supportsImagePreviews,
- imageValidation: supportsImagePreviews,
- itemSizeValidation: supportsAjaxFileUploading,
- pause: supportsChunking,
- progressBar: supportsUploadProgress,
- resume: supportsResume,
- scaling: supportsImagePreviews && supportsUploadingBlobs,
- tiffPreviews: qq.safari(),
- unlimitedScaledImageSize: !qq.ios(),
- uploading: supportsUploading,
- uploadCors: supportsUploadCors,
- uploadCustomHeaders: supportsAjaxFileUploading,
- uploadNonMultipart: supportsAjaxFileUploading,
- uploadViaPaste: supportsUploadViaPaste
};
- }();
- qq.isGenericPromise = function(maybePromise) {
- "use strict";
- return !!(maybePromise && maybePromise.then && qq.isFunction(maybePromise.then));
- };
- qq.Promise = function() {
- "use strict";
- var successArgs, failureArgs, successCallbacks = [], failureCallbacks = [], doneCallbacks = [], state = 0;
- qq.extend(this, {
- then: function(onSuccess, onFailure) {
- if (state === 0) {
- if (onSuccess) {
- successCallbacks.push(onSuccess);
- }
- if (onFailure) {
- failureCallbacks.push(onFailure);
- }
- } else if (state === -1) {
- onFailure && onFailure.apply(null, failureArgs);
- } else if (onSuccess) {
- onSuccess.apply(null, successArgs);
- }
- return this;
- },
- done: function(callback) {
- if (state === 0) {
- doneCallbacks.push(callback);
- } else {
- callback.apply(null, failureArgs === undefined ? successArgs : failureArgs);
- }
- return this;
- },
- success: function() {
- state = 1;
- successArgs = arguments;
- if (successCallbacks.length) {
- qq.each(successCallbacks, function(idx, callback) {
- callback.apply(null, successArgs);
- });
- }
- if (doneCallbacks.length) {
- qq.each(doneCallbacks, function(idx, callback) {
- callback.apply(null, successArgs);
- });
- }
- return this;
- },
- failure: function() {
- state = -1;
- failureArgs = arguments;
- if (failureCallbacks.length) {
- qq.each(failureCallbacks, function(idx, callback) {
- callback.apply(null, failureArgs);
- });
- }
- if (doneCallbacks.length) {
- qq.each(doneCallbacks, function(idx, callback) {
- callback.apply(null, failureArgs);
- });
- }
- return this;
- }
- });
- };
- qq.BlobProxy = function(referenceBlob, onCreate) {
- "use strict";
- qq.extend(this, {
- referenceBlob: referenceBlob,
- create: function() {
- return onCreate(referenceBlob);
- }
- });
- };
- qq.UploadButton = function(o) {
- "use strict";
- var self = this, disposeSupport = new qq.DisposeSupport(), options = {
- acceptFiles: null,
- element: null,
- focusClass: "qq-upload-button-focus",
- folders: false,
- hoverClass: "qq-upload-button-hover",
- ios8BrowserCrashWorkaround: false,
- multiple: false,
- name: "qqfile",
- onChange: function(input) {},
- title: null
- }, input, buttonId;
- qq.extend(options, o);
- buttonId = qq.getUniqueId();
- function createInput() {
- var input = document.createElement("input");
- input.setAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME, buttonId);
- input.setAttribute("title", options.title);
- self.setMultiple(options.multiple, input);
- if (options.folders && qq.supportedFeatures.folderSelection) {
- input.setAttribute("webkitdirectory", "");
- }
- if (options.acceptFiles) {
- input.setAttribute("accept", options.acceptFiles);
- }
- input.setAttribute("type", "file");
- input.setAttribute("name", options.name);
- qq(input).css({
- position: "absolute",
- right: 0,
- top: 0,
- fontFamily: "Arial",
- fontSize: qq.ie() && !qq.ie8() ? "3500px" : "118px",
- margin: 0,
- padding: 0,
- cursor: "pointer",
- opacity: 0
- });
- !qq.ie7() && qq(input).css({
- height: "100%"
- });
- options.element.appendChild(input);
- disposeSupport.attach(input, "change", function() {
- options.onChange(input);
- });
- disposeSupport.attach(input, "mouseover", function() {
- qq(options.element).addClass(options.hoverClass);
- });
- disposeSupport.attach(input, "mouseout", function() {
- qq(options.element).removeClass(options.hoverClass);
- });
- disposeSupport.attach(input, "focus", function() {
- qq(options.element).addClass(options.focusClass);
- });
- disposeSupport.attach(input, "blur", function() {
- qq(options.element).removeClass(options.focusClass);
- });
- return input;
+
+ if (!prefixDone && temp) {
+ prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
+ uristrings.push(temp);
+ uristrings.push(qq.obj2url(obj));
+ } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj !== 'undefined') ) {
+ // we wont use a for-in-loop on an array (performance)
+ for (i = -1, len = obj.length; i < len; i+=1){
+ add(obj[i], i);
}
- qq(options.element).css({
- position: "relative",
- overflow: "hidden",
- direction: "ltr"
- });
- qq.extend(this, {
- getInput: function() {
- return input;
- },
- getButtonId: function() {
- return buttonId;
- },
- setMultiple: function(isMultiple, optInput) {
- var input = optInput || this.getInput();
- if (options.ios8BrowserCrashWorkaround && qq.ios8() && (qq.iosChrome() || qq.iosSafariWebView())) {
- input.setAttribute("multiple", "");
- } else {
- if (isMultiple) {
- input.setAttribute("multiple", "");
- } else {
- input.removeAttribute("multiple");
- }
- }
- },
- setAcceptFiles: function(acceptFiles) {
- if (acceptFiles !== options.acceptFiles) {
- input.setAttribute("accept", acceptFiles);
- }
- },
- reset: function() {
- if (input.parentNode) {
- qq(input).remove();
- }
- qq(options.element).removeClass(options.focusClass);
- input = null;
- input = createInput();
+ } else if ((typeof obj !== 'undefined') && (obj !== null) && (typeof obj === "object")){
+ // for anything else but a scalar, we will use for-in-loop
+ for (i in obj){
+ if (obj.hasOwnProperty(i)) {
+ add(obj[i], i);
}
- });
- input = createInput();
+ }
+ } else {
+ uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
+ }
+
+ if (temp) {
+ return uristrings.join(prefix);
+ } else {
+ return uristrings.join(prefix)
+ .replace(/^&/, '')
+ .replace(/%20/g, '+');
+ }
+};
+
+qq.obj2FormData = function(obj, formData, arrayKeyName) {
+ "use strict";
+ if (!formData) {
+ formData = new FormData();
+ }
+
+ qq.each(obj, function(key, val) {
+ key = arrayKeyName ? arrayKeyName + '[' + key + ']' : key;
+
+ if (qq.isObject(val)) {
+ qq.obj2FormData(val, formData, key);
+ }
+ else if (qq.isFunction(val)) {
+ formData.append(key, val());
+ }
+ else {
+ formData.append(key, val);
+ }
+ });
+
+ return formData;
+};
+
+qq.obj2Inputs = function(obj, form) {
+ "use strict";
+ var input;
+
+ if (!form) {
+ form = document.createElement('form');
+ }
+
+ qq.obj2FormData(obj, {
+ append: function(key, val) {
+ input = document.createElement('input');
+ input.setAttribute('name', key);
+ input.setAttribute('value', val);
+ form.appendChild(input);
+ }
+ });
+
+ return form;
+};
+
+qq.setCookie = function(name, value, days) {
+ var date = new Date(),
+ expires = "";
+
+ if (days) {
+ date.setTime(date.getTime()+(days*24*60*60*1000));
+ expires = "; expires="+date.toGMTString();
+ }
+
+ document.cookie = name+"="+value+expires+"; path=/";
+};
+
+qq.getCookie = function(name) {
+ var nameEQ = name + "=",
+ ca = document.cookie.split(';'),
+ c;
+
+ for(var i=0;i < ca.length;i++) {
+ c = ca[i];
+ while (c.charAt(0)==' ') {
+ c = c.substring(1,c.length);
+ }
+ if (c.indexOf(nameEQ) === 0) {
+ return c.substring(nameEQ.length,c.length);
+ }
+ }
+};
+
+qq.getCookieNames = function(regexp) {
+ var cookies = document.cookie.split(';'),
+ cookieNames = [];
+
+ qq.each(cookies, function(idx, cookie) {
+ cookie = qq.trimStr(cookie);
+
+ var equalsIdx = cookie.indexOf("=");
+
+ if (cookie.match(regexp)) {
+ cookieNames.push(cookie.substr(0, equalsIdx));
+ }
+ });
+
+ return cookieNames;
+};
+
+qq.deleteCookie = function(name) {
+ qq.setCookie(name, "", -1);
+};
+
+qq.areCookiesEnabled = function() {
+ var randNum = Math.random() * 100000,
+ name = "qqCookieTest:" + randNum;
+ qq.setCookie(name, 1);
+
+ if (qq.getCookie(name)) {
+ qq.deleteCookie(name);
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not
+ * implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js.
+ */
+qq.parseJson = function(json) {
+ /*jshint evil: true*/
+ if (window.JSON && qq.isFunction(JSON.parse)) {
+ return JSON.parse(json);
+ } else {
+ return eval("(" + json + ")");
+ }
+};
+
+/**
+ * A generic module which supports object disposing in dispose() method.
+ * */
+qq.DisposeSupport = function() {
+ "use strict";
+ var disposers = [];
+
+ return {
+ /** Run all registered disposers */
+ dispose: function() {
+ var disposer;
+ do {
+ disposer = disposers.shift();
+ if (disposer) {
+ disposer();
+ }
+ }
+ while (disposer);
+ },
+
+ /** Attach event handler and register de-attacher as a disposer */
+ attach: function() {
+ var args = arguments;
+ /*jslint undef:true*/
+ this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
+ },
+
+ /** Add disposer to the collection */
+ addDisposer: function(disposeFunction) {
+ disposers.push(disposeFunction);
+ }
};
- qq.UploadButton.BUTTON_ID_ATTR_NAME = "qq-button-id";
- qq.UploadData = function(uploaderProxy) {
+};
+qq.UploadButton = function(o){
+ this._options = {
+ element: null,
+ // if set to true adds multiple attribute to file input
+ multiple: false,
+ acceptFiles: null,
+ // name attribute of file input
+ name: 'file',
+ onChange: function(input){},
+ hoverClass: 'qq-upload-button-hover',
+ focusClass: 'qq-upload-button-focus'
+ };
+
+ qq.extend(this._options, o);
+ this._disposeSupport = new qq.DisposeSupport();
+
+ this._element = this._options.element;
+
+ // make button suitable container for input
+ qq(this._element).css({
+ position: 'relative',
+ overflow: 'hidden',
+ // Make sure browse button is in the right side
+ // in Internet Explorer
+ direction: 'ltr'
+ });
+
+ this._input = this._createInput();
+};
+
+qq.UploadButton.prototype = {
+ /* returns file input element */
+ getInput: function(){
+ return this._input;
+ },
+ /* cleans/recreates the file input */
+ reset: function(){
+ if (this._input.parentNode){
+ qq(this._input).remove();
+ }
+
+ qq(this._element).removeClass(this._options.focusClass);
+ this._input = this._createInput();
+ },
+ _createInput: function(){
+ var input = document.createElement("input");
+
+ if (this._options.multiple){
+ input.setAttribute("multiple", "multiple");
+ }
+
+ if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles);
+
+ input.setAttribute("type", "file");
+ input.setAttribute("name", this._options.name);
+
+ qq(input).css({
+ position: 'absolute',
+ // in Opera only 'browse' button
+ // is clickable and it is located at
+ // the right side of the input
+ right: 0,
+ top: 0,
+ fontFamily: 'Arial',
+ // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
+ fontSize: '118px',
+ margin: 0,
+ padding: 0,
+ cursor: 'pointer',
+ opacity: 0
+ });
+
+ this._element.appendChild(input);
+
+ var self = this;
+ this._disposeSupport.attach(input, 'change', function(){
+ self._options.onChange(input);
+ });
+
+ this._disposeSupport.attach(input, 'mouseover', function(){
+ qq(self._element).addClass(self._options.hoverClass);
+ });
+ this._disposeSupport.attach(input, 'mouseout', function(){
+ qq(self._element).removeClass(self._options.hoverClass);
+ });
+ this._disposeSupport.attach(input, 'focus', function(){
+ qq(self._element).addClass(self._options.focusClass);
+ });
+ this._disposeSupport.attach(input, 'blur', function(){
+ qq(self._element).removeClass(self._options.focusClass);
+ });
+
+ // IE and Opera, unfortunately have 2 tab stops on file input
+ // which is unacceptable in our case, disable keyboard access
+ if (window.attachEvent){
+ // it is IE or Opera
+ input.setAttribute('tabIndex', "-1");
+ }
+
+ return input;
+ }
+};
+qq.FineUploaderBasic = function(o){
+ var that = this;
+ this._options = {
+ debug: false,
+ button: null,
+ multiple: true,
+ maxConnections: 3,
+ disableCancelForFormUploads: false,
+ autoUpload: true,
+ request: {
+ endpoint: '/server/upload',
+ params: {},
+ paramsInBody: true,
+ customHeaders: {},
+ forceMultipart: true,
+ inputName: 'qqfile',
+ uuidName: 'qquuid',
+ totalFileSizeName: 'qqtotalfilesize'
+ },
+ validation: {
+ allowedExtensions: [],
+ sizeLimit: 0,
+ minSizeLimit: 0,
+ stopOnFirstInvalidFile: true
+ },
+ callbacks: {
+ onSubmit: function(id, name){},
+ onComplete: function(id, name, responseJSON){},
+ onCancel: function(id, name){},
+ onUpload: function(id, name){},
+ onUploadChunk: function(id, name, chunkData){},
+ onResume: function(id, fileName, chunkData){},
+ onProgress: function(id, name, loaded, total){},
+ onError: function(id, name, reason) {},
+ onAutoRetry: function(id, name, attemptNumber) {},
+ onManualRetry: function(id, name) {},
+ onValidateBatch: function(fileOrBlobData) {},
+ onValidate: function(fileOrBlobData) {},
+ onSubmitDelete: function(id) {},
+ onDelete: function(id){},
+ onDeleteComplete: function(id, xhr, isError){}
+ },
+ messages: {
+ typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
+ sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
+ minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
+ emptyError: "{file} is empty, please select files again without it.",
+ noFilesError: "No files to upload.",
+ onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
+ },
+ retry: {
+ enableAuto: false,
+ maxAutoAttempts: 3,
+ autoAttemptDelay: 5,
+ preventRetryResponseProperty: 'preventRetry'
+ },
+ classes: {
+ buttonHover: 'qq-upload-button-hover',
+ buttonFocus: 'qq-upload-button-focus'
+ },
+ chunking: {
+ enabled: false,
+ partSize: 2000000,
+ paramNames: {
+ partIndex: 'qqpartindex',
+ partByteOffset: 'qqpartbyteoffset',
+ chunkSize: 'qqchunksize',
+ totalFileSize: 'qqtotalfilesize',
+ totalParts: 'qqtotalparts',
+ filename: 'qqfilename'
+ }
+ },
+ resume: {
+ enabled: false,
+ id: null,
+ cookiesExpireIn: 7, //days
+ paramNames: {
+ resuming: "qqresume"
+ }
+ },
+ formatFileName: function(fileOrBlobName) {
+ if (fileOrBlobName.length > 33) {
+ fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14);
+ }
+ return fileOrBlobName;
+ },
+ text: {
+ sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB']
+ },
+ deleteFile : {
+ enabled: false,
+ endpoint: '/server/upload',
+ customHeaders: {},
+ params: {}
+ },
+ cors: {
+ expected: false,
+ sendCredentials: false
+ },
+ blobs: {
+ defaultName: 'Misc data',
+ paramNames: {
+ name: 'qqblobname'
+ }
+ }
+ };
+
+ qq.extend(this._options, o, true);
+ this._wrapCallbacks();
+ this._disposeSupport = new qq.DisposeSupport();
+
+ // number of files being uploaded
+ this._filesInProgress = [];
+
+ this._storedIds = [];
+
+ this._autoRetries = [];
+ this._retryTimeouts = [];
+ this._preventRetries = [];
+
+ this._paramsStore = this._createParamsStore("request");
+ this._deleteFileParamsStore = this._createParamsStore("deleteFile");
+
+ this._endpointStore = this._createEndpointStore("request");
+ this._deleteFileEndpointStore = this._createEndpointStore("deleteFile");
+
+ this._handler = this._createUploadHandler();
+ this._deleteHandler = this._createDeleteHandler();
+
+ if (this._options.button){
+ this._button = this._createUploadButton(this._options.button);
+ }
+
+ this._preventLeaveInProgress();
+};
+
+qq.FineUploaderBasic.prototype = {
+ log: function(str, level) {
+ if (this._options.debug && (!level || level === 'info')) {
+ qq.log('[FineUploader] ' + str);
+ }
+ else if (level && level !== 'info') {
+ qq.log('[FineUploader] ' + str, level);
+
+ }
+ },
+ setParams: function(params, id) {
+ /*jshint eqeqeq: true, eqnull: true*/
+ if (id == null) {
+ this._options.request.params = params;
+ }
+ else {
+ this._paramsStore.setParams(params, id);
+ }
+ },
+ setDeleteFileParams: function(params, id) {
+ /*jshint eqeqeq: true, eqnull: true*/
+ if (id == null) {
+ this._options.deleteFile.params = params;
+ }
+ else {
+ this._deleteFileParamsStore.setParams(params, id);
+ }
+ },
+ setEndpoint: function(endpoint, id) {
+ /*jshint eqeqeq: true, eqnull: true*/
+ if (id == null) {
+ this._options.request.endpoint = endpoint;
+ }
+ else {
+ this._endpointStore.setEndpoint(endpoint, id);
+ }
+ },
+ getInProgress: function(){
+ return this._filesInProgress.length;
+ },
+ uploadStoredFiles: function(){
"use strict";
- var data = [], byUuid = {}, byStatus = {}, byProxyGroupId = {}, byBatchId = {};
- function getDataByIds(idOrIds) {
- if (qq.isArray(idOrIds)) {
- var entries = [];
- qq.each(idOrIds, function(idx, id) {
- entries.push(data[id]);
- });
- return entries;
- }
- return data[idOrIds];
+ var idToUpload;
+
+ while(this._storedIds.length) {
+ idToUpload = this._storedIds.shift();
+ this._filesInProgress.push(idToUpload);
+ this._handler.upload(idToUpload);
}
- function getDataByUuids(uuids) {
- if (qq.isArray(uuids)) {
- var entries = [];
- qq.each(uuids, function(idx, uuid) {
- entries.push(data[byUuid[uuid]]);
- });
- return entries;
- }
- return data[byUuid[uuids]];
+ },
+ clearStoredFiles: function(){
+ this._storedIds = [];
+ },
+ retry: function(id) {
+ if (this._onBeforeManualRetry(id)) {
+ this._handler.retry(id);
+ return true;
}
- function getDataByStatus(status) {
- var statusResults = [], statuses = [].concat(status);
- qq.each(statuses, function(index, statusEnum) {
- var statusResultIndexes = byStatus[statusEnum];
- if (statusResultIndexes !== undefined) {
- qq.each(statusResultIndexes, function(i, dataIndex) {
- statusResults.push(data[dataIndex]);
+ else {
+ return false;
+ }
+ },
+ cancel: function(id) {
+ this._handler.cancel(id);
+ },
+ cancelAll: function() {
+ var storedIdsCopy = [],
+ self = this;
+
+ qq.extend(storedIdsCopy, this._storedIds);
+ qq.each(storedIdsCopy, function(idx, storedFileId) {
+ self.cancel(storedFileId);
+ });
+
+ this._handler.cancelAll();
+ },
+ reset: function() {
+ this.log("Resetting uploader...");
+ this._handler.reset();
+ this._filesInProgress = [];
+ this._storedIds = [];
+ this._autoRetries = [];
+ this._retryTimeouts = [];
+ this._preventRetries = [];
+ this._button.reset();
+ this._paramsStore.reset();
+ this._endpointStore.reset();
+ },
+ addFiles: function(filesBlobDataOrInputs) {
+ var self = this,
+ verifiedFilesOrInputs = [],
+ index, fileOrInput;
+
+ if (filesBlobDataOrInputs) {
+ if (!window.FileList || !(filesBlobDataOrInputs instanceof FileList)) {
+ filesBlobDataOrInputs = [].concat(filesBlobDataOrInputs);
+ }
+
+ for (index = 0; index < filesBlobDataOrInputs.length; index+=1) {
+ fileOrInput = filesBlobDataOrInputs[index];
+
+ if (qq.isFileOrInput(fileOrInput)) {
+ verifiedFilesOrInputs.push(fileOrInput);
+ }
+ else {
+ self.log(fileOrInput + ' is not a File or INPUT element! Ignoring!', 'warn');
+ }
+ }
+
+ this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...');
+ this._uploadFileOrBlobDataList(verifiedFilesOrInputs);
+ }
+ },
+ addBlobs: function(blobDataOrArray) {
+ if (blobDataOrArray) {
+ var blobDataArray = [].concat(blobDataOrArray),
+ verifiedBlobDataList = [],
+ self = this;
+
+ qq.each(blobDataArray, function(idx, blobData) {
+ if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) {
+ verifiedBlobDataList.push({
+ blob: blobData,
+ name: self._options.blobs.defaultName
});
}
+ else if (qq.isObject(blobData) && blobData.blob && blobData.name) {
+ verifiedBlobDataList.push(blobData);
+ }
+ else {
+ self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error");
+ }
});
- return statusResults;
+
+ this._uploadFileOrBlobDataList(verifiedBlobDataList);
}
- qq.extend(this, {
- addFile: function(spec) {
- var status = spec.status || qq.status.SUBMITTING, id = data.push({
- name: spec.name,
- originalName: spec.name,
- uuid: spec.uuid,
- size: spec.size == null ? -1 : spec.size,
- status: status
- }) - 1;
- if (spec.batchId) {
- data[id].batchId = spec.batchId;
- if (byBatchId[spec.batchId] === undefined) {
- byBatchId[spec.batchId] = [];
- }
- byBatchId[spec.batchId].push(id);
- }
- if (spec.proxyGroupId) {
- data[id].proxyGroupId = spec.proxyGroupId;
- if (byProxyGroupId[spec.proxyGroupId] === undefined) {
- byProxyGroupId[spec.proxyGroupId] = [];
- }
- byProxyGroupId[spec.proxyGroupId].push(id);
- }
- data[id].id = id;
- byUuid[spec.uuid] = id;
- if (byStatus[status] === undefined) {
- byStatus[status] = [];
- }
- byStatus[status].push(id);
- spec.onBeforeStatusChange && spec.onBeforeStatusChange(id);
- uploaderProxy.onStatusChange(id, null, status);
- return id;
+ else {
+ this.log("undefined or non-array parameter passed into addBlobs", "error");
+ }
+ },
+ getUuid: function(id) {
+ return this._handler.getUuid(id);
+ },
+ getResumableFilesData: function() {
+ return this._handler.getResumableFilesData();
+ },
+ getSize: function(id) {
+ return this._handler.getSize(id);
+ },
+ getFile: function(fileOrBlobId) {
+ return this._handler.getFile(fileOrBlobId);
+ },
+ deleteFile: function(id) {
+ this._onSubmitDelete(id);
+ },
+ setDeleteFileEndpoint: function(endpoint, id) {
+ /*jshint eqeqeq: true, eqnull: true*/
+ if (id == null) {
+ this._options.deleteFile.endpoint = endpoint;
+ }
+ else {
+ this._deleteFileEndpointStore.setEndpoint(endpoint, id);
+ }
+ },
+ _createUploadButton: function(element){
+ var self = this;
+
+ var button = new qq.UploadButton({
+ element: element,
+ multiple: this._options.multiple && qq.isXhrUploadSupported(),
+ acceptFiles: this._options.validation.acceptFiles,
+ onChange: function(input){
+ self._onInputChange(input);
},
- retrieve: function(optionalFilter) {
- if (qq.isObject(optionalFilter) && data.length) {
- if (optionalFilter.id !== undefined) {
- return getDataByIds(optionalFilter.id);
- } else if (optionalFilter.uuid !== undefined) {
- return getDataByUuids(optionalFilter.uuid);
- } else if (optionalFilter.status) {
- return getDataByStatus(optionalFilter.status);
- }
- } else {
- return qq.extend([], data, true);
- }
- },
- reset: function() {
- data = [];
- byUuid = {};
- byStatus = {};
- byBatchId = {};
- },
- setStatus: function(id, newStatus) {
- var oldStatus = data[id].status, byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], id);
- byStatus[oldStatus].splice(byStatusOldStatusIndex, 1);
- data[id].status = newStatus;
- if (byStatus[newStatus] === undefined) {
- byStatus[newStatus] = [];
- }
- byStatus[newStatus].push(id);
- uploaderProxy.onStatusChange(id, oldStatus, newStatus);
- },
- uuidChanged: function(id, newUuid) {
- var oldUuid = data[id].uuid;
- data[id].uuid = newUuid;
- byUuid[newUuid] = id;
- delete byUuid[oldUuid];
- },
- updateName: function(id, newName) {
- data[id].name = newName;
- },
- updateSize: function(id, newSize) {
- data[id].size = newSize;
- },
- setParentId: function(targetId, parentId) {
- data[targetId].parentId = parentId;
- },
- getIdsInProxyGroup: function(id) {
- var proxyGroupId = data[id].proxyGroupId;
- if (proxyGroupId) {
- return byProxyGroupId[proxyGroupId];
- }
- return [];
- },
- getIdsInBatch: function(id) {
- var batchId = data[id].batchId;
- return byBatchId[batchId];
- }
+ hoverClass: this._options.classes.buttonHover,
+ focusClass: this._options.classes.buttonFocus
});
- };
- qq.status = {
- SUBMITTING: "submitting",
- SUBMITTED: "submitted",
- REJECTED: "rejected",
- QUEUED: "queued",
- CANCELED: "canceled",
- PAUSED: "paused",
- UPLOADING: "uploading",
- UPLOAD_RETRYING: "retrying upload",
- UPLOAD_SUCCESSFUL: "upload successful",
- UPLOAD_FAILED: "upload failed",
- DELETE_FAILED: "delete failed",
- DELETING: "deleting",
- DELETED: "deleted"
- };
- (function() {
- "use strict";
- qq.basePublicApi = {
- addBlobs: function(blobDataOrArray, params, endpoint) {
- this.addFiles(blobDataOrArray, params, endpoint);
- },
- addInitialFiles: function(cannedFileList) {
- var self = this;
- qq.each(cannedFileList, function(index, cannedFile) {
- self._addCannedFile(cannedFile);
- });
- },
- addFiles: function(data, params, endpoint) {
- this._maybeHandleIos8SafariWorkaround();
- var batchId = this._storedIds.length === 0 ? qq.getUniqueId() : this._currentBatchId, processBlob = qq.bind(function(blob) {
- this._handleNewFile({
- blob: blob,
- name: this._options.blobs.defaultName
- }, batchId, verifiedFiles);
- }, this), processBlobData = qq.bind(function(blobData) {
- this._handleNewFile(blobData, batchId, verifiedFiles);
- }, this), processCanvas = qq.bind(function(canvas) {
- var blob = qq.canvasToBlob(canvas);
- this._handleNewFile({
- blob: blob,
- name: this._options.blobs.defaultName + ".png"
- }, batchId, verifiedFiles);
- }, this), processCanvasData = qq.bind(function(canvasData) {
- var normalizedQuality = canvasData.quality && canvasData.quality / 100, blob = qq.canvasToBlob(canvasData.canvas, canvasData.type, normalizedQuality);
- this._handleNewFile({
- blob: blob,
- name: canvasData.name
- }, batchId, verifiedFiles);
- }, this), processFileOrInput = qq.bind(function(fileOrInput) {
- if (qq.isInput(fileOrInput) && qq.supportedFeatures.ajaxUploading) {
- var files = Array.prototype.slice.call(fileOrInput.files), self = this;
- qq.each(files, function(idx, file) {
- self._handleNewFile(file, batchId, verifiedFiles);
- });
- } else {
- this._handleNewFile(fileOrInput, batchId, verifiedFiles);
- }
- }, this), normalizeData = function() {
- if (qq.isFileList(data)) {
- data = Array.prototype.slice.call(data);
- }
- data = [].concat(data);
- }, self = this, verifiedFiles = [];
- this._currentBatchId = batchId;
- if (data) {
- normalizeData();
- qq.each(data, function(idx, fileContainer) {
- if (qq.isFileOrInput(fileContainer)) {
- processFileOrInput(fileContainer);
- } else if (qq.isBlob(fileContainer)) {
- processBlob(fileContainer);
- } else if (qq.isObject(fileContainer)) {
- if (fileContainer.blob && fileContainer.name) {
- processBlobData(fileContainer);
- } else if (fileContainer.canvas && fileContainer.name) {
- processCanvasData(fileContainer);
- }
- } else if (fileContainer.tagName && fileContainer.tagName.toLowerCase() === "canvas") {
- processCanvas(fileContainer);
- } else {
- self.log(fileContainer + " is not a valid file container! Ignoring!", "warn");
- }
- });
- this.log("Received " + verifiedFiles.length + " files.");
- this._prepareItemsForUpload(verifiedFiles, params, endpoint);
- }
- },
- cancel: function(id) {
- this._handler.cancel(id);
- },
- cancelAll: function() {
- var storedIdsCopy = [], self = this;
- qq.extend(storedIdsCopy, this._storedIds);
- qq.each(storedIdsCopy, function(idx, storedFileId) {
- self.cancel(storedFileId);
- });
- this._handler.cancelAll();
- },
- clearStoredFiles: function() {
- this._storedIds = [];
- },
- continueUpload: function(id) {
- var uploadData = this._uploadData.retrieve({
- id: id
- });
- if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) {
- return false;
- }
- if (uploadData.status === qq.status.PAUSED) {
- this.log(qq.format("Paused file ID {} ({}) will be continued. Not paused.", id, this.getName(id)));
- this._uploadFile(id);
- return true;
- } else {
- this.log(qq.format("Ignoring continue for file ID {} ({}). Not paused.", id, this.getName(id)), "error");
- }
- return false;
- },
- deleteFile: function(id) {
- return this._onSubmitDelete(id);
- },
- doesExist: function(fileOrBlobId) {
- return this._handler.isValid(fileOrBlobId);
- },
- drawThumbnail: function(fileId, imgOrCanvas, maxSize, fromServer, customResizeFunction) {
- var promiseToReturn = new qq.Promise(), fileOrUrl, options;
- if (this._imageGenerator) {
- fileOrUrl = this._thumbnailUrls[fileId];
- options = {
- customResizeFunction: customResizeFunction,
- maxSize: maxSize > 0 ? maxSize : null,
- scale: maxSize > 0
- };
- if (!fromServer && qq.supportedFeatures.imagePreviews) {
- fileOrUrl = this.getFile(fileId);
- }
- if (fileOrUrl == null) {
- promiseToReturn.failure({
- container: imgOrCanvas,
- error: "File or URL not found."
- });
- } else {
- this._imageGenerator.generate(fileOrUrl, imgOrCanvas, options).then(function success(modifiedContainer) {
- promiseToReturn.success(modifiedContainer);
- }, function failure(container, reason) {
- promiseToReturn.failure({
- container: container,
- error: reason || "Problem generating thumbnail"
- });
- });
- }
- } else {
- promiseToReturn.failure({
- container: imgOrCanvas,
- error: "Missing image generator module"
- });
- }
- return promiseToReturn;
- },
- getButton: function(fileId) {
- return this._getButton(this._buttonIdsForFileIds[fileId]);
- },
- getEndpoint: function(fileId) {
- return this._endpointStore.get(fileId);
- },
- getFile: function(fileOrBlobId) {
- return this._handler.getFile(fileOrBlobId) || null;
- },
- getInProgress: function() {
- return this._uploadData.retrieve({
- status: [ qq.status.UPLOADING, qq.status.UPLOAD_RETRYING, qq.status.QUEUED ]
- }).length;
- },
- getName: function(id) {
- return this._uploadData.retrieve({
- id: id
- }).name;
- },
- getParentId: function(id) {
- var uploadDataEntry = this.getUploads({
- id: id
- }), parentId = null;
- if (uploadDataEntry) {
- if (uploadDataEntry.parentId !== undefined) {
- parentId = uploadDataEntry.parentId;
- }
- }
- return parentId;
- },
- getResumableFilesData: function() {
- return this._handler.getResumableFilesData();
- },
- getSize: function(id) {
- return this._uploadData.retrieve({
- id: id
- }).size;
- },
- getNetUploads: function() {
- return this._netUploaded;
- },
- getRemainingAllowedItems: function() {
- var allowedItems = this._currentItemLimit;
- if (allowedItems > 0) {
- return allowedItems - this._netUploadedOrQueued;
- }
- return null;
- },
- getUploads: function(optionalFilter) {
- return this._uploadData.retrieve(optionalFilter);
- },
- getUuid: function(id) {
- return this._uploadData.retrieve({
- id: id
- }).uuid;
- },
+
+ this._disposeSupport.addDisposer(function() { button.dispose(); });
+ return button;
+ },
+ _createUploadHandler: function(){
+ var self = this;
+
+ return new qq.UploadHandler({
+ debug: this._options.debug,
+ forceMultipart: this._options.request.forceMultipart,
+ maxConnections: this._options.maxConnections,
+ customHeaders: this._options.request.customHeaders,
+ inputName: this._options.request.inputName,
+ uuidParamName: this._options.request.uuidName,
+ totalFileSizeParamName: this._options.request.totalFileSizeName,
+ cors: this._options.cors,
+ demoMode: this._options.demoMode,
+ paramsInBody: this._options.request.paramsInBody,
+ paramsStore: this._paramsStore,
+ endpointStore: this._endpointStore,
+ chunking: this._options.chunking,
+ resume: this._options.resume,
+ blobs: this._options.blobs,
log: function(str, level) {
- if (this._options.debug && (!level || level === "info")) {
- qq.log("[Fine Uploader " + qq.version + "] " + str);
- } else if (level && level !== "info") {
- qq.log("[Fine Uploader " + qq.version + "] " + str, level);
- }
+ self.log(str, level);
},
- pauseUpload: function(id) {
- var uploadData = this._uploadData.retrieve({
- id: id
- });
- if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) {
+ onProgress: function(id, name, loaded, total){
+ self._onProgress(id, name, loaded, total);
+ self._options.callbacks.onProgress(id, name, loaded, total);
+ },
+ onComplete: function(id, name, result, xhr){
+ self._onComplete(id, name, result, xhr);
+ self._options.callbacks.onComplete(id, name, result);
+ },
+ onCancel: function(id, name){
+ self._onCancel(id, name);
+ self._options.callbacks.onCancel(id, name);
+ },
+ onUpload: function(id, name){
+ self._onUpload(id, name);
+ self._options.callbacks.onUpload(id, name);
+ },
+ onUploadChunk: function(id, name, chunkData){
+ self._options.callbacks.onUploadChunk(id, name, chunkData);
+ },
+ onResume: function(id, name, chunkData) {
+ return self._options.callbacks.onResume(id, name, chunkData);
+ },
+ onAutoRetry: function(id, name, responseJSON, xhr) {
+ self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
+
+ if (self._shouldAutoRetry(id, name, responseJSON)) {
+ self._maybeParseAndSendUploadError(id, name, responseJSON, xhr);
+ self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1);
+ self._onBeforeAutoRetry(id, name);
+
+ self._retryTimeouts[id] = setTimeout(function() {
+ self._onAutoRetry(id, name, responseJSON)
+ }, self._options.retry.autoAttemptDelay * 1000);
+
+ return true;
+ }
+ else {
return false;
}
- if (qq.indexOf([ qq.status.UPLOADING, qq.status.UPLOAD_RETRYING ], uploadData.status) >= 0) {
- if (this._handler.pause(id)) {
- this._uploadData.setStatus(id, qq.status.PAUSED);
- return true;
- } else {
- this.log(qq.format("Unable to pause file ID {} ({}).", id, this.getName(id)), "error");
- }
- } else {
- this.log(qq.format("Ignoring pause for file ID {} ({}). Not in progress.", id, this.getName(id)), "error");
- }
- return false;
- },
- removeFileRef: function(id) {
- this._handler.expunge(id);
- },
- reset: function() {
- this.log("Resetting uploader...");
- this._handler.reset();
- this._storedIds = [];
- this._autoRetries = [];
- this._retryTimeouts = [];
- this._preventRetries = [];
- this._thumbnailUrls = [];
- qq.each(this._buttons, function(idx, button) {
- button.reset();
- });
- this._paramsStore.reset();
- this._endpointStore.reset();
- this._netUploadedOrQueued = 0;
- this._netUploaded = 0;
- this._uploadData.reset();
- this._buttonIdsForFileIds = [];
- this._pasteHandler && this._pasteHandler.reset();
- this._options.session.refreshOnReset && this._refreshSessionData();
- this._succeededSinceLastAllComplete = [];
- this._failedSinceLastAllComplete = [];
- this._totalProgress && this._totalProgress.reset();
- },
- retry: function(id) {
- return this._manualRetry(id);
- },
- scaleImage: function(id, specs) {
- var self = this;
- return qq.Scaler.prototype.scaleImage(id, specs, {
- log: qq.bind(self.log, self),
- getFile: qq.bind(self.getFile, self),
- uploadData: self._uploadData
- });
- },
- setCustomHeaders: function(headers, id) {
- this._customHeadersStore.set(headers, id);
- },
- setDeleteFileCustomHeaders: function(headers, id) {
- this._deleteFileCustomHeadersStore.set(headers, id);
- },
- setDeleteFileEndpoint: function(endpoint, id) {
- this._deleteFileEndpointStore.set(endpoint, id);
- },
- setDeleteFileParams: function(params, id) {
- this._deleteFileParamsStore.set(params, id);
- },
- setEndpoint: function(endpoint, id) {
- this._endpointStore.set(endpoint, id);
- },
- setForm: function(elementOrId) {
- this._updateFormSupportAndParams(elementOrId);
- },
- setItemLimit: function(newItemLimit) {
- this._currentItemLimit = newItemLimit;
- },
- setName: function(id, newName) {
- this._uploadData.updateName(id, newName);
- },
- setParams: function(params, id) {
- this._paramsStore.set(params, id);
- },
- setUuid: function(id, newUuid) {
- return this._uploadData.uuidChanged(id, newUuid);
- },
- setStatus: function(id, newStatus) {
- var fileRecord = this.getUploads({
- id: id
- });
- if (!fileRecord) {
- throw new qq.Error(id + " is not a valid file ID.");
- }
- switch (newStatus) {
- case qq.status.DELETED:
- this._onDeleteComplete(id, null, false);
- break;
-
- case qq.status.DELETE_FAILED:
- this._onDeleteComplete(id, null, true);
- break;
-
- default:
- var errorMessage = "Method setStatus called on '" + name + "' not implemented yet for " + newStatus;
- this.log(errorMessage);
- throw new qq.Error(errorMessage);
- }
- },
- uploadStoredFiles: function() {
- if (this._storedIds.length === 0) {
- this._itemError("noFilesError");
- } else {
- this._uploadStoredFiles();
- }
+ }
+ });
+ },
+ _createDeleteHandler: function() {
+ var self = this;
+
+ return new qq.DeleteFileAjaxRequestor({
+ maxConnections: this._options.maxConnections,
+ customHeaders: this._options.deleteFile.customHeaders,
+ paramsStore: this._deleteFileParamsStore,
+ endpointStore: this._deleteFileEndpointStore,
+ demoMode: this._options.demoMode,
+ cors: this._options.cors,
+ log: function(str, level) {
+ self.log(str, level);
+ },
+ onDelete: function(id) {
+ self._onDelete(id);
+ self._options.callbacks.onDelete(id);
+ },
+ onDeleteComplete: function(id, xhr, isError) {
+ self._onDeleteComplete(id, xhr, isError);
+ self._options.callbacks.onDeleteComplete(id, xhr, isError);
+ }
+
+ });
+ },
+ _preventLeaveInProgress: function(){
+ var self = this;
+
+ this._disposeSupport.attach(window, 'beforeunload', function(e){
+ if (!self._filesInProgress.length){return;}
+
+ var e = e || window.event;
+ // for ie, ff
+ e.returnValue = self._options.messages.onLeave;
+ // for webkit
+ return self._options.messages.onLeave;
+ });
+ },
+ _onSubmit: function(id, name){
+ if (this._options.autoUpload) {
+ this._filesInProgress.push(id);
+ }
+ },
+ _onProgress: function(id, name, loaded, total){
+ },
+ _onComplete: function(id, name, result, xhr){
+ this._removeFromFilesInProgress(id);
+ this._maybeParseAndSendUploadError(id, name, result, xhr);
+ },
+ _onCancel: function(id, name){
+ this._removeFromFilesInProgress(id);
+
+ clearTimeout(this._retryTimeouts[id]);
+
+ var storedItemIndex = qq.indexOf(this._storedIds, id);
+ if (!this._options.autoUpload && storedItemIndex >= 0) {
+ this._storedIds.splice(storedItemIndex, 1);
+ }
+ },
+ _isDeletePossible: function() {
+ return (this._options.deleteFile.enabled &&
+ (!this._options.cors.expected ||
+ (this._options.cors.expected && (qq.ie10() || !qq.ie()))
+ )
+ );
+ },
+ _onSubmitDelete: function(id) {
+ if (this._isDeletePossible()) {
+ if (this._options.callbacks.onSubmitDelete(id)) {
+ this._deleteHandler.sendDelete(id, this.getUuid(id));
+ }
+ }
+ else {
+ this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " +
+ "due to CORS on a user agent that does not support pre-flighting.", "warn");
+ return false;
+ }
+ },
+ _onDelete: function(fileId) {},
+ _onDeleteComplete: function(id, xhr, isError) {
+ var name = this._handler.getName(id);
+
+ if (isError) {
+ this.log("Delete request for '" + name + "' has failed.", "error");
+ this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status);
+ }
+ else {
+ this.log("Delete request for '" + name + "' has succeeded.");
+ }
+ },
+ _removeFromFilesInProgress: function(id) {
+ var index = qq.indexOf(this._filesInProgress, id);
+ if (index >= 0) {
+ this._filesInProgress.splice(index, 1);
+ }
+ },
+ _onUpload: function(id, name){},
+ _onInputChange: function(input){
+ if (qq.isXhrUploadSupported()){
+ this.addFiles(input.files);
+ } else {
+ this.addFiles(input);
+ }
+ this._button.reset();
+ },
+ _onBeforeAutoRetry: function(id, name) {
+ this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
+ },
+ _onAutoRetry: function(id, name, responseJSON) {
+ this.log("Retrying " + name + "...");
+ this._autoRetries[id]++;
+ this._handler.retry(id);
+ },
+ _shouldAutoRetry: function(id, name, responseJSON) {
+ if (!this._preventRetries[id] && this._options.retry.enableAuto) {
+ if (this._autoRetries[id] === undefined) {
+ this._autoRetries[id] = 0;
+ }
+
+ return this._autoRetries[id] < this._options.retry.maxAutoAttempts
+ }
+
+ return false;
+ },
+ //return false if we should not attempt the requested retry
+ _onBeforeManualRetry: function(id) {
+ if (this._preventRetries[id]) {
+ this.log("Retries are forbidden for id " + id, 'warn');
+ return false;
+ }
+ else if (this._handler.isValid(id)) {
+ var fileName = this._handler.getName(id);
+
+ if (this._options.callbacks.onManualRetry(id, fileName) === false) {
+ return false;
+ }
+
+ this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
+ this._filesInProgress.push(id);
+ return true;
+ }
+ else {
+ this.log("'" + id + "' is not a valid file ID", 'error');
+ return false;
+ }
+ },
+ _maybeParseAndSendUploadError: function(id, name, response, xhr) {
+ //assuming no one will actually set the response code to something other than 200 and still set 'success' to true
+ if (!response.success){
+ if (xhr && xhr.status !== 200 && !response.error) {
+ this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status);
+ }
+ else {
+ var errorReason = response.error ? response.error : "Upload failure reason unknown";
+ this._options.callbacks.onError(id, name, errorReason);
+ }
+ }
+ },
+ _uploadFileOrBlobDataList: function(fileOrBlobDataList){
+ var validationDescriptors, index, batchInvalid;
+
+ validationDescriptors = this._getValidationDescriptors(fileOrBlobDataList);
+ batchInvalid = this._options.callbacks.onValidateBatch(validationDescriptors) === false;
+
+ if (!batchInvalid) {
+ if (fileOrBlobDataList.length > 0) {
+ for (index = 0; index < fileOrBlobDataList.length; index++){
+ if (this._validateFileOrBlobData(fileOrBlobDataList[index])){
+ this._upload(fileOrBlobDataList[index]);
+ } else {
+ if (this._options.validation.stopOnFirstInvalidFile){
+ return;
+ }
+ }
+ }
+ }
+ else {
+ this._error('noFilesError', "");
+ }
+ }
+ },
+ _upload: function(blobOrFileContainer){
+ var id = this._handler.add(blobOrFileContainer);
+ var name = this._handler.getName(id);
+
+ if (this._options.callbacks.onSubmit(id, name) !== false){
+ this._onSubmit(id, name);
+ if (this._options.autoUpload) {
+ this._handler.upload(id);
+ }
+ else {
+ this._storeForLater(id);
+ }
+ }
+ },
+ _storeForLater: function(id) {
+ this._storedIds.push(id);
+ },
+ _validateFileOrBlobData: function(fileOrBlobData){
+ var validationDescriptor, name, size;
+
+ validationDescriptor = this._getValidationDescriptor(fileOrBlobData);
+ name = validationDescriptor.name;
+ size = validationDescriptor.size;
+
+ if (this._options.callbacks.onValidate(validationDescriptor) === false) {
+ return false;
+ }
+
+ if (qq.isFileOrInput(fileOrBlobData) && !this._isAllowedExtension(name)){
+ this._error('typeError', name);
+ return false;
+
+ }
+ else if (size === 0){
+ this._error('emptyError', name);
+ return false;
+
+ }
+ else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){
+ this._error('sizeError', name);
+ return false;
+
+ }
+ else if (size && size < this._options.validation.minSizeLimit){
+ this._error('minSizeError', name);
+ return false;
+ }
+
+ return true;
+ },
+ _error: function(code, name){
+ var message = this._options.messages[code];
+ function r(name, replacement){ message = message.replace(name, replacement); }
+
+ var extensions = this._options.validation.allowedExtensions.join(', ').toLowerCase();
+
+ r('{file}', this._options.formatFileName(name));
+ r('{extensions}', extensions);
+ r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
+ r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
+
+ this._options.callbacks.onError(null, name, message);
+
+ return message;
+ },
+ _isAllowedExtension: function(fileName){
+ var allowed = this._options.validation.allowedExtensions,
+ valid = false;
+
+ if (!allowed.length) {
+ return true;
+ }
+
+ qq.each(allowed, function(idx, allowedExt) {
+ /*jshint eqeqeq: true, eqnull: true*/
+ var extRegex = new RegExp('\\.' + allowedExt + "$", 'i');
+
+ if (fileName.match(extRegex) != null) {
+ valid = true;
+ return false;
+ }
+ });
+
+ return valid;
+ },
+ _formatSize: function(bytes){
+ var i = -1;
+ do {
+ bytes = bytes / 1024;
+ i++;
+ } while (bytes > 99);
+
+ return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
+ },
+ _wrapCallbacks: function() {
+ var self, safeCallback;
+
+ self = this;
+
+ safeCallback = function(name, callback, args) {
+ try {
+ return callback.apply(self, args);
+ }
+ catch (exception) {
+ self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error');
+ }
+ }
+
+ for (var prop in this._options.callbacks) {
+ (function() {
+ var callbackName, callbackFunc;
+ callbackName = prop;
+ callbackFunc = self._options.callbacks[callbackName];
+ self._options.callbacks[callbackName] = function() {
+ return safeCallback(callbackName, callbackFunc, arguments);
+ }
+ }());
+ }
+ },
+ _parseFileOrBlobDataName: function(fileOrBlobData) {
+ var name;
+
+ if (qq.isFileOrInput(fileOrBlobData)) {
+ if (fileOrBlobData.value) {
+ // it is a file input
+ // get input value and remove path to normalize
+ name = fileOrBlobData.value.replace(/.*(\/|\\)/, "");
+ } else {
+ // fix missing properties in Safari 4 and firefox 11.0a2
+ name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name;
+ }
+ }
+ else {
+ name = fileOrBlobData.name;
+ }
+
+ return name;
+ },
+ _parseFileOrBlobDataSize: function(fileOrBlobData) {
+ var size;
+
+ if (qq.isFileOrInput(fileOrBlobData)) {
+ if (!fileOrBlobData.value){
+ // fix missing properties in Safari 4 and firefox 11.0a2
+ size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size;
+ }
+ }
+ else {
+ size = fileOrBlobData.blob.size;
+ }
+
+ return size;
+ },
+ _getValidationDescriptor: function(fileOrBlobData) {
+ var name, size, fileDescriptor;
+
+ fileDescriptor = {};
+ name = this._parseFileOrBlobDataName(fileOrBlobData);
+ size = this._parseFileOrBlobDataSize(fileOrBlobData);
+
+ fileDescriptor.name = name;
+ if (size) {
+ fileDescriptor.size = size;
+ }
+
+ return fileDescriptor;
+ },
+ _getValidationDescriptors: function(files) {
+ var self = this,
+ fileDescriptors = [];
+
+ qq.each(files, function(idx, file) {
+ fileDescriptors.push(self._getValidationDescriptor(file));
+ });
+
+ return fileDescriptors;
+ },
+ _createParamsStore: function(type) {
+ var paramsStore = {},
+ self = this;
+
+ return {
+ setParams: function(params, id) {
+ var paramsCopy = {};
+ qq.extend(paramsCopy, params);
+ paramsStore[id] = paramsCopy;
+ },
+
+ getParams: function(id) {
+ /*jshint eqeqeq: true, eqnull: true*/
+ var paramsCopy = {};
+
+ if (id != null && paramsStore[id]) {
+ qq.extend(paramsCopy, paramsStore[id]);
+ }
+ else {
+ qq.extend(paramsCopy, self._options[type].params);
+ }
+
+ return paramsCopy;
+ },
+
+ remove: function(fileId) {
+ return delete paramsStore[fileId];
+ },
+
+ reset: function() {
+ paramsStore = {};
}
};
- qq.basePrivateApi = {
- _addCannedFile: function(sessionData) {
- var self = this;
- return this._uploadData.addFile({
- uuid: sessionData.uuid,
- name: sessionData.name,
- size: sessionData.size,
- status: qq.status.UPLOAD_SUCCESSFUL,
- onBeforeStatusChange: function(id) {
- sessionData.deleteFileEndpoint && self.setDeleteFileEndpoint(sessionData.deleteFileEndpoint, id);
- sessionData.deleteFileParams && self.setDeleteFileParams(sessionData.deleteFileParams, id);
- if (sessionData.thumbnailUrl) {
- self._thumbnailUrls[id] = sessionData.thumbnailUrl;
- }
- self._netUploaded++;
- self._netUploadedOrQueued++;
- }
- });
+ },
+ _createEndpointStore: function(type) {
+ var endpointStore = {},
+ self = this;
+
+ return {
+ setEndpoint: function(endpoint, id) {
+ endpointStore[id] = endpoint;
},
- _annotateWithButtonId: function(file, associatedInput) {
- if (qq.isFile(file)) {
- file.qqButtonId = this._getButtonId(associatedInput);
+
+ getEndpoint: function(id) {
+ /*jshint eqeqeq: true, eqnull: true*/
+ if (id != null && endpointStore[id]) {
+ return endpointStore[id];
}
+
+ return self._options[type].endpoint;
},
- _batchError: function(message) {
- this._options.callbacks.onError(null, null, message, undefined);
+
+ remove: function(fileId) {
+ return delete endpointStore[fileId];
},
- _createDeleteHandler: function() {
- var self = this;
- return new qq.DeleteFileAjaxRequester({
- method: this._options.deleteFile.method.toUpperCase(),
- maxConnections: this._options.maxConnections,
- uuidParamName: this._options.request.uuidName,
- customHeaders: this._deleteFileCustomHeadersStore,
- paramsStore: this._deleteFileParamsStore,
- endpointStore: this._deleteFileEndpointStore,
- cors: this._options.cors,
- log: qq.bind(self.log, self),
- onDelete: function(id) {
- self._onDelete(id);
- self._options.callbacks.onDelete(id);
- },
- onDeleteComplete: function(id, xhrOrXdr, isError) {
- self._onDeleteComplete(id, xhrOrXdr, isError);
- self._options.callbacks.onDeleteComplete(id, xhrOrXdr, isError);
- }
- });
- },
- _createPasteHandler: function() {
- var self = this;
- return new qq.PasteSupport({
- targetElement: this._options.paste.targetElement,
- callbacks: {
- log: qq.bind(self.log, self),
- pasteReceived: function(blob) {
- self._handleCheckedCallback({
- name: "onPasteReceived",
- callback: qq.bind(self._options.callbacks.onPasteReceived, self, blob),
- onSuccess: qq.bind(self._handlePasteSuccess, self, blob),
- identifier: "pasted image"
- });
- }
- }
- });
- },
- _createStore: function(initialValue, _readOnlyValues_) {
- var store = {}, catchall = initialValue, perIdReadOnlyValues = {}, readOnlyValues = _readOnlyValues_, copy = function(orig) {
- if (qq.isObject(orig)) {
- return qq.extend({}, orig);
- }
- return orig;
- }, getReadOnlyValues = function() {
- if (qq.isFunction(readOnlyValues)) {
- return readOnlyValues();
- }
- return readOnlyValues;
- }, includeReadOnlyValues = function(id, existing) {
- if (readOnlyValues && qq.isObject(existing)) {
- qq.extend(existing, getReadOnlyValues());
- }
- if (perIdReadOnlyValues[id]) {
- qq.extend(existing, perIdReadOnlyValues[id]);
- }
- };
- return {
- set: function(val, id) {
- if (id == null) {
- store = {};
- catchall = copy(val);
- } else {
- store[id] = copy(val);
- }
- },
- get: function(id) {
- var values;
- if (id != null && store[id]) {
- values = store[id];
- } else {
- values = copy(catchall);
- }
- includeReadOnlyValues(id, values);
- return copy(values);
- },
- addReadOnly: function(id, values) {
- if (qq.isObject(store)) {
- if (id === null) {
- if (qq.isFunction(values)) {
- readOnlyValues = values;
- } else {
- readOnlyValues = readOnlyValues || {};
- qq.extend(readOnlyValues, values);
- }
- } else {
- perIdReadOnlyValues[id] = perIdReadOnlyValues[id] || {};
- qq.extend(perIdReadOnlyValues[id], values);
+
+ reset: function() {
+ endpointStore = {};
+ }
+ };
+ }
+};
+/*globals qq, document*/
+qq.DragAndDrop = function(o) {
+ "use strict";
+
+ var options, dz, dirPending,
+ droppedFiles = [],
+ droppedEntriesCount = 0,
+ droppedEntriesParsedCount = 0,
+ disposeSupport = new qq.DisposeSupport();
+
+ options = {
+ dropArea: null,
+ extraDropzones: [],
+ hideDropzones: true,
+ multiple: true,
+ classes: {
+ dropActive: null
+ },
+ callbacks: {
+ dropProcessing: function(isProcessing, files) {},
+ error: function(code, filename) {},
+ log: function(message, level) {}
+ }
+ };
+
+ qq.extend(options, o);
+
+ function maybeUploadDroppedFiles() {
+ if (droppedEntriesCount === droppedEntriesParsedCount && !dirPending) {
+ options.callbacks.log('Grabbed ' + droppedFiles.length + " files after tree traversal.");
+ dz.dropDisabled(false);
+ options.callbacks.dropProcessing(false, droppedFiles);
+ }
+ }
+ function addDroppedFile(file) {
+ droppedFiles.push(file);
+ droppedEntriesParsedCount+=1;
+ maybeUploadDroppedFiles();
+ }
+
+ function traverseFileTree(entry) {
+ var dirReader, i;
+
+ droppedEntriesCount+=1;
+
+ if (entry.isFile) {
+ entry.file(function(file) {
+ addDroppedFile(file);
+ });
+ }
+ else if (entry.isDirectory) {
+ dirPending = true;
+ dirReader = entry.createReader();
+ dirReader.readEntries(function(entries) {
+ droppedEntriesParsedCount+=1;
+ for (i = 0; i < entries.length; i+=1) {
+ traverseFileTree(entries[i]);
+ }
+
+ dirPending = false;
+
+ if (!entries.length) {
+ maybeUploadDroppedFiles();
+ }
+ });
+ }
+ }
+
+ function handleDataTransfer(dataTransfer) {
+ var i, items, entry;
+
+ options.callbacks.dropProcessing(true);
+ dz.dropDisabled(true);
+
+ if (dataTransfer.files.length > 1 && !options.multiple) {
+ options.callbacks.dropProcessing(false);
+ options.callbacks.error('tooManyFilesError', "");
+ dz.dropDisabled(false);
+ }
+ else {
+ droppedFiles = [];
+ droppedEntriesCount = 0;
+ droppedEntriesParsedCount = 0;
+
+ if (qq.isFolderDropSupported(dataTransfer)) {
+ items = dataTransfer.items;
+
+ for (i = 0; i < items.length; i+=1) {
+ entry = items[i].webkitGetAsEntry();
+ if (entry) {
+ //due to a bug in Chrome's File System API impl - #149735
+ if (entry.isFile) {
+ droppedFiles.push(items[i].getAsFile());
+ if (i === items.length-1) {
+ maybeUploadDroppedFiles();
}
}
- },
- remove: function(fileId) {
- return delete store[fileId];
- },
- reset: function() {
- store = {};
- perIdReadOnlyValues = {};
- catchall = initialValue;
- }
- };
- },
- _createUploadDataTracker: function() {
- var self = this;
- return new qq.UploadData({
- getName: function(id) {
- return self.getName(id);
- },
- getUuid: function(id) {
- return self.getUuid(id);
- },
- getSize: function(id) {
- return self.getSize(id);
- },
- onStatusChange: function(id, oldStatus, newStatus) {
- self._onUploadStatusChange(id, oldStatus, newStatus);
- self._options.callbacks.onStatusChange(id, oldStatus, newStatus);
- self._maybeAllComplete(id, newStatus);
- if (self._totalProgress) {
- setTimeout(function() {
- self._totalProgress.onStatusChange(id, oldStatus, newStatus);
- }, 0);
- }
- }
- });
- },
- _createUploadButton: function(spec) {
- var self = this, acceptFiles = spec.accept || this._options.validation.acceptFiles, allowedExtensions = spec.allowedExtensions || this._options.validation.allowedExtensions, button;
- function allowMultiple() {
- if (qq.supportedFeatures.ajaxUploading) {
- if (self._options.workarounds.iosEmptyVideos && qq.ios() && !qq.ios6() && self._isAllowedExtension(allowedExtensions, ".mov")) {
- return false;
- }
- if (spec.multiple === undefined) {
- return self._options.multiple;
- }
- return spec.multiple;
- }
- return false;
- }
- button = new qq.UploadButton({
- acceptFiles: acceptFiles,
- element: spec.element,
- focusClass: this._options.classes.buttonFocus,
- folders: spec.folders,
- hoverClass: this._options.classes.buttonHover,
- ios8BrowserCrashWorkaround: this._options.workarounds.ios8BrowserCrash,
- multiple: allowMultiple(),
- name: this._options.request.inputName,
- onChange: function(input) {
- self._onInputChange(input);
- },
- title: spec.title == null ? this._options.text.fileInputTitle : spec.title
- });
- this._disposeSupport.addDisposer(function() {
- button.dispose();
- });
- self._buttons.push(button);
- return button;
- },
- _createUploadHandler: function(additionalOptions, namespace) {
- var self = this, lastOnProgress = {}, options = {
- debug: this._options.debug,
- maxConnections: this._options.maxConnections,
- cors: this._options.cors,
- paramsStore: this._paramsStore,
- endpointStore: this._endpointStore,
- chunking: this._options.chunking,
- resume: this._options.resume,
- blobs: this._options.blobs,
- log: qq.bind(self.log, self),
- preventRetryParam: this._options.retry.preventRetryResponseProperty,
- onProgress: function(id, name, loaded, total) {
- if (loaded < 0 || total < 0) {
- return;
- }
- if (lastOnProgress[id]) {
- if (lastOnProgress[id].loaded !== loaded || lastOnProgress[id].total !== total) {
- self._onProgress(id, name, loaded, total);
- self._options.callbacks.onProgress(id, name, loaded, total);
- }
- } else {
- self._onProgress(id, name, loaded, total);
- self._options.callbacks.onProgress(id, name, loaded, total);
- }
- lastOnProgress[id] = {
- loaded: loaded,
- total: total
- };
- },
- onComplete: function(id, name, result, xhr) {
- delete lastOnProgress[id];
- var status = self.getUploads({
- id: id
- }).status, retVal;
- if (status === qq.status.UPLOAD_SUCCESSFUL || status === qq.status.UPLOAD_FAILED) {
- return;
- }
- retVal = self._onComplete(id, name, result, xhr);
- if (retVal instanceof qq.Promise) {
- retVal.done(function() {
- self._options.callbacks.onComplete(id, name, result, xhr);
- });
- } else {
- self._options.callbacks.onComplete(id, name, result, xhr);
- }
- },
- onCancel: function(id, name, cancelFinalizationEffort) {
- var promise = new qq.Promise();
- self._handleCheckedCallback({
- name: "onCancel",
- callback: qq.bind(self._options.callbacks.onCancel, self, id, name),
- onFailure: promise.failure,
- onSuccess: function() {
- cancelFinalizationEffort.then(function() {
- self._onCancel(id, name);
- });
- promise.success();
- },
- identifier: id
- });
- return promise;
- },
- onUploadPrep: qq.bind(this._onUploadPrep, this),
- onUpload: function(id, name) {
- self._onUpload(id, name);
- self._options.callbacks.onUpload(id, name);
- },
- onUploadChunk: function(id, name, chunkData) {
- self._onUploadChunk(id, chunkData);
- self._options.callbacks.onUploadChunk(id, name, chunkData);
- },
- onUploadChunkSuccess: function(id, chunkData, result, xhr) {
- self._options.callbacks.onUploadChunkSuccess.apply(self, arguments);
- },
- onResume: function(id, name, chunkData) {
- return self._options.callbacks.onResume(id, name, chunkData);
- },
- onAutoRetry: function(id, name, responseJSON, xhr) {
- return self._onAutoRetry.apply(self, arguments);
- },
- onUuidChanged: function(id, newUuid) {
- self.log("Server requested UUID change from '" + self.getUuid(id) + "' to '" + newUuid + "'");
- self.setUuid(id, newUuid);
- },
- getName: qq.bind(self.getName, self),
- getUuid: qq.bind(self.getUuid, self),
- getSize: qq.bind(self.getSize, self),
- setSize: qq.bind(self._setSize, self),
- getDataByUuid: function(uuid) {
- return self.getUploads({
- uuid: uuid
- });
- },
- isQueued: function(id) {
- var status = self.getUploads({
- id: id
- }).status;
- return status === qq.status.QUEUED || status === qq.status.SUBMITTED || status === qq.status.UPLOAD_RETRYING || status === qq.status.PAUSED;
- },
- getIdsInProxyGroup: self._uploadData.getIdsInProxyGroup,
- getIdsInBatch: self._uploadData.getIdsInBatch
- };
- qq.each(this._options.request, function(prop, val) {
- options[prop] = val;
- });
- options.customHeaders = this._customHeadersStore;
- if (additionalOptions) {
- qq.each(additionalOptions, function(key, val) {
- options[key] = val;
- });
- }
- return new qq.UploadHandlerController(options, namespace);
- },
- _fileOrBlobRejected: function(id) {
- this._netUploadedOrQueued--;
- this._uploadData.setStatus(id, qq.status.REJECTED);
- },
- _formatSize: function(bytes) {
- if (bytes === 0) {
- return bytes + this._options.text.sizeSymbols[0];
- }
- var i = -1;
- do {
- bytes = bytes / 1e3;
- i++;
- } while (bytes > 999);
- return Math.max(bytes, .1).toFixed(1) + this._options.text.sizeSymbols[i];
- },
- _generateExtraButtonSpecs: function() {
- var self = this;
- this._extraButtonSpecs = {};
- qq.each(this._options.extraButtons, function(idx, extraButtonOptionEntry) {
- var multiple = extraButtonOptionEntry.multiple, validation = qq.extend({}, self._options.validation, true), extraButtonSpec = qq.extend({}, extraButtonOptionEntry);
- if (multiple === undefined) {
- multiple = self._options.multiple;
- }
- if (extraButtonSpec.validation) {
- qq.extend(validation, extraButtonOptionEntry.validation, true);
- }
- qq.extend(extraButtonSpec, {
- multiple: multiple,
- validation: validation
- }, true);
- self._initExtraButton(extraButtonSpec);
- });
- },
- _getButton: function(buttonId) {
- var extraButtonsSpec = this._extraButtonSpecs[buttonId];
- if (extraButtonsSpec) {
- return extraButtonsSpec.element;
- } else if (buttonId === this._defaultButtonId) {
- return this._options.button;
- }
- },
- _getButtonId: function(buttonOrFileInputOrFile) {
- var inputs, fileInput, fileBlobOrInput = buttonOrFileInputOrFile;
- if (fileBlobOrInput instanceof qq.BlobProxy) {
- fileBlobOrInput = fileBlobOrInput.referenceBlob;
- }
- if (fileBlobOrInput && !qq.isBlob(fileBlobOrInput)) {
- if (qq.isFile(fileBlobOrInput)) {
- return fileBlobOrInput.qqButtonId;
- } else if (fileBlobOrInput.tagName.toLowerCase() === "input" && fileBlobOrInput.type.toLowerCase() === "file") {
- return fileBlobOrInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME);
- }
- inputs = fileBlobOrInput.getElementsByTagName("input");
- qq.each(inputs, function(idx, input) {
- if (input.getAttribute("type") === "file") {
- fileInput = input;
- return false;
- }
- });
- if (fileInput) {
- return fileInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME);
- }
- }
- },
- _getNotFinished: function() {
- return this._uploadData.retrieve({
- status: [ qq.status.UPLOADING, qq.status.UPLOAD_RETRYING, qq.status.QUEUED, qq.status.SUBMITTING, qq.status.SUBMITTED, qq.status.PAUSED ]
- }).length;
- },
- _getValidationBase: function(buttonId) {
- var extraButtonSpec = this._extraButtonSpecs[buttonId];
- return extraButtonSpec ? extraButtonSpec.validation : this._options.validation;
- },
- _getValidationDescriptor: function(fileWrapper) {
- if (fileWrapper.file instanceof qq.BlobProxy) {
- return {
- name: qq.getFilename(fileWrapper.file.referenceBlob),
- size: fileWrapper.file.referenceBlob.size
- };
- }
- return {
- name: this.getUploads({
- id: fileWrapper.id
- }).name,
- size: this.getUploads({
- id: fileWrapper.id
- }).size
- };
- },
- _getValidationDescriptors: function(fileWrappers) {
- var self = this, fileDescriptors = [];
- qq.each(fileWrappers, function(idx, fileWrapper) {
- fileDescriptors.push(self._getValidationDescriptor(fileWrapper));
- });
- return fileDescriptors;
- },
- _handleCameraAccess: function() {
- if (this._options.camera.ios && qq.ios()) {
- var acceptIosCamera = "image/*;capture=camera", button = this._options.camera.button, buttonId = button ? this._getButtonId(button) : this._defaultButtonId, optionRoot = this._options;
- if (buttonId && buttonId !== this._defaultButtonId) {
- optionRoot = this._extraButtonSpecs[buttonId];
- }
- optionRoot.multiple = false;
- if (optionRoot.validation.acceptFiles === null) {
- optionRoot.validation.acceptFiles = acceptIosCamera;
- } else {
- optionRoot.validation.acceptFiles += "," + acceptIosCamera;
- }
- qq.each(this._buttons, function(idx, button) {
- if (button.getButtonId() === buttonId) {
- button.setMultiple(optionRoot.multiple);
- button.setAcceptFiles(optionRoot.acceptFiles);
- return false;
- }
- });
- }
- },
- _handleCheckedCallback: function(details) {
- var self = this, callbackRetVal = details.callback();
- if (qq.isGenericPromise(callbackRetVal)) {
- this.log(details.name + " - waiting for " + details.name + " promise to be fulfilled for " + details.identifier);
- return callbackRetVal.then(function(successParam) {
- self.log(details.name + " promise success for " + details.identifier);
- details.onSuccess(successParam);
- }, function() {
- if (details.onFailure) {
- self.log(details.name + " promise failure for " + details.identifier);
- details.onFailure();
- } else {
- self.log(details.name + " promise failure for " + details.identifier);
- }
- });
- }
- if (callbackRetVal !== false) {
- details.onSuccess(callbackRetVal);
- } else {
- if (details.onFailure) {
- this.log(details.name + " - return value was 'false' for " + details.identifier + ". Invoking failure callback.");
- details.onFailure();
- } else {
- this.log(details.name + " - return value was 'false' for " + details.identifier + ". Will not proceed.");
- }
- }
- return callbackRetVal;
- },
- _handleNewFile: function(file, batchId, newFileWrapperList) {
- var self = this, uuid = qq.getUniqueId(), size = -1, name = qq.getFilename(file), actualFile = file.blob || file, handler = this._customNewFileHandler ? this._customNewFileHandler : qq.bind(self._handleNewFileGeneric, self);
- if (!qq.isInput(actualFile) && actualFile.size >= 0) {
- size = actualFile.size;
- }
- handler(actualFile, name, uuid, size, newFileWrapperList, batchId, this._options.request.uuidName, {
- uploadData: self._uploadData,
- paramsStore: self._paramsStore,
- addFileToHandler: function(id, file) {
- self._handler.add(id, file);
- self._netUploadedOrQueued++;
- self._trackButton(id);
- }
- });
- },
- _handleNewFileGeneric: function(file, name, uuid, size, fileList, batchId) {
- var id = this._uploadData.addFile({
- uuid: uuid,
- name: name,
- size: size,
- batchId: batchId
- });
- this._handler.add(id, file);
- this._trackButton(id);
- this._netUploadedOrQueued++;
- fileList.push({
- id: id,
- file: file
- });
- },
- _handlePasteSuccess: function(blob, extSuppliedName) {
- var extension = blob.type.split("/")[1], name = extSuppliedName;
- if (name == null) {
- name = this._options.paste.defaultName;
- }
- name += "." + extension;
- this.addFiles({
- name: name,
- blob: blob
- });
- },
- _handleDeleteSuccess: function(id) {
- if (this.getUploads({
- id: id
- }).status !== qq.status.DELETED) {
- var name = this.getName(id);
- this._netUploadedOrQueued--;
- this._netUploaded--;
- this._handler.expunge(id);
- this._uploadData.setStatus(id, qq.status.DELETED);
- this.log("Delete request for '" + name + "' has succeeded.");
- }
- },
- _handleDeleteFailed: function(id, xhrOrXdr) {
- var name = this.getName(id);
- this._uploadData.setStatus(id, qq.status.DELETE_FAILED);
- this.log("Delete request for '" + name + "' has failed.", "error");
- if (!xhrOrXdr || xhrOrXdr.withCredentials === undefined) {
- this._options.callbacks.onError(id, name, "Delete request failed", xhrOrXdr);
- } else {
- this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhrOrXdr.status, xhrOrXdr);
- }
- },
- _initExtraButton: function(spec) {
- var button = this._createUploadButton({
- accept: spec.validation.acceptFiles,
- allowedExtensions: spec.validation.allowedExtensions,
- element: spec.element,
- folders: spec.folders,
- multiple: spec.multiple,
- title: spec.fileInputTitle
- });
- this._extraButtonSpecs[button.getButtonId()] = spec;
- },
- _initFormSupportAndParams: function() {
- this._formSupport = qq.FormSupport && new qq.FormSupport(this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this));
- if (this._formSupport && this._formSupport.attachedToForm) {
- this._paramsStore = this._createStore(this._options.request.params, this._formSupport.getFormInputsAsObject);
- this._options.autoUpload = this._formSupport.newAutoUpload;
- if (this._formSupport.newEndpoint) {
- this._options.request.endpoint = this._formSupport.newEndpoint;
- }
- } else {
- this._paramsStore = this._createStore(this._options.request.params);
- }
- },
- _isDeletePossible: function() {
- if (!qq.DeleteFileAjaxRequester || !this._options.deleteFile.enabled) {
- return false;
- }
- if (this._options.cors.expected) {
- if (qq.supportedFeatures.deleteFileCorsXhr) {
- return true;
- }
- if (qq.supportedFeatures.deleteFileCorsXdr && this._options.cors.allowXdr) {
- return true;
- }
- return false;
- }
- return true;
- },
- _isAllowedExtension: function(allowed, fileName) {
- var valid = false;
- if (!allowed.length) {
- return true;
- }
- qq.each(allowed, function(idx, allowedExt) {
- if (qq.isString(allowedExt)) {
- var extRegex = new RegExp("\\." + allowedExt + "$", "i");
- if (fileName.match(extRegex) != null) {
- valid = true;
- return false;
- }
- }
- });
- return valid;
- },
- _itemError: function(code, maybeNameOrNames, item) {
- var message = this._options.messages[code], allowedExtensions = [], names = [].concat(maybeNameOrNames), name = names[0], buttonId = this._getButtonId(item), validationBase = this._getValidationBase(buttonId), extensionsForMessage, placeholderMatch;
- function r(name, replacement) {
- message = message.replace(name, replacement);
- }
- qq.each(validationBase.allowedExtensions, function(idx, allowedExtension) {
- if (qq.isString(allowedExtension)) {
- allowedExtensions.push(allowedExtension);
- }
- });
- extensionsForMessage = allowedExtensions.join(", ").toLowerCase();
- r("{file}", this._options.formatFileName(name));
- r("{extensions}", extensionsForMessage);
- r("{sizeLimit}", this._formatSize(validationBase.sizeLimit));
- r("{minSizeLimit}", this._formatSize(validationBase.minSizeLimit));
- placeholderMatch = message.match(/(\{\w+\})/g);
- if (placeholderMatch !== null) {
- qq.each(placeholderMatch, function(idx, placeholder) {
- r(placeholder, names[idx]);
- });
- }
- this._options.callbacks.onError(null, name, message, undefined);
- return message;
- },
- _manualRetry: function(id, callback) {
- if (this._onBeforeManualRetry(id)) {
- this._netUploadedOrQueued++;
- this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING);
- if (callback) {
- callback(id);
- } else {
- this._handler.retry(id);
- }
- return true;
- }
- },
- _maybeAllComplete: function(id, status) {
- var self = this, notFinished = this._getNotFinished();
- if (status === qq.status.UPLOAD_SUCCESSFUL) {
- this._succeededSinceLastAllComplete.push(id);
- } else if (status === qq.status.UPLOAD_FAILED) {
- this._failedSinceLastAllComplete.push(id);
- }
- if (notFinished === 0 && (this._succeededSinceLastAllComplete.length || this._failedSinceLastAllComplete.length)) {
- setTimeout(function() {
- self._onAllComplete(self._succeededSinceLastAllComplete, self._failedSinceLastAllComplete);
- }, 0);
- }
- },
- _maybeHandleIos8SafariWorkaround: function() {
- var self = this;
- if (this._options.workarounds.ios8SafariUploads && qq.ios800() && qq.iosSafari()) {
- setTimeout(function() {
- window.alert(self._options.messages.unsupportedBrowserIos8Safari);
- }, 0);
- throw new qq.Error(this._options.messages.unsupportedBrowserIos8Safari);
- }
- },
- _maybeParseAndSendUploadError: function(id, name, response, xhr) {
- if (!response.success) {
- if (xhr && xhr.status !== 200 && !response.error) {
- this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr);
- } else {
- var errorReason = response.error ? response.error : this._options.text.defaultResponseError;
- this._options.callbacks.onError(id, name, errorReason, xhr);
- }
- }
- },
- _maybeProcessNextItemAfterOnValidateCallback: function(validItem, items, index, params, endpoint) {
- var self = this;
- if (items.length > index) {
- if (validItem || !this._options.validation.stopOnFirstInvalidFile) {
- setTimeout(function() {
- var validationDescriptor = self._getValidationDescriptor(items[index]), buttonId = self._getButtonId(items[index].file), button = self._getButton(buttonId);
- self._handleCheckedCallback({
- name: "onValidate",
- callback: qq.bind(self._options.callbacks.onValidate, self, validationDescriptor, button),
- onSuccess: qq.bind(self._onValidateCallbackSuccess, self, items, index, params, endpoint),
- onFailure: qq.bind(self._onValidateCallbackFailure, self, items, index, params, endpoint),
- identifier: "Item '" + validationDescriptor.name + "', size: " + validationDescriptor.size
- });
- }, 0);
- } else if (!validItem) {
- for (;index < items.length; index++) {
- self._fileOrBlobRejected(items[index].id);
+
+ else {
+ traverseFileTree(entry);
}
}
}
+ }
+ else {
+ options.callbacks.dropProcessing(false, dataTransfer.files);
+ dz.dropDisabled(false);
+ }
+ }
+ }
+
+ function setupDropzone(dropArea){
+ dz = new qq.UploadDropZone({
+ element: dropArea,
+ onEnter: function(e){
+ qq(dropArea).addClass(options.classes.dropActive);
+ e.stopPropagation();
},
- _onAllComplete: function(successful, failed) {
- this._totalProgress && this._totalProgress.onAllComplete(successful, failed, this._preventRetries);
- this._options.callbacks.onAllComplete(qq.extend([], successful), qq.extend([], failed));
- this._succeededSinceLastAllComplete = [];
- this._failedSinceLastAllComplete = [];
+ onLeaveNotDescendants: function(e){
+ qq(dropArea).removeClass(options.classes.dropActive);
},
- _onAutoRetry: function(id, name, responseJSON, xhr, callback) {
- var self = this;
- self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
- if (self._shouldAutoRetry(id, name, responseJSON)) {
- var retryWaitPeriod = self._options.retry.autoAttemptDelay * 1e3;
- self._maybeParseAndSendUploadError.apply(self, arguments);
- self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id]);
- self._onBeforeAutoRetry(id, name);
- self._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING);
- self._retryTimeouts[id] = setTimeout(function() {
- self.log("Starting retry for " + name + "...");
- if (callback) {
- callback(id);
- } else {
- self._handler.retry(id);
- }
- }, retryWaitPeriod);
- return true;
+ onDrop: function(e){
+ if (options.hideDropzones) {
+ qq(dropArea).hide();
}
- },
- _onBeforeAutoRetry: function(id, name) {
- this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
- },
- _onBeforeManualRetry: function(id) {
- var itemLimit = this._currentItemLimit, fileName;
- if (this._preventRetries[id]) {
- this.log("Retries are forbidden for id " + id, "warn");
- return false;
- } else if (this._handler.isValid(id)) {
- fileName = this.getName(id);
- if (this._options.callbacks.onManualRetry(id, fileName) === false) {
- return false;
+ qq(dropArea).removeClass(options.classes.dropActive);
+
+ handleDataTransfer(e.dataTransfer);
+ }
+ });
+
+ disposeSupport.addDisposer(function() {
+ dz.dispose();
+ });
+
+ if (options.hideDropzones) {
+ qq(dropArea).hide();
+ }
+ }
+
+ function isFileDrag(dragEvent) {
+ var fileDrag;
+
+ qq.each(dragEvent.dataTransfer.types, function(key, val) {
+ if (val === 'Files') {
+ fileDrag = true;
+ return false;
+ }
+ });
+
+ return fileDrag;
+ }
+
+ function setupDragDrop(){
+ if (options.dropArea) {
+ options.extraDropzones.push(options.dropArea);
+ }
+
+ var i, dropzones = options.extraDropzones;
+
+ for (i=0; i < dropzones.length; i+=1){
+ setupDropzone(dropzones[i]);
+ }
+
+ // IE <= 9 does not support the File API used for drag+drop uploads
+ if (options.dropArea && (!qq.ie() || qq.ie10())) {
+ disposeSupport.attach(document, 'dragenter', function(e) {
+ if (!dz.dropDisabled() && isFileDrag(e)) {
+ if (qq(options.dropArea).hasClass(options.classes.dropDisabled)) {
+ return;
}
- if (itemLimit > 0 && this._netUploadedOrQueued + 1 > itemLimit) {
- this._itemError("retryFailTooManyItems");
- return false;
+
+ options.dropArea.style.display = 'block';
+ for (i=0; i < dropzones.length; i+=1) {
+ dropzones[i].style.display = 'block';
}
- this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
- return true;
- } else {
- this.log("'" + id + "' is not a valid file ID", "error");
- return false;
}
- },
- _onCancel: function(id, name) {
- this._netUploadedOrQueued--;
- clearTimeout(this._retryTimeouts[id]);
- var storedItemIndex = qq.indexOf(this._storedIds, id);
- if (!this._options.autoUpload && storedItemIndex >= 0) {
- this._storedIds.splice(storedItemIndex, 1);
+ });
+ }
+ disposeSupport.attach(document, 'dragleave', function(e){
+ if (options.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) {
+ for (i=0; i < dropzones.length; i+=1) {
+ qq(dropzones[i]).hide();
}
- this._uploadData.setStatus(id, qq.status.CANCELED);
- },
- _onComplete: function(id, name, result, xhr) {
- if (!result.success) {
- this._netUploadedOrQueued--;
- this._uploadData.setStatus(id, qq.status.UPLOAD_FAILED);
- if (result[this._options.retry.preventRetryResponseProperty] === true) {
- this._preventRetries[id] = true;
- }
- } else {
- if (result.thumbnailUrl) {
- this._thumbnailUrls[id] = result.thumbnailUrl;
- }
- this._netUploaded++;
- this._uploadData.setStatus(id, qq.status.UPLOAD_SUCCESSFUL);
+ }
+ });
+ disposeSupport.attach(document, 'drop', function(e){
+ if (options.hideDropzones) {
+ for (i=0; i < dropzones.length; i+=1) {
+ qq(dropzones[i]).hide();
}
- this._maybeParseAndSendUploadError(id, name, result, xhr);
- return result.success ? true : false;
- },
- _onDelete: function(id) {
- this._uploadData.setStatus(id, qq.status.DELETING);
- },
- _onDeleteComplete: function(id, xhrOrXdr, isError) {
- var name = this.getName(id);
- if (isError) {
- this._handleDeleteFailed(id, xhrOrXdr);
- } else {
- this._handleDeleteSuccess(id);
+ }
+ e.preventDefault();
+ });
+ }
+
+ return {
+ setup: function() {
+ setupDragDrop();
+ },
+
+ setupExtraDropzone: function(element) {
+ options.extraDropzones.push(element);
+ setupDropzone(element);
+ },
+
+ removeExtraDropzone: function(element) {
+ var i, dzs = options.extraDropzones;
+ for(i in dzs) {
+ if (dzs[i] === element) {
+ return dzs.splice(i, 1);
}
- },
- _onInputChange: function(input) {
- var fileIndex;
- if (qq.supportedFeatures.ajaxUploading) {
- for (fileIndex = 0; fileIndex < input.files.length; fileIndex++) {
- this._annotateWithButtonId(input.files[fileIndex], input);
- }
- this.addFiles(input.files);
- } else if (input.value.length > 0) {
- this.addFiles(input);
- }
- qq.each(this._buttons, function(idx, button) {
- button.reset();
+ }
+ },
+
+ dispose: function() {
+ disposeSupport.dispose();
+ dz.dispose();
+ }
+ };
+};
+
+
+qq.UploadDropZone = function(o){
+ "use strict";
+
+ var options, element, preventDrop, dropOutsideDisabled, disposeSupport = new qq.DisposeSupport();
+
+ options = {
+ element: null,
+ onEnter: function(e){},
+ onLeave: function(e){},
+ // is not fired when leaving element by hovering descendants
+ onLeaveNotDescendants: function(e){},
+ onDrop: function(e){}
+ };
+
+ qq.extend(options, o);
+ element = options.element;
+
+ function dragover_should_be_canceled(){
+ return qq.safari() || (qq.firefox() && qq.windows());
+ }
+
+ function disableDropOutside(e){
+ // run only once for all instances
+ if (!dropOutsideDisabled ){
+
+ // for these cases we need to catch onDrop to reset dropArea
+ if (dragover_should_be_canceled){
+ disposeSupport.attach(document, 'dragover', function(e){
+ e.preventDefault();
});
- },
- _onProgress: function(id, name, loaded, total) {
- this._totalProgress && this._totalProgress.onIndividualProgress(id, loaded, total);
- },
- _onSubmit: function(id, name) {},
- _onSubmitCallbackSuccess: function(id, name) {
- this._onSubmit.apply(this, arguments);
- this._uploadData.setStatus(id, qq.status.SUBMITTED);
- this._onSubmitted.apply(this, arguments);
- if (this._options.autoUpload) {
- this._options.callbacks.onSubmitted.apply(this, arguments);
- this._uploadFile(id);
- } else {
- this._storeForLater(id);
- this._options.callbacks.onSubmitted.apply(this, arguments);
- }
- },
- _onSubmitDelete: function(id, onSuccessCallback, additionalMandatedParams) {
- var uuid = this.getUuid(id), adjustedOnSuccessCallback;
- if (onSuccessCallback) {
- adjustedOnSuccessCallback = qq.bind(onSuccessCallback, this, id, uuid, additionalMandatedParams);
- }
- if (this._isDeletePossible()) {
- this._handleCheckedCallback({
- name: "onSubmitDelete",
- callback: qq.bind(this._options.callbacks.onSubmitDelete, this, id),
- onSuccess: adjustedOnSuccessCallback || qq.bind(this._deleteHandler.sendDelete, this, id, uuid, additionalMandatedParams),
- identifier: id
- });
- return true;
- } else {
- this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " + "due to CORS on a user agent that does not support pre-flighting.", "warn");
- return false;
- }
- },
- _onSubmitted: function(id) {},
- _onTotalProgress: function(loaded, total) {
- this._options.callbacks.onTotalProgress(loaded, total);
- },
- _onUploadPrep: function(id) {},
- _onUpload: function(id, name) {
- this._uploadData.setStatus(id, qq.status.UPLOADING);
- },
- _onUploadChunk: function(id, chunkData) {},
- _onUploadStatusChange: function(id, oldStatus, newStatus) {
- if (newStatus === qq.status.PAUSED) {
- clearTimeout(this._retryTimeouts[id]);
- }
- },
- _onValidateBatchCallbackFailure: function(fileWrappers) {
- var self = this;
- qq.each(fileWrappers, function(idx, fileWrapper) {
- self._fileOrBlobRejected(fileWrapper.id);
- });
- },
- _onValidateBatchCallbackSuccess: function(validationDescriptors, items, params, endpoint, button) {
- var errorMessage, itemLimit = this._currentItemLimit, proposedNetFilesUploadedOrQueued = this._netUploadedOrQueued;
- if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) {
- if (items.length > 0) {
- this._handleCheckedCallback({
- name: "onValidate",
- callback: qq.bind(this._options.callbacks.onValidate, this, validationDescriptors[0], button),
- onSuccess: qq.bind(this._onValidateCallbackSuccess, this, items, 0, params, endpoint),
- onFailure: qq.bind(this._onValidateCallbackFailure, this, items, 0, params, endpoint),
- identifier: "Item '" + items[0].file.name + "', size: " + items[0].file.size
- });
- } else {
- this._itemError("noFilesError");
+ } else {
+ disposeSupport.attach(document, 'dragover', function(e){
+ if (e.dataTransfer){
+ e.dataTransfer.dropEffect = 'none';
+ e.preventDefault();
}
- } else {
- this._onValidateBatchCallbackFailure(items);
- errorMessage = this._options.messages.tooManyItemsError.replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued).replace(/\{itemLimit\}/g, itemLimit);
- this._batchError(errorMessage);
- }
- },
- _onValidateCallbackFailure: function(items, index, params, endpoint) {
- var nextIndex = index + 1;
- this._fileOrBlobRejected(items[index].id, items[index].file.name);
- this._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint);
- },
- _onValidateCallbackSuccess: function(items, index, params, endpoint) {
- var self = this, nextIndex = index + 1, validationDescriptor = this._getValidationDescriptor(items[index]);
- this._validateFileOrBlobData(items[index], validationDescriptor).then(function() {
- self._upload(items[index].id, params, endpoint);
- self._maybeProcessNextItemAfterOnValidateCallback(true, items, nextIndex, params, endpoint);
- }, function() {
- self._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint);
});
- },
- _prepareItemsForUpload: function(items, params, endpoint) {
- if (items.length === 0) {
- this._itemError("noFilesError");
+ }
+
+ dropOutsideDisabled = true;
+ }
+ }
+
+ function isValidFileDrag(e){
+ // e.dataTransfer currently causing IE errors
+ // IE9 does NOT support file API, so drag-and-drop is not possible
+ if (qq.ie() && !qq.ie10()) {
+ return false;
+ }
+
+ var effectTest, dt = e.dataTransfer,
+ // do not check dt.types.contains in webkit, because it crashes safari 4
+ isSafari = qq.safari();
+
+ // dt.effectAllowed is none in Safari 5
+ // dt.types.contains check is for firefox
+ effectTest = qq.ie10() ? true : dt.effectAllowed !== 'none';
+ return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
+ }
+
+ function isOrSetDropDisabled(isDisabled) {
+ if (isDisabled !== undefined) {
+ preventDrop = isDisabled;
+ }
+ return preventDrop;
+ }
+
+ function attachEvents(){
+ disposeSupport.attach(element, 'dragover', function(e){
+ if (!isValidFileDrag(e)) {
+ return;
+ }
+
+ var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
+ if (effect === 'move' || effect === 'linkMove'){
+ e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
+ } else {
+ e.dataTransfer.dropEffect = 'copy'; // for Chrome
+ }
+
+ e.stopPropagation();
+ e.preventDefault();
+ });
+
+ disposeSupport.attach(element, 'dragenter', function(e){
+ if (!isOrSetDropDisabled()) {
+ if (!isValidFileDrag(e)) {
return;
}
- var validationDescriptors = this._getValidationDescriptors(items), buttonId = this._getButtonId(items[0].file), button = this._getButton(buttonId);
- this._handleCheckedCallback({
- name: "onValidateBatch",
- callback: qq.bind(this._options.callbacks.onValidateBatch, this, validationDescriptors, button),
- onSuccess: qq.bind(this._onValidateBatchCallbackSuccess, this, validationDescriptors, items, params, endpoint, button),
- onFailure: qq.bind(this._onValidateBatchCallbackFailure, this, items),
- identifier: "batch validation"
- });
- },
- _preventLeaveInProgress: function() {
- var self = this;
- this._disposeSupport.attach(window, "beforeunload", function(e) {
- if (self.getInProgress()) {
- e = e || window.event;
- e.returnValue = self._options.messages.onLeave;
- return self._options.messages.onLeave;
- }
- });
- },
- _refreshSessionData: function() {
- var self = this, options = this._options.session;
- if (qq.Session && this._options.session.endpoint != null) {
- if (!this._session) {
- qq.extend(options, {
- cors: this._options.cors
- });
- options.log = qq.bind(this.log, this);
- options.addFileRecord = qq.bind(this._addCannedFile, this);
- this._session = new qq.Session(options);
- }
- setTimeout(function() {
- self._session.refresh().then(function(response, xhrOrXdr) {
- self._sessionRequestComplete();
- self._options.callbacks.onSessionRequestComplete(response, true, xhrOrXdr);
- }, function(response, xhrOrXdr) {
- self._options.callbacks.onSessionRequestComplete(response, false, xhrOrXdr);
- });
- }, 0);
- }
- },
- _sessionRequestComplete: function() {},
- _setSize: function(id, newSize) {
- this._uploadData.updateSize(id, newSize);
- this._totalProgress && this._totalProgress.onNewSize(id);
- },
- _shouldAutoRetry: function(id, name, responseJSON) {
- var uploadData = this._uploadData.retrieve({
- id: id
- });
- if (!this._preventRetries[id] && this._options.retry.enableAuto && uploadData.status !== qq.status.PAUSED) {
- if (this._autoRetries[id] === undefined) {
- this._autoRetries[id] = 0;
- }
- if (this._autoRetries[id] < this._options.retry.maxAutoAttempts) {
- this._autoRetries[id] += 1;
- return true;
- }
- }
- return false;
- },
- _storeForLater: function(id) {
- this._storedIds.push(id);
- },
- _trackButton: function(id) {
- var buttonId;
- if (qq.supportedFeatures.ajaxUploading) {
- buttonId = this._handler.getFile(id).qqButtonId;
- } else {
- buttonId = this._getButtonId(this._handler.getInput(id));
- }
- if (buttonId) {
- this._buttonIdsForFileIds[id] = buttonId;
- }
- },
- _updateFormSupportAndParams: function(formElementOrId) {
- this._options.form.element = formElementOrId;
- this._formSupport = qq.FormSupport && new qq.FormSupport(this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this));
- if (this._formSupport && this._formSupport.attachedToForm) {
- this._paramsStore.addReadOnly(null, this._formSupport.getFormInputsAsObject);
- this._options.autoUpload = this._formSupport.newAutoUpload;
- if (this._formSupport.newEndpoint) {
- this.setEndpoint(this._formSupport.newEndpoint);
- }
- }
- },
- _upload: function(id, params, endpoint) {
- var name = this.getName(id);
- if (params) {
- this.setParams(params, id);
- }
- if (endpoint) {
- this.setEndpoint(endpoint, id);
- }
- this._handleCheckedCallback({
- name: "onSubmit",
- callback: qq.bind(this._options.callbacks.onSubmit, this, id, name),
- onSuccess: qq.bind(this._onSubmitCallbackSuccess, this, id, name),
- onFailure: qq.bind(this._fileOrBlobRejected, this, id, name),
- identifier: id
- });
- },
- _uploadFile: function(id) {
- if (!this._handler.upload(id)) {
- this._uploadData.setStatus(id, qq.status.QUEUED);
- }
- },
- _uploadStoredFiles: function() {
- var idToUpload, stillSubmitting, self = this;
- while (this._storedIds.length) {
- idToUpload = this._storedIds.shift();
- this._uploadFile(idToUpload);
- }
- stillSubmitting = this.getUploads({
- status: qq.status.SUBMITTING
- }).length;
- if (stillSubmitting) {
- qq.log("Still waiting for " + stillSubmitting + " files to clear submit queue. Will re-parse stored IDs array shortly.");
- setTimeout(function() {
- self._uploadStoredFiles();
- }, 1e3);
- }
- },
- _validateFileOrBlobData: function(fileWrapper, validationDescriptor) {
- var self = this, file = function() {
- if (fileWrapper.file instanceof qq.BlobProxy) {
- return fileWrapper.file.referenceBlob;
- }
- return fileWrapper.file;
- }(), name = validationDescriptor.name, size = validationDescriptor.size, buttonId = this._getButtonId(fileWrapper.file), validationBase = this._getValidationBase(buttonId), validityChecker = new qq.Promise();
- validityChecker.then(function() {}, function() {
- self._fileOrBlobRejected(fileWrapper.id, name);
- });
- if (qq.isFileOrInput(file) && !this._isAllowedExtension(validationBase.allowedExtensions, name)) {
- this._itemError("typeError", name, file);
- return validityChecker.failure();
- }
- if (!this._options.validation.allowEmpty && size === 0) {
- this._itemError("emptyError", name, file);
- return validityChecker.failure();
- }
- if (size > 0 && validationBase.sizeLimit && size > validationBase.sizeLimit) {
- this._itemError("sizeError", name, file);
- return validityChecker.failure();
- }
- if (size > 0 && size < validationBase.minSizeLimit) {
- this._itemError("minSizeError", name, file);
- return validityChecker.failure();
- }
- if (qq.ImageValidation && qq.supportedFeatures.imagePreviews && qq.isFile(file)) {
- new qq.ImageValidation(file, qq.bind(self.log, self)).validate(validationBase.image).then(validityChecker.success, function(errorCode) {
- self._itemError(errorCode + "ImageError", name, file);
- validityChecker.failure();
- });
- } else {
- validityChecker.success();
- }
- return validityChecker;
- },
- _wrapCallbacks: function() {
- var self, safeCallback, prop;
- self = this;
- safeCallback = function(name, callback, args) {
- var errorMsg;
- try {
- return callback.apply(self, args);
- } catch (exception) {
- errorMsg = exception.message || exception.toString();
- self.log("Caught exception in '" + name + "' callback - " + errorMsg, "error");
- }
- };
- for (prop in this._options.callbacks) {
- (function() {
- var callbackName, callbackFunc;
- callbackName = prop;
- callbackFunc = self._options.callbacks[callbackName];
- self._options.callbacks[callbackName] = function() {
- return safeCallback(callbackName, callbackFunc, arguments);
- };
- })();
- }
+ options.onEnter(e);
}
+ });
+
+ disposeSupport.attach(element, 'dragleave', function(e){
+ if (!isValidFileDrag(e)) {
+ return;
+ }
+
+ options.onLeave(e);
+
+ var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
+ // do not fire when moving a mouse over a descendant
+ if (qq(this).contains(relatedTarget)) {
+ return;
+ }
+
+ options.onLeaveNotDescendants(e);
+ });
+
+ disposeSupport.attach(element, 'drop', function(e){
+ if (!isOrSetDropDisabled()) {
+ if (!isValidFileDrag(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ options.onDrop(e);
+ }
+ });
+ }
+
+ disableDropOutside();
+ attachEvents();
+
+ return {
+ dropDisabled: function(isDisabled) {
+ return isOrSetDropDisabled(isDisabled);
+ },
+
+ dispose: function() {
+ disposeSupport.dispose();
+ }
+ };
+};
+/**
+ * Class that creates upload widget with drag-and-drop and file list
+ * @inherits qq.FineUploaderBasic
+ */
+qq.FineUploader = function(o){
+ // call parent constructor
+ qq.FineUploaderBasic.apply(this, arguments);
+
+ // additional options
+ qq.extend(this._options, {
+ element: null,
+ listElement: null,
+ dragAndDrop: {
+ extraDropzones: [],
+ hideDropzones: true,
+ disableDefaultDropzone: false
+ },
+ text: {
+ uploadButton: 'Upload a file',
+ cancelButton: 'Cancel',
+ retryButton: 'Retry',
+ deleteButton: 'Delete',
+ failUpload: 'Upload failed',
+ dragZone: 'Drop files here to upload',
+ dropProcessing: 'Processing dropped files...',
+ formatProgress: "{percent}% of {total_size}",
+ waitingForResponse: "Processing..."
+ },
+ template: '' +
+ ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '{dragZoneText}' : '') +
+ (!this._options.button ? '' : '') +
+ '{dropProcessingText}' +
+ (!this._options.listElement ? '
' : '') +
+ '',
+
+ // template for one item in file list
+ fileTemplate: ' tag
+ if (innerHTML && innerHTML.match(/^');
+
+ iframe.setAttribute('id', id);
+
+ iframe.style.display = 'none';
+ document.body.appendChild(iframe);
+
+ return iframe;
+ }
+
+ /**
+ * Creates form, that will be submitted to iframe
+ */
+ function createForm(id, iframe){
+ var params = options.paramsStore.getParams(id),
+ protocol = options.demoMode ? "GET" : "POST",
+ form = qq.toElement(''),
+ endpoint = options.endpointStore.getEndpoint(id),
+ url = endpoint;
+
+ params[options.uuidParamName] = uuids[id];
+
+ if (!options.paramsInBody) {
+ url = qq.obj2url(params, endpoint);
+ }
+ else {
+ qq.obj2Inputs(params, form);
+ }
+
+ form.setAttribute('action', url);
+ form.setAttribute('target', iframe.name);
+ form.style.display = 'none';
+ document.body.appendChild(form);
+
+ return form;
+ }
+
+
+ api = {
+ add: function(fileInput) {
+ fileInput.setAttribute('name', options.inputName);
+
+ var id = inputs.push(fileInput) - 1;
+ uuids[id] = qq.getUniqueId();
+
+ // remove file input from DOM
+ if (fileInput.parentNode){
+ qq(fileInput).remove();
+ }
+
+ return id;
+ },
+ getName: function(id) {
+ /*jslint regexp: true*/
+
+ // get input value and remove path to normalize
+ return inputs[id].value.replace(/.*(\/|\\)/, "");
+ },
+ isValid: function(id) {
+ return inputs[id] !== undefined;
+ },
+ reset: function() {
+ qq.UploadHandler.prototype.reset.apply(this, arguments);
+ inputs = [];
+ uuids = [];
+ detachLoadEvents = {};
+ },
+ getUuid: function(id) {
+ return uuids[id];
+ },
+ cancel: function(id) {
+ options.onCancel(id, this.getName(id));
+
+ delete inputs[id];
+ delete uuids[id];
+ delete detachLoadEvents[id];
+
+ if (options.cors.expected) {
+ clearTimeout(postMessageCallbackTimers[id]);
+ delete postMessageCallbackTimers[id];
+ corsMessageReceiver.stopReceivingMessages(id);
+ }
+
+ var iframe = document.getElementById(id);
+ if (iframe) {
+ // to cancel request set src to something else
+ // we use src="javascript:false;" because it doesn't
+ // trigger ie6 prompt on https
+ iframe.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;'); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off
+
+ qq(iframe).remove();
+ }
+ },
+ upload: function(id){
+ var input = inputs[id],
+ fileName = api.getName(id),
+ iframe = createIframe(id),
+ form;
+
+ if (!input){
+ throw new Error('file with passed id was not added, or already uploaded or cancelled');
+ }
+
+ options.onUpload(id, this.getName(id));
+
form = createForm(id, iframe);
form.appendChild(input);
- handler._attachLoadEvent(iframe, function(responseFromMessage) {
- log("iframe loaded");
- var response = responseFromMessage ? responseFromMessage : getIframeContentJson(id, iframe);
- handler._detachLoadEvent(id);
+
+ attachLoadEvent(iframe, function(responseFromMessage){
+ log('iframe loaded');
+
+ var response = responseFromMessage ? responseFromMessage : getIframeContentJson(iframe);
+
+ detachLoadEvent(id);
+
+ //we can't remove an iframe if the iframe doesn't belong to the same domain
if (!options.cors.expected) {
qq(iframe).remove();
}
- if (response.success) {
- promise.success(response);
- } else {
- promise.failure(response);
+
+ if (!response.success) {
+ if (options.onAutoRetry(id, fileName, response)) {
+ return;
+ }
}
+ options.onComplete(id, fileName, response);
+ uploadComplete(id);
});
- log("Sending upload request for " + id);
+
+ log('Sending upload request for ' + id);
form.submit();
qq(form).remove();
- return promise;
- };
- qq.extend(this, new qq.FormUploadHandler({
- options: {
- isCors: options.cors.expected,
- inputName: options.inputName
- },
- proxy: {
- onCancel: options.onCancel,
- getName: getName,
- getUuid: getUuid,
- log: log
- }
- }));
+
+ return id;
+ }
};
- qq.traditional = qq.traditional || {};
- qq.traditional.XhrUploadHandler = function(spec, proxy) {
- "use strict";
- var handler = this, getName = proxy.getName, getSize = proxy.getSize, getUuid = proxy.getUuid, log = proxy.log, multipart = spec.forceMultipart || spec.paramsInBody, addChunkingSpecificParams = function(id, params, chunkData) {
- var size = getSize(id), name = getName(id);
- params[spec.chunking.paramNames.partIndex] = chunkData.part;
- params[spec.chunking.paramNames.partByteOffset] = chunkData.start;
- params[spec.chunking.paramNames.chunkSize] = chunkData.size;
- params[spec.chunking.paramNames.totalParts] = chunkData.count;
- params[spec.totalFileSizeName] = size;
- if (multipart) {
- params[spec.filenameParam] = name;
- }
- }, allChunksDoneRequester = new qq.traditional.AllChunksDoneAjaxRequester({
- cors: spec.cors,
- endpoint: spec.chunking.success.endpoint,
- log: log
- }), createReadyStateChangedHandler = function(id, xhr) {
- var promise = new qq.Promise();
- xhr.onreadystatechange = function() {
- if (xhr.readyState === 4) {
- var result = onUploadOrChunkComplete(id, xhr);
- if (result.success) {
- promise.success(result.response, xhr);
- } else {
- promise.failure(result.response, xhr);
- }
- }
- };
- return promise;
- }, getChunksCompleteParams = function(id) {
- var params = spec.paramsStore.get(id), name = getName(id), size = getSize(id);
- params[spec.uuidName] = getUuid(id);
- params[spec.filenameParam] = name;
- params[spec.totalFileSizeName] = size;
- params[spec.chunking.paramNames.totalParts] = handler._getTotalChunks(id);
- return params;
- }, isErrorUploadResponse = function(xhr, response) {
- return qq.indexOf([ 200, 201, 202, 203, 204 ], xhr.status) < 0 || !response.success || response.reset;
- }, onUploadOrChunkComplete = function(id, xhr) {
- var response;
- log("xhr - server response received for " + id);
- log("responseText = " + xhr.responseText);
- response = parseResponse(true, xhr);
- return {
- success: !isErrorUploadResponse(xhr, response),
- response: response
- };
- }, parseResponse = function(upload, xhr) {
- var response = {};
- try {
- log(qq.format("Received response status {} with body: {}", xhr.status, xhr.responseText));
- response = qq.parseJson(xhr.responseText);
- } catch (error) {
- upload && log("Error when attempting to parse xhr response text (" + error.message + ")", "error");
- }
- return response;
- }, sendChunksCompleteRequest = function(id) {
- var promise = new qq.Promise();
- allChunksDoneRequester.complete(id, handler._createXhr(id), getChunksCompleteParams(id), spec.customHeaders.get(id)).then(function(xhr) {
- promise.success(parseResponse(false, xhr), xhr);
- }, function(xhr) {
- promise.failure(parseResponse(false, xhr), xhr);
- });
- return promise;
- }, setParamsAndGetEntityToSend = function(params, xhr, fileOrBlob, id) {
- var formData = new FormData(), method = spec.method, endpoint = spec.endpointStore.get(id), name = getName(id), size = getSize(id);
- params[spec.uuidName] = getUuid(id);
- params[spec.filenameParam] = name;
- if (multipart) {
- params[spec.totalFileSizeName] = size;
- }
- if (!spec.paramsInBody) {
- if (!multipart) {
- params[spec.inputName] = name;
- }
- endpoint = qq.obj2url(params, endpoint);
- }
- xhr.open(method, endpoint, true);
- if (spec.cors.expected && spec.cors.sendCredentials) {
- xhr.withCredentials = true;
- }
- if (multipart) {
- if (spec.paramsInBody) {
- qq.obj2FormData(params, formData);
- }
- formData.append(spec.inputName, fileOrBlob);
- return formData;
- }
- return fileOrBlob;
- }, setUploadHeaders = function(id, xhr) {
- var extraHeaders = spec.customHeaders.get(id), fileOrBlob = handler.getFile(id);
- xhr.setRequestHeader("Accept", "application/json");
- xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
- xhr.setRequestHeader("Cache-Control", "no-cache");
- if (!multipart) {
- xhr.setRequestHeader("Content-Type", "application/octet-stream");
- xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type);
- }
- qq.each(extraHeaders, function(name, val) {
- xhr.setRequestHeader(name, val);
- });
- };
- qq.extend(this, {
- uploadChunk: function(id, chunkIdx, resuming) {
- var chunkData = handler._getChunkData(id, chunkIdx), xhr = handler._createXhr(id, chunkIdx), size = getSize(id), promise, toSend, params;
- promise = createReadyStateChangedHandler(id, xhr);
- handler._registerProgressHandler(id, chunkIdx, chunkData.size);
- params = spec.paramsStore.get(id);
- addChunkingSpecificParams(id, params, chunkData);
- if (resuming) {
- params[spec.resume.paramNames.resuming] = true;
- }
- toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
- setUploadHeaders(id, xhr);
- xhr.send(toSend);
- return promise;
- },
- uploadFile: function(id) {
- var fileOrBlob = handler.getFile(id), promise, xhr, params, toSend;
- xhr = handler._createXhr(id);
- handler._registerProgressHandler(id);
- promise = createReadyStateChangedHandler(id, xhr);
- params = spec.paramsStore.get(id);
- toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
- setUploadHeaders(id, xhr);
- xhr.send(toSend);
- return promise;
- }
- });
- qq.extend(this, new qq.XhrUploadHandler({
- options: qq.extend({
- namespace: "traditional"
- }, spec),
- proxy: qq.extend({
- getEndpoint: spec.endpointStore.get
- }, proxy)
- }));
- qq.override(this, function(super_) {
- return {
- finalizeChunks: function(id) {
- if (spec.chunking.success.endpoint) {
- return sendChunksCompleteRequest(id);
- } else {
- return super_.finalizeChunks(id, qq.bind(parseResponse, this, true));
- }
- }
- };
- });
- };
- qq.traditional.AllChunksDoneAjaxRequester = function(o) {
- "use strict";
- var requester, method = "POST", options = {
- cors: {
- allowXdr: false,
- expected: false,
- sendCredentials: false
- },
- endpoint: null,
- log: function(str, level) {}
- }, promises = {}, endpointHandler = {
- get: function(id) {
- return options.endpoint;
- }
- };
- qq.extend(options, o);
- requester = qq.extend(this, new qq.AjaxRequester({
- acceptHeader: "application/json",
- validMethods: [ method ],
- method: method,
- endpointStore: endpointHandler,
- allowXRequestedWithAndCacheControl: false,
- cors: options.cors,
- log: options.log,
- onComplete: function(id, xhr, isError) {
- var promise = promises[id];
- delete promises[id];
- if (isError) {
- promise.failure(xhr);
- } else {
- promise.success(xhr);
- }
- }
- }));
- qq.extend(this, {
- complete: function(id, xhr, params, headers) {
- var promise = new qq.Promise();
- options.log("Submitting All Chunks Done request for " + id);
- promises[id] = promise;
- requester.initTransport(id).withParams(params).withHeaders(headers).send(xhr);
- return promise;
- }
- });
- };
- qq.DragAndDrop = function(o) {
- "use strict";
- var options, HIDE_ZONES_EVENT_NAME = "qq-hidezones", HIDE_BEFORE_ENTER_ATTR = "qq-hide-dropzone", uploadDropZones = [], droppedFiles = [], disposeSupport = new qq.DisposeSupport();
- options = {
- dropZoneElements: [],
- allowMultipleItems: true,
- classes: {
- dropActive: null
- },
- callbacks: new qq.DragAndDrop.callbacks()
- };
- qq.extend(options, o, true);
- function uploadDroppedFiles(files, uploadDropZone) {
- var filesAsArray = Array.prototype.slice.call(files);
- options.callbacks.dropLog("Grabbed " + files.length + " dropped files.");
- uploadDropZone.dropDisabled(false);
- options.callbacks.processingDroppedFilesComplete(filesAsArray, uploadDropZone.getElement());
+
+ return api;
+};
+/*globals qq, File, XMLHttpRequest, FormData, Blob*/
+qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) {
+ "use strict";
+
+ var options = o,
+ uploadComplete = uploadCompleteCallback,
+ log = logCallback,
+ fileState = [],
+ cookieItemDelimiter = "|",
+ chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(),
+ resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(),
+ resumeId = getResumeId(),
+ multipart = options.forceMultipart || options.paramsInBody,
+ api;
+
+
+ function addChunkingSpecificParams(id, params, chunkData) {
+ var size = api.getSize(id),
+ name = api.getName(id);
+
+ params[options.chunking.paramNames.partIndex] = chunkData.part;
+ params[options.chunking.paramNames.partByteOffset] = chunkData.start;
+ params[options.chunking.paramNames.chunkSize] = chunkData.size;
+ params[options.chunking.paramNames.totalParts] = chunkData.count;
+ params[options.totalFileSizeParamName] = size;
+
+ /**
+ * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
+ * or an empty string. So, we will need to include the actual file name as a param in this case.
+ */
+ if (multipart) {
+ params[options.chunking.paramNames.filename] = name;
}
- function traverseFileTree(entry) {
- var parseEntryPromise = new qq.Promise();
- if (entry.isFile) {
- entry.file(function(file) {
- var name = entry.name, fullPath = entry.fullPath, indexOfNameInFullPath = fullPath.indexOf(name);
- fullPath = fullPath.substr(0, indexOfNameInFullPath);
- if (fullPath.charAt(0) === "/") {
- fullPath = fullPath.substr(1);
- }
- file.qqPath = fullPath;
- droppedFiles.push(file);
- parseEntryPromise.success();
- }, function(fileError) {
- options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error");
- parseEntryPromise.failure();
- });
- } else if (entry.isDirectory) {
- getFilesInDirectory(entry).then(function allEntriesRead(entries) {
- var entriesLeft = entries.length;
- qq.each(entries, function(idx, entry) {
- traverseFileTree(entry).done(function() {
- entriesLeft -= 1;
- if (entriesLeft === 0) {
- parseEntryPromise.success();
- }
- });
- });
- if (!entries.length) {
- parseEntryPromise.success();
- }
- }, function readFailure(fileError) {
- options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error");
- parseEntryPromise.failure();
- });
- }
- return parseEntryPromise;
+ }
+
+ function addResumeSpecificParams(params) {
+ params[options.resume.paramNames.resuming] = true;
+ }
+
+ function getChunk(fileOrBlob, startByte, endByte) {
+ if (fileOrBlob.slice) {
+ return fileOrBlob.slice(startByte, endByte);
}
- function getFilesInDirectory(entry, reader, accumEntries, existingPromise) {
- var promise = existingPromise || new qq.Promise(), dirReader = reader || entry.createReader();
- dirReader.readEntries(function readSuccess(entries) {
- var newEntries = accumEntries ? accumEntries.concat(entries) : entries;
- if (entries.length) {
- setTimeout(function() {
- getFilesInDirectory(entry, dirReader, newEntries, promise);
- }, 0);
- } else {
- promise.success(newEntries);
- }
- }, promise.failure);
- return promise;
+ else if (fileOrBlob.mozSlice) {
+ return fileOrBlob.mozSlice(startByte, endByte);
}
- function handleDataTransfer(dataTransfer, uploadDropZone) {
- var pendingFolderPromises = [], handleDataTransferPromise = new qq.Promise();
- options.callbacks.processingDroppedFiles();
- uploadDropZone.dropDisabled(true);
- if (dataTransfer.files.length > 1 && !options.allowMultipleItems) {
- options.callbacks.processingDroppedFilesComplete([]);
- options.callbacks.dropError("tooManyFilesError", "");
- uploadDropZone.dropDisabled(false);
- handleDataTransferPromise.failure();
- } else {
- droppedFiles = [];
- if (qq.isFolderDropSupported(dataTransfer)) {
- qq.each(dataTransfer.items, function(idx, item) {
- var entry = item.webkitGetAsEntry();
- if (entry) {
- if (entry.isFile) {
- droppedFiles.push(item.getAsFile());
- } else {
- pendingFolderPromises.push(traverseFileTree(entry).done(function() {
- pendingFolderPromises.pop();
- if (pendingFolderPromises.length === 0) {
- handleDataTransferPromise.success();
- }
- }));
- }
- }
- });
- } else {
- droppedFiles = dataTransfer.files;
- }
- if (pendingFolderPromises.length === 0) {
- handleDataTransferPromise.success();
- }
- }
- return handleDataTransferPromise;
+ else if (fileOrBlob.webkitSlice) {
+ return fileOrBlob.webkitSlice(startByte, endByte);
}
- function setupDropzone(dropArea) {
- var dropZone = new qq.UploadDropZone({
- HIDE_ZONES_EVENT_NAME: HIDE_ZONES_EVENT_NAME,
- element: dropArea,
- onEnter: function(e) {
- qq(dropArea).addClass(options.classes.dropActive);
- e.stopPropagation();
- },
- onLeaveNotDescendants: function(e) {
- qq(dropArea).removeClass(options.classes.dropActive);
- },
- onDrop: function(e) {
- handleDataTransfer(e.dataTransfer, dropZone).then(function() {
- uploadDroppedFiles(droppedFiles, dropZone);
- }, function() {
- options.callbacks.dropLog("Drop event DataTransfer parsing failed. No files will be uploaded.", "error");
- });
- }
- });
- disposeSupport.addDisposer(function() {
- dropZone.dispose();
- });
- qq(dropArea).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropArea).hide();
- uploadDropZones.push(dropZone);
- return dropZone;
- }
- function isFileDrag(dragEvent) {
- var fileDrag;
- qq.each(dragEvent.dataTransfer.types, function(key, val) {
- if (val === "Files") {
- fileDrag = true;
- return false;
- }
- });
- return fileDrag;
- }
- function leavingDocumentOut(e) {
- if (qq.safari()) {
- return e.x < 0 || e.y < 0;
- }
- return e.x === 0 && e.y === 0;
- }
- function setupDragDrop() {
- var dropZones = options.dropZoneElements, maybeHideDropZones = function() {
- setTimeout(function() {
- qq.each(dropZones, function(idx, dropZone) {
- qq(dropZone).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropZone).hide();
- qq(dropZone).removeClass(options.classes.dropActive);
- });
- }, 10);
- };
- qq.each(dropZones, function(idx, dropZone) {
- var uploadDropZone = setupDropzone(dropZone);
- if (dropZones.length && qq.supportedFeatures.fileDrop) {
- disposeSupport.attach(document, "dragenter", function(e) {
- if (!uploadDropZone.dropDisabled() && isFileDrag(e)) {
- qq.each(dropZones, function(idx, dropZone) {
- if (dropZone instanceof HTMLElement && qq(dropZone).hasAttribute(HIDE_BEFORE_ENTER_ATTR)) {
- qq(dropZone).css({
- display: "block"
- });
- }
- });
- }
- });
- }
- });
- disposeSupport.attach(document, "dragleave", function(e) {
- if (leavingDocumentOut(e)) {
- maybeHideDropZones();
- }
- });
- disposeSupport.attach(qq(document).children()[0], "mouseenter", function(e) {
- maybeHideDropZones();
- });
- disposeSupport.attach(document, "drop", function(e) {
- if (isFileDrag(e)) {
- e.preventDefault();
- maybeHideDropZones();
- }
- });
- disposeSupport.attach(document, HIDE_ZONES_EVENT_NAME, maybeHideDropZones);
- }
- setupDragDrop();
- qq.extend(this, {
- setupExtraDropzone: function(element) {
- options.dropZoneElements.push(element);
- setupDropzone(element);
- },
- removeDropzone: function(element) {
- var i, dzs = options.dropZoneElements;
- for (i in dzs) {
- if (dzs[i] === element) {
- return dzs.splice(i, 1);
- }
- }
- },
- dispose: function() {
- disposeSupport.dispose();
- qq.each(uploadDropZones, function(idx, dropZone) {
- dropZone.dispose();
- });
- }
- });
- };
- qq.DragAndDrop.callbacks = function() {
- "use strict";
+ }
+
+ function getChunkData(id, chunkIndex) {
+ var chunkSize = options.chunking.partSize,
+ fileSize = api.getSize(id),
+ fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
+ startBytes = chunkSize * chunkIndex,
+ endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize,
+ totalChunks = getTotalChunks(id);
+
return {
- processingDroppedFiles: function() {},
- processingDroppedFilesComplete: function(files, targetEl) {},
- dropError: function(code, errorSpecifics) {
- qq.log("Drag & drop error code '" + code + " with these specifics: '" + errorSpecifics + "'", "error");
- },
- dropLog: function(message, level) {
- qq.log(message, level);
- }
+ part: chunkIndex,
+ start: startBytes,
+ end: endBytes,
+ count: totalChunks,
+ blob: getChunk(fileOrBlob, startBytes, endBytes),
+ size: endBytes - startBytes
};
- };
- qq.UploadDropZone = function(o) {
- "use strict";
- var disposeSupport = new qq.DisposeSupport(), options, element, preventDrop, dropOutsideDisabled;
- options = {
- element: null,
- onEnter: function(e) {},
- onLeave: function(e) {},
- onLeaveNotDescendants: function(e) {},
- onDrop: function(e) {}
- };
- qq.extend(options, o);
- element = options.element;
- function dragoverShouldBeCanceled() {
- return qq.safari() || qq.firefox() && qq.windows();
- }
- function disableDropOutside(e) {
- if (!dropOutsideDisabled) {
- if (dragoverShouldBeCanceled) {
- disposeSupport.attach(document, "dragover", function(e) {
- e.preventDefault();
- });
- } else {
- disposeSupport.attach(document, "dragover", function(e) {
- if (e.dataTransfer) {
- e.dataTransfer.dropEffect = "none";
- e.preventDefault();
- }
- });
- }
- dropOutsideDisabled = true;
+ }
+
+ function getTotalChunks(id) {
+ var fileSize = api.getSize(id),
+ chunkSize = options.chunking.partSize;
+
+ return Math.ceil(fileSize / chunkSize);
+ }
+
+ function createXhr(id) {
+ var xhr = new XMLHttpRequest();
+
+ fileState[id].xhr = xhr;
+
+ return xhr;
+ }
+
+ function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
+ var formData = new FormData(),
+ method = options.demoMode ? "GET" : "POST",
+ endpoint = options.endpointStore.getEndpoint(id),
+ url = endpoint,
+ name = api.getName(id),
+ size = api.getSize(id),
+ blobData = fileState[id].blobData;
+
+ params[options.uuidParamName] = fileState[id].uuid;
+
+ if (multipart) {
+ params[options.totalFileSizeParamName] = size;
+
+ if (blobData) {
+ /**
+ * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
+ * or an empty string. So, we will need to include the actual file name as a param in this case.
+ */
+ params[options.blobs.paramNames.name] = blobData.name;
}
}
- function isValidFileDrag(e) {
- if (!qq.supportedFeatures.fileDrop) {
- return false;
+
+ //build query string
+ if (!options.paramsInBody) {
+ if (!multipart) {
+ params[options.inputName] = name;
}
- var effectTest, dt = e.dataTransfer, isSafari = qq.safari();
- effectTest = qq.ie() && qq.supportedFeatures.fileDrop ? true : dt.effectAllowed !== "none";
- return dt && effectTest && (dt.files && dt.files.length || !isSafari && dt.types.contains && dt.types.contains("Files") || dt.types.includes && dt.types.includes("Files"));
+ url = qq.obj2url(params, endpoint);
}
- function isOrSetDropDisabled(isDisabled) {
- if (isDisabled !== undefined) {
- preventDrop = isDisabled;
- }
- return preventDrop;
+
+ xhr.open(method, url, true);
+
+ if (options.cors.expected && options.cors.sendCredentials) {
+ xhr.withCredentials = true;
}
- function triggerHidezonesEvent() {
- var hideZonesEvent;
- function triggerUsingOldApi() {
- hideZonesEvent = document.createEvent("Event");
- hideZonesEvent.initEvent(options.HIDE_ZONES_EVENT_NAME, true, true);
+
+ if (multipart) {
+ if (options.paramsInBody) {
+ qq.obj2FormData(params, formData);
}
- if (window.CustomEvent) {
- try {
- hideZonesEvent = new CustomEvent(options.HIDE_ZONES_EVENT_NAME);
- } catch (err) {
- triggerUsingOldApi();
- }
- } else {
- triggerUsingOldApi();
- }
- document.dispatchEvent(hideZonesEvent);
+
+ formData.append(options.inputName, fileOrBlob);
+ return formData;
}
- function attachEvents() {
- disposeSupport.attach(element, "dragover", function(e) {
- if (!isValidFileDrag(e)) {
- return;
- }
- var effect = qq.ie() && qq.supportedFeatures.fileDrop ? null : e.dataTransfer.effectAllowed;
- if (effect === "move" || effect === "linkMove") {
- e.dataTransfer.dropEffect = "move";
- } else {
- e.dataTransfer.dropEffect = "copy";
- }
- e.stopPropagation();
- e.preventDefault();
- });
- disposeSupport.attach(element, "dragenter", function(e) {
- if (!isOrSetDropDisabled()) {
- if (!isValidFileDrag(e)) {
- return;
- }
- options.onEnter(e);
- }
- });
- disposeSupport.attach(element, "dragleave", function(e) {
- if (!isValidFileDrag(e)) {
- return;
- }
- options.onLeave(e);
- var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
- if (qq(this).contains(relatedTarget)) {
- return;
- }
- options.onLeaveNotDescendants(e);
- });
- disposeSupport.attach(element, "drop", function(e) {
- if (!isOrSetDropDisabled()) {
- if (!isValidFileDrag(e)) {
- return;
- }
- e.preventDefault();
- e.stopPropagation();
- options.onDrop(e);
- triggerHidezonesEvent();
- }
- });
+
+ return fileOrBlob;
+ }
+
+ function setHeaders(id, xhr) {
+ var extraHeaders = options.customHeaders,
+ fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
+
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.setRequestHeader("Cache-Control", "no-cache");
+
+ if (!multipart) {
+ xhr.setRequestHeader("Content-Type", "application/octet-stream");
+ //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
+ xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type);
}
- disableDropOutside();
- attachEvents();
- qq.extend(this, {
- dropDisabled: function(isDisabled) {
- return isOrSetDropDisabled(isDisabled);
- },
- dispose: function() {
- disposeSupport.dispose();
- },
- getElement: function() {
- return element;
- }
+
+ qq.each(extraHeaders, function(name, val) {
+ xhr.setRequestHeader(name, val);
});
- this._testing = {};
- this._testing.isValidFileDrag = isValidFileDrag;
- };
- (function() {
- "use strict";
- qq.uiPublicApi = {
- addInitialFiles: function(cannedFileList) {
- this._parent.prototype.addInitialFiles.apply(this, arguments);
- this._templating.addCacheToDom();
- },
- clearStoredFiles: function() {
- this._parent.prototype.clearStoredFiles.apply(this, arguments);
- this._templating.clearFiles();
- },
- addExtraDropzone: function(element) {
- this._dnd && this._dnd.setupExtraDropzone(element);
- },
- removeExtraDropzone: function(element) {
- if (this._dnd) {
- return this._dnd.removeDropzone(element);
- }
- },
- getItemByFileId: function(id) {
- if (!this._templating.isHiddenForever(id)) {
- return this._templating.getFileContainer(id);
- }
- },
- reset: function() {
- this._parent.prototype.reset.apply(this, arguments);
- this._templating.reset();
- if (!this._options.button && this._templating.getButton()) {
- this._defaultButtonId = this._createUploadButton({
- element: this._templating.getButton(),
- title: this._options.text.fileInputTitle
- }).getButtonId();
- }
- if (this._dnd) {
- this._dnd.dispose();
- this._dnd = this._setupDragAndDrop();
- }
- this._totalFilesInBatch = 0;
- this._filesInBatchAddedToUi = 0;
- this._setupClickAndEditEventHandlers();
- },
- setName: function(id, newName) {
- var formattedFilename = this._options.formatFileName(newName);
- this._parent.prototype.setName.apply(this, arguments);
- this._templating.updateFilename(id, formattedFilename);
- },
- pauseUpload: function(id) {
- var paused = this._parent.prototype.pauseUpload.apply(this, arguments);
- paused && this._templating.uploadPaused(id);
- return paused;
- },
- continueUpload: function(id) {
- var continued = this._parent.prototype.continueUpload.apply(this, arguments);
- continued && this._templating.uploadContinued(id);
- return continued;
- },
- getId: function(fileContainerOrChildEl) {
- return this._templating.getFileId(fileContainerOrChildEl);
- },
- getDropTarget: function(fileId) {
- var file = this.getFile(fileId);
- return file.qqDropTarget;
+ }
+
+ function handleCompletedItem(id, response, xhr) {
+ var name = api.getName(id),
+ size = api.getSize(id);
+
+ fileState[id].attemptingResume = false;
+
+ options.onProgress(id, name, size, size);
+
+ options.onComplete(id, name, response, xhr);
+ delete fileState[id].xhr;
+ uploadComplete(id);
+ }
+
+ function uploadNextChunk(id) {
+ var chunkIdx = fileState[id].remainingChunkIdxs[0],
+ chunkData = getChunkData(id, chunkIdx),
+ xhr = createXhr(id),
+ size = api.getSize(id),
+ name = api.getName(id),
+ toSend, params;
+
+ if (fileState[id].loaded === undefined) {
+ fileState[id].loaded = 0;
+ }
+
+ if (resumeEnabled && fileState[id].file) {
+ persistChunkData(id, chunkData);
+ }
+
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
+
+ xhr.upload.onprogress = function(e) {
+ if (e.lengthComputable) {
+ var totalLoaded = e.loaded + fileState[id].loaded,
+ estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total);
+
+ options.onProgress(id, name, totalLoaded, estTotalRequestsSize);
}
};
- qq.uiPrivateApi = {
- _getButton: function(buttonId) {
- var button = this._parent.prototype._getButton.apply(this, arguments);
- if (!button) {
- if (buttonId === this._defaultButtonId) {
- button = this._templating.getButton();
- }
- }
- return button;
- },
- _removeFileItem: function(fileId) {
- this._templating.removeFile(fileId);
- },
- _setupClickAndEditEventHandlers: function() {
- this._fileButtonsClickHandler = qq.FileButtonsClickHandler && this._bindFileButtonsClickEvent();
- this._focusinEventSupported = !qq.firefox();
- if (this._isEditFilenameEnabled()) {
- this._filenameClickHandler = this._bindFilenameClickEvent();
- this._filenameInputFocusInHandler = this._bindFilenameInputFocusInEvent();
- this._filenameInputFocusHandler = this._bindFilenameInputFocusEvent();
- }
- },
- _setupDragAndDrop: function() {
- var self = this, dropZoneElements = this._options.dragAndDrop.extraDropzones, templating = this._templating, defaultDropZone = templating.getDropZone();
- defaultDropZone && dropZoneElements.push(defaultDropZone);
- return new qq.DragAndDrop({
- dropZoneElements: dropZoneElements,
- allowMultipleItems: this._options.multiple,
- classes: {
- dropActive: this._options.classes.dropActive
- },
- callbacks: {
- processingDroppedFiles: function() {
- templating.showDropProcessing();
- },
- processingDroppedFilesComplete: function(files, targetEl) {
- templating.hideDropProcessing();
- qq.each(files, function(idx, file) {
- file.qqDropTarget = targetEl;
- });
- if (files.length) {
- self.addFiles(files, null, null);
- }
- },
- dropError: function(code, errorData) {
- self._itemError(code, errorData);
- },
- dropLog: function(message, level) {
- self.log(message, level);
- }
- }
- });
- },
- _bindFileButtonsClickEvent: function() {
- var self = this;
- return new qq.FileButtonsClickHandler({
- templating: this._templating,
- log: function(message, lvl) {
- self.log(message, lvl);
- },
- onDeleteFile: function(fileId) {
- self.deleteFile(fileId);
- },
- onCancel: function(fileId) {
- self.cancel(fileId);
- },
- onRetry: function(fileId) {
- self.retry(fileId);
- },
- onPause: function(fileId) {
- self.pauseUpload(fileId);
- },
- onContinue: function(fileId) {
- self.continueUpload(fileId);
- },
- onGetName: function(fileId) {
- return self.getName(fileId);
- }
- });
- },
- _isEditFilenameEnabled: function() {
- return this._templating.isEditFilenamePossible() && !this._options.autoUpload && qq.FilenameClickHandler && qq.FilenameInputFocusHandler && qq.FilenameInputFocusHandler;
- },
- _filenameEditHandler: function() {
- var self = this, templating = this._templating;
+
+ options.onUploadChunk(id, name, getChunkDataForCallback(chunkData));
+
+ params = options.paramsStore.getParams(id);
+ addChunkingSpecificParams(id, params, chunkData);
+
+ if (fileState[id].attemptingResume) {
+ addResumeSpecificParams(params);
+ }
+
+ toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
+ setHeaders(id, xhr);
+
+ log('Sending chunked upload request for item ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
+ xhr.send(toSend);
+ }
+
+ function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) {
+ var chunkData = getChunkData(id, chunkIdx),
+ blobSize = chunkData.size,
+ overhead = requestSize - blobSize,
+ size = api.getSize(id),
+ chunkCount = chunkData.count,
+ initialRequestOverhead = fileState[id].initialRequestOverhead,
+ overheadDiff = overhead - initialRequestOverhead;
+
+ fileState[id].lastRequestOverhead = overhead;
+
+ if (chunkIdx === 0) {
+ fileState[id].lastChunkIdxProgress = 0;
+ fileState[id].initialRequestOverhead = overhead;
+ fileState[id].estTotalRequestsSize = size + (chunkCount * overhead);
+ }
+ else if (fileState[id].lastChunkIdxProgress !== chunkIdx) {
+ fileState[id].lastChunkIdxProgress = chunkIdx;
+ fileState[id].estTotalRequestsSize += overheadDiff;
+ }
+
+ return fileState[id].estTotalRequestsSize;
+ }
+
+ function getLastRequestOverhead(id) {
+ if (multipart) {
+ return fileState[id].lastRequestOverhead;
+ }
+ else {
+ return 0;
+ }
+ }
+
+ function handleSuccessfullyCompletedChunk(id, response, xhr) {
+ var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
+ chunkData = getChunkData(id, chunkIdx);
+
+ fileState[id].attemptingResume = false;
+ fileState[id].loaded += chunkData.size + getLastRequestOverhead(id);
+
+ if (fileState[id].remainingChunkIdxs.length > 0) {
+ uploadNextChunk(id);
+ }
+ else {
+ if (resumeEnabled) {
+ deletePersistedChunkData(id);
+ }
+
+ handleCompletedItem(id, response, xhr);
+ }
+ }
+
+ function isErrorResponse(xhr, response) {
+ return xhr.status !== 200 || !response.success || response.reset;
+ }
+
+ function parseResponse(xhr) {
+ var response;
+
+ try {
+ response = qq.parseJson(xhr.responseText);
+ }
+ catch(error) {
+ log('Error when attempting to parse xhr response text (' + error + ')', 'error');
+ response = {};
+ }
+
+ return response;
+ }
+
+ function handleResetResponse(id) {
+ log('Server has ordered chunking effort to be restarted on next attempt for item ID ' + id, 'error');
+
+ if (resumeEnabled) {
+ deletePersistedChunkData(id);
+ fileState[id].attemptingResume = false;
+ }
+
+ fileState[id].remainingChunkIdxs = [];
+ delete fileState[id].loaded;
+ delete fileState[id].estTotalRequestsSize;
+ delete fileState[id].initialRequestOverhead;
+ }
+
+ function handleResetResponseOnResumeAttempt(id) {
+ fileState[id].attemptingResume = false;
+ log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", 'error');
+ handleResetResponse(id);
+ api.upload(id, true);
+ }
+
+ function handleNonResetErrorResponse(id, response, xhr) {
+ var name = api.getName(id);
+
+ if (options.onAutoRetry(id, name, response, xhr)) {
+ return;
+ }
+ else {
+ handleCompletedItem(id, response, xhr);
+ }
+ }
+
+ function onComplete(id, xhr) {
+ var response;
+
+ // the request was aborted/cancelled
+ if (!fileState[id]) {
+ return;
+ }
+
+ log("xhr - server response received for " + id);
+ log("responseText = " + xhr.responseText);
+ response = parseResponse(xhr);
+
+ if (isErrorResponse(xhr, response)) {
+ if (response.reset) {
+ handleResetResponse(id);
+ }
+
+ if (fileState[id].attemptingResume && response.reset) {
+ handleResetResponseOnResumeAttempt(id);
+ }
+ else {
+ handleNonResetErrorResponse(id, response, xhr);
+ }
+ }
+ else if (chunkFiles) {
+ handleSuccessfullyCompletedChunk(id, response, xhr);
+ }
+ else {
+ handleCompletedItem(id, response, xhr);
+ }
+ }
+
+ function getChunkDataForCallback(chunkData) {
+ return {
+ partIndex: chunkData.part,
+ startByte: chunkData.start + 1,
+ endByte: chunkData.end,
+ totalParts: chunkData.count
+ };
+ }
+
+ function getReadyStateChangeHandler(id, xhr) {
+ return function() {
+ if (xhr.readyState === 4) {
+ onComplete(id, xhr);
+ }
+ };
+ }
+
+ function persistChunkData(id, chunkData) {
+ var fileUuid = api.getUuid(id),
+ lastByteSent = fileState[id].loaded,
+ initialRequestOverhead = fileState[id].initialRequestOverhead,
+ estTotalRequestsSize = fileState[id].estTotalRequestsSize,
+ cookieName = getChunkDataCookieName(id),
+ cookieValue = fileUuid +
+ cookieItemDelimiter + chunkData.part +
+ cookieItemDelimiter + lastByteSent +
+ cookieItemDelimiter + initialRequestOverhead +
+ cookieItemDelimiter + estTotalRequestsSize,
+ cookieExpDays = options.resume.cookiesExpireIn;
+
+ qq.setCookie(cookieName, cookieValue, cookieExpDays);
+ }
+
+ function deletePersistedChunkData(id) {
+ if (fileState[id].file) {
+ var cookieName = getChunkDataCookieName(id);
+ qq.deleteCookie(cookieName);
+ }
+ }
+
+ function getPersistedChunkData(id) {
+ var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
+ filename = api.getName(id),
+ sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize;
+
+ if (chunkCookieValue) {
+ sections = chunkCookieValue.split(cookieItemDelimiter);
+
+ if (sections.length === 5) {
+ uuid = sections[0];
+ partIndex = parseInt(sections[1], 10);
+ lastByteSent = parseInt(sections[2], 10);
+ initialRequestOverhead = parseInt(sections[3], 10);
+ estTotalRequestsSize = parseInt(sections[4], 10);
+
return {
- templating: templating,
- log: function(message, lvl) {
- self.log(message, lvl);
- },
- onGetUploadStatus: function(fileId) {
- return self.getUploads({
- id: fileId
- }).status;
- },
- onGetName: function(fileId) {
- return self.getName(fileId);
- },
- onSetName: function(id, newName) {
- self.setName(id, newName);
- },
- onEditingStatusChange: function(id, isEditing) {
- var qqInput = qq(templating.getEditInput(id)), qqFileContainer = qq(templating.getFileContainer(id));
- if (isEditing) {
- qqInput.addClass("qq-editing");
- templating.hideFilename(id);
- templating.hideEditIcon(id);
- } else {
- qqInput.removeClass("qq-editing");
- templating.showFilename(id);
- templating.showEditIcon(id);
- }
- qqFileContainer.addClass("qq-temp").removeClass("qq-temp");
- }
+ uuid: uuid,
+ part: partIndex,
+ lastByteSent: lastByteSent,
+ initialRequestOverhead: initialRequestOverhead,
+ estTotalRequestsSize: estTotalRequestsSize
};
- },
- _onUploadStatusChange: function(id, oldStatus, newStatus) {
- this._parent.prototype._onUploadStatusChange.apply(this, arguments);
- if (this._isEditFilenameEnabled()) {
- if (this._templating.getFileContainer(id) && newStatus !== qq.status.SUBMITTED) {
- this._templating.markFilenameEditable(id);
- this._templating.hideEditIcon(id);
+ }
+ else {
+ log('Ignoring previously stored resume/chunk cookie for ' + filename + " - old cookie format", "warn");
+ }
+ }
+ }
+
+ function getChunkDataCookieName(id) {
+ var filename = api.getName(id),
+ fileSize = api.getSize(id),
+ maxChunkSize = options.chunking.partSize,
+ cookieName;
+
+ cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
+
+ if (resumeId !== undefined) {
+ cookieName += cookieItemDelimiter + resumeId;
+ }
+
+ return cookieName;
+ }
+
+ function getResumeId() {
+ if (options.resume.id !== null &&
+ options.resume.id !== undefined &&
+ !qq.isFunction(options.resume.id) &&
+ !qq.isObject(options.resume.id)) {
+
+ return options.resume.id;
+ }
+ }
+
+ function handleFileChunkingUpload(id, retry) {
+ var name = api.getName(id),
+ firstChunkIndex = 0,
+ persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex;
+
+ if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
+ fileState[id].remainingChunkIdxs = [];
+
+ if (resumeEnabled && !retry && fileState[id].file) {
+ persistedChunkInfoForResume = getPersistedChunkData(id);
+ if (persistedChunkInfoForResume) {
+ firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part);
+ if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) {
+ firstChunkIndex = persistedChunkInfoForResume.part;
+ fileState[id].uuid = persistedChunkInfoForResume.uuid;
+ fileState[id].loaded = persistedChunkInfoForResume.lastByteSent;
+ fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize;
+ fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead;
+ fileState[id].attemptingResume = true;
+ log('Resuming ' + name + " at partition index " + firstChunkIndex);
}
}
- if (oldStatus === qq.status.UPLOAD_RETRYING && newStatus === qq.status.UPLOADING) {
- this._templating.hideRetry(id);
- this._templating.setStatusText(id);
- qq(this._templating.getFileContainer(id)).removeClass(this._classes.retrying);
- } else if (newStatus === qq.status.UPLOAD_FAILED) {
- this._templating.hidePause(id);
- }
- },
- _bindFilenameInputFocusInEvent: function() {
- var spec = qq.extend({}, this._filenameEditHandler());
- return new qq.FilenameInputFocusInHandler(spec);
- },
- _bindFilenameInputFocusEvent: function() {
- var spec = qq.extend({}, this._filenameEditHandler());
- return new qq.FilenameInputFocusHandler(spec);
- },
- _bindFilenameClickEvent: function() {
- var spec = qq.extend({}, this._filenameEditHandler());
- return new qq.FilenameClickHandler(spec);
- },
- _storeForLater: function(id) {
- this._parent.prototype._storeForLater.apply(this, arguments);
- this._templating.hideSpinner(id);
- },
- _onAllComplete: function(successful, failed) {
- this._parent.prototype._onAllComplete.apply(this, arguments);
- this._templating.resetTotalProgress();
- },
- _onSubmit: function(id, name) {
- var file = this.getFile(id);
- if (file && file.qqPath && this._options.dragAndDrop.reportDirectoryPaths) {
- this._paramsStore.addReadOnly(id, {
- qqpath: file.qqPath
- });
- }
- this._parent.prototype._onSubmit.apply(this, arguments);
- this._addToList(id, name);
- },
- _onSubmitted: function(id) {
- if (this._isEditFilenameEnabled()) {
- this._templating.markFilenameEditable(id);
- this._templating.showEditIcon(id);
- if (!this._focusinEventSupported) {
- this._filenameInputFocusHandler.addHandler(this._templating.getEditInput(id));
- }
- }
- },
- _onProgress: function(id, name, loaded, total) {
- this._parent.prototype._onProgress.apply(this, arguments);
- this._templating.updateProgress(id, loaded, total);
- if (total === 0 || Math.round(loaded / total * 100) === 100) {
- this._templating.hideCancel(id);
- this._templating.hidePause(id);
- this._templating.hideProgress(id);
- this._templating.setStatusText(id, this._options.text.waitingForResponse);
- this._displayFileSize(id);
- } else {
- this._displayFileSize(id, loaded, total);
- }
- },
- _onTotalProgress: function(loaded, total) {
- this._parent.prototype._onTotalProgress.apply(this, arguments);
- this._templating.updateTotalProgress(loaded, total);
- },
- _onComplete: function(id, name, result, xhr) {
- var parentRetVal = this._parent.prototype._onComplete.apply(this, arguments), templating = this._templating, fileContainer = templating.getFileContainer(id), self = this;
- function completeUpload(result) {
- if (!fileContainer) {
- return;
- }
- templating.setStatusText(id);
- qq(fileContainer).removeClass(self._classes.retrying);
- templating.hideProgress(id);
- if (self.getUploads({
- id: id
- }).status !== qq.status.UPLOAD_FAILED) {
- templating.hideCancel(id);
- }
- templating.hideSpinner(id);
- if (result.success) {
- self._markFileAsSuccessful(id);
- } else {
- qq(fileContainer).addClass(self._classes.fail);
- templating.showCancel(id);
- if (templating.isRetryPossible() && !self._preventRetries[id]) {
- qq(fileContainer).addClass(self._classes.retryable);
- templating.showRetry(id);
- }
- self._controlFailureTextDisplay(id, result);
- }
- }
- if (parentRetVal instanceof qq.Promise) {
- parentRetVal.done(function(newResult) {
- completeUpload(newResult);
- });
- } else {
- completeUpload(result);
- }
- return parentRetVal;
- },
- _markFileAsSuccessful: function(id) {
- var templating = this._templating;
- if (this._isDeletePossible()) {
- templating.showDeleteButton(id);
- }
- qq(templating.getFileContainer(id)).addClass(this._classes.success);
- this._maybeUpdateThumbnail(id);
- },
- _onUploadPrep: function(id) {
- this._parent.prototype._onUploadPrep.apply(this, arguments);
- this._templating.showSpinner(id);
- },
- _onUpload: function(id, name) {
- var parentRetVal = this._parent.prototype._onUpload.apply(this, arguments);
- this._templating.showSpinner(id);
- return parentRetVal;
- },
- _onUploadChunk: function(id, chunkData) {
- this._parent.prototype._onUploadChunk.apply(this, arguments);
- if (chunkData.partIndex > 0 && this._handler.isResumable(id)) {
- this._templating.allowPause(id);
- }
- },
- _onCancel: function(id, name) {
- this._parent.prototype._onCancel.apply(this, arguments);
- this._removeFileItem(id);
- if (this._getNotFinished() === 0) {
- this._templating.resetTotalProgress();
- }
- },
- _onBeforeAutoRetry: function(id) {
- var retryNumForDisplay, maxAuto, retryNote;
- this._parent.prototype._onBeforeAutoRetry.apply(this, arguments);
- this._showCancelLink(id);
- if (this._options.retry.showAutoRetryNote) {
- retryNumForDisplay = this._autoRetries[id];
- maxAuto = this._options.retry.maxAutoAttempts;
- retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
- retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
- this._templating.setStatusText(id, retryNote);
- qq(this._templating.getFileContainer(id)).addClass(this._classes.retrying);
- }
- },
- _onBeforeManualRetry: function(id) {
- if (this._parent.prototype._onBeforeManualRetry.apply(this, arguments)) {
- this._templating.resetProgress(id);
- qq(this._templating.getFileContainer(id)).removeClass(this._classes.fail);
- this._templating.setStatusText(id);
- this._templating.showSpinner(id);
- this._showCancelLink(id);
- return true;
- } else {
- qq(this._templating.getFileContainer(id)).addClass(this._classes.retryable);
- this._templating.showRetry(id);
- return false;
- }
- },
- _onSubmitDelete: function(id) {
- var onSuccessCallback = qq.bind(this._onSubmitDeleteSuccess, this);
- this._parent.prototype._onSubmitDelete.call(this, id, onSuccessCallback);
- },
- _onSubmitDeleteSuccess: function(id, uuid, additionalMandatedParams) {
- if (this._options.deleteFile.forceConfirm) {
- this._showDeleteConfirm.apply(this, arguments);
- } else {
- this._sendDeleteRequest.apply(this, arguments);
- }
- },
- _onDeleteComplete: function(id, xhr, isError) {
- this._parent.prototype._onDeleteComplete.apply(this, arguments);
- this._templating.hideSpinner(id);
- if (isError) {
- this._templating.setStatusText(id, this._options.deleteFile.deletingFailedText);
- this._templating.showDeleteButton(id);
- } else {
- this._removeFileItem(id);
- }
- },
- _sendDeleteRequest: function(id, uuid, additionalMandatedParams) {
- this._templating.hideDeleteButton(id);
- this._templating.showSpinner(id);
- this._templating.setStatusText(id, this._options.deleteFile.deletingStatusText);
- this._deleteHandler.sendDelete.apply(this, arguments);
- },
- _showDeleteConfirm: function(id, uuid, mandatedParams) {
- var fileName = this.getName(id), confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName), uuid = this.getUuid(id), deleteRequestArgs = arguments, self = this, retVal;
- retVal = this._options.showConfirm(confirmMessage);
- if (qq.isGenericPromise(retVal)) {
- retVal.then(function() {
- self._sendDeleteRequest.apply(self, deleteRequestArgs);
- });
- } else if (retVal !== false) {
- self._sendDeleteRequest.apply(self, deleteRequestArgs);
- }
- },
- _addToList: function(id, name, canned) {
- var prependData, prependIndex = 0, dontDisplay = this._handler.isProxied(id) && this._options.scaling.hideScaled, record;
- if (this._options.display.prependFiles) {
- if (this._totalFilesInBatch > 1 && this._filesInBatchAddedToUi > 0) {
- prependIndex = this._filesInBatchAddedToUi - 1;
- }
- prependData = {
- index: prependIndex
- };
- }
- if (!canned) {
- if (this._options.disableCancelForFormUploads && !qq.supportedFeatures.ajaxUploading) {
- this._templating.disableCancel();
- }
- if (!this._options.multiple) {
- record = this.getUploads({
- id: id
- });
- this._handledProxyGroup = this._handledProxyGroup || record.proxyGroupId;
- if (record.proxyGroupId !== this._handledProxyGroup || !record.proxyGroupId) {
- this._handler.cancelAll();
- this._clearList();
- this._handledProxyGroup = null;
- }
- }
- }
- if (canned) {
- this._templating.addFileToCache(id, this._options.formatFileName(name), prependData, dontDisplay);
- this._templating.updateThumbnail(id, this._thumbnailUrls[id], true, this._options.thumbnails.customResizer);
- } else {
- this._templating.addFile(id, this._options.formatFileName(name), prependData, dontDisplay);
- this._templating.generatePreview(id, this.getFile(id), this._options.thumbnails.customResizer);
- }
- this._filesInBatchAddedToUi += 1;
- if (canned || this._options.display.fileSizeOnSubmit && qq.supportedFeatures.ajaxUploading) {
- this._displayFileSize(id);
- }
- },
- _clearList: function() {
- this._templating.clearFiles();
- this.clearStoredFiles();
- },
- _displayFileSize: function(id, loadedSize, totalSize) {
- var size = this.getSize(id), sizeForDisplay = this._formatSize(size);
- if (size >= 0) {
- if (loadedSize !== undefined && totalSize !== undefined) {
- sizeForDisplay = this._formatProgress(loadedSize, totalSize);
- }
- this._templating.updateSize(id, sizeForDisplay);
- }
- },
- _formatProgress: function(uploadedSize, totalSize) {
- var message = this._options.text.formatProgress;
- function r(name, replacement) {
- message = message.replace(name, replacement);
- }
- r("{percent}", Math.round(uploadedSize / totalSize * 100));
- r("{total_size}", this._formatSize(totalSize));
- return message;
- },
- _controlFailureTextDisplay: function(id, response) {
- var mode, responseProperty, failureReason;
- mode = this._options.failedUploadTextDisplay.mode;
- responseProperty = this._options.failedUploadTextDisplay.responseProperty;
- if (mode === "custom") {
- failureReason = response[responseProperty];
- if (!failureReason) {
- failureReason = this._options.text.failUpload;
- }
- this._templating.setStatusText(id, failureReason);
- if (this._options.failedUploadTextDisplay.enableTooltip) {
- this._showTooltip(id, failureReason);
- }
- } else if (mode === "default") {
- this._templating.setStatusText(id, this._options.text.failUpload);
- } else if (mode !== "none") {
- this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", "warn");
- }
- },
- _showTooltip: function(id, text) {
- this._templating.getFileContainer(id).title = text;
- },
- _showCancelLink: function(id) {
- if (!this._options.disableCancelForFormUploads || qq.supportedFeatures.ajaxUploading) {
- this._templating.showCancel(id);
- }
- },
- _itemError: function(code, name, item) {
- var message = this._parent.prototype._itemError.apply(this, arguments);
- this._options.showMessage(message);
- },
- _batchError: function(message) {
- this._parent.prototype._batchError.apply(this, arguments);
- this._options.showMessage(message);
- },
- _setupPastePrompt: function() {
- var self = this;
- this._options.callbacks.onPasteReceived = function() {
- var message = self._options.paste.namePromptMessage, defaultVal = self._options.paste.defaultName;
- return self._options.showPrompt(message, defaultVal);
- };
- },
- _fileOrBlobRejected: function(id, name) {
- this._totalFilesInBatch -= 1;
- this._parent.prototype._fileOrBlobRejected.apply(this, arguments);
- },
- _prepareItemsForUpload: function(items, params, endpoint) {
- this._totalFilesInBatch = items.length;
- this._filesInBatchAddedToUi = 0;
- this._parent.prototype._prepareItemsForUpload.apply(this, arguments);
- },
- _maybeUpdateThumbnail: function(fileId) {
- var thumbnailUrl = this._thumbnailUrls[fileId], fileStatus = this.getUploads({
- id: fileId
- }).status;
- if (fileStatus !== qq.status.DELETED && (thumbnailUrl || this._options.thumbnails.placeholders.waitUntilResponse || !qq.supportedFeatures.imagePreviews)) {
- this._templating.updateThumbnail(fileId, thumbnailUrl, this._options.thumbnails.customResizer);
- }
- },
- _addCannedFile: function(sessionData) {
- var id = this._parent.prototype._addCannedFile.apply(this, arguments);
- this._addToList(id, this.getName(id), true);
- this._templating.hideSpinner(id);
- this._templating.hideCancel(id);
- this._markFileAsSuccessful(id);
- return id;
- },
- _setSize: function(id, newSize) {
- this._parent.prototype._setSize.apply(this, arguments);
- this._templating.updateSize(id, this._formatSize(newSize));
- },
- _sessionRequestComplete: function() {
- this._templating.addCacheToDom();
- this._parent.prototype._sessionRequestComplete.apply(this, arguments);
+ }
+
+ for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
+ fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
+ }
+ }
+
+ uploadNextChunk(id);
+ }
+
+ function handleStandardFileUpload(id) {
+ var fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
+ name = api.getName(id),
+ xhr, params, toSend;
+
+ fileState[id].loaded = 0;
+
+ xhr = createXhr(id);
+
+ xhr.upload.onprogress = function(e){
+ if (e.lengthComputable){
+ fileState[id].loaded = e.loaded;
+ options.onProgress(id, name, e.loaded, e.total);
}
};
- })();
- qq.FineUploader = function(o, namespace) {
- "use strict";
- var self = this;
- this._parent = namespace ? qq[namespace].FineUploaderBasic : qq.FineUploaderBasic;
- this._parent.apply(this, arguments);
- qq.extend(this._options, {
- element: null,
- button: null,
- listElement: null,
- dragAndDrop: {
- extraDropzones: [],
- reportDirectoryPaths: false
- },
- text: {
- formatProgress: "{percent}% of {total_size}",
- failUpload: "Upload failed",
- waitingForResponse: "Processing...",
- paused: "Paused"
- },
- template: "qq-template",
- classes: {
- retrying: "qq-upload-retrying",
- retryable: "qq-upload-retryable",
- success: "qq-upload-success",
- fail: "qq-upload-fail",
- editable: "qq-editable",
- hide: "qq-hide",
- dropActive: "qq-upload-drop-area-active"
- },
- failedUploadTextDisplay: {
- mode: "default",
- responseProperty: "error",
- enableTooltip: true
- },
- messages: {
- tooManyFilesError: "You may only drop one file",
- unsupportedBrowser: "Unrecoverable error - this browser does not permit file uploading of any kind."
- },
- retry: {
- showAutoRetryNote: true,
- autoRetryNote: "Retrying {retryNum}/{maxAuto}..."
- },
- deleteFile: {
- forceConfirm: false,
- confirmMessage: "Are you sure you want to delete {filename}?",
- deletingStatusText: "Deleting...",
- deletingFailedText: "Delete failed"
- },
- display: {
- fileSizeOnSubmit: false,
- prependFiles: false
- },
- paste: {
- promptForName: false,
- namePromptMessage: "Please name this image"
- },
- thumbnails: {
- customResizer: null,
- maxCount: 0,
- placeholders: {
- waitUntilResponse: false,
- notAvailablePath: null,
- waitingPath: null
- },
- timeBetweenThumbs: 750
- },
- scaling: {
- hideScaled: false
- },
- showMessage: function(message) {
- if (self._templating.hasDialog("alert")) {
- return self._templating.showDialog("alert", message);
- } else {
- setTimeout(function() {
- window.alert(message);
- }, 0);
+
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
+
+ params = options.paramsStore.getParams(id);
+ toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
+ setHeaders(id, xhr);
+
+ log('Sending upload request for ' + id);
+ xhr.send(toSend);
+ }
+
+
+ api = {
+ /**
+ * Adds File or Blob to the queue
+ * Returns id to use with upload, cancel
+ **/
+ add: function(fileOrBlobData){
+ var id;
+
+ if (fileOrBlobData instanceof File) {
+ id = fileState.push({file: fileOrBlobData}) - 1;
+ }
+ else if (fileOrBlobData.blob instanceof Blob) {
+ id = fileState.push({blobData: fileOrBlobData}) - 1;
+ }
+ else {
+ throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)');
+ }
+
+ fileState[id].uuid = qq.getUniqueId();
+ return id;
+ },
+ getName: function(id){
+ var file = fileState[id].file,
+ blobData = fileState[id].blobData;
+
+ if (file) {
+ // fix missing name in Safari 4
+ //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
+ return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
+ }
+ else {
+ return blobData.name;
+ }
+ },
+ getSize: function(id){
+ /*jshint eqnull: true*/
+ var fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
+
+ if (qq.isFileOrInput(fileOrBlob)) {
+ return fileOrBlob.fileSize != null ? fileOrBlob.fileSize : fileOrBlob.size;
+ }
+ else {
+ return fileOrBlob.size;
+ }
+ },
+ getFile: function(id) {
+ if (fileState[id]) {
+ return fileState[id].file || fileState[id].blobData.blob;
+ }
+ },
+ /**
+ * Returns uploaded bytes for file identified by id
+ */
+ getLoaded: function(id){
+ return fileState[id].loaded || 0;
+ },
+ isValid: function(id) {
+ return fileState[id] !== undefined;
+ },
+ reset: function() {
+ fileState = [];
+ },
+ getUuid: function(id) {
+ return fileState[id].uuid;
+ },
+ /**
+ * Sends the file identified by id to the server
+ */
+ upload: function(id, retry){
+ var name = this.getName(id);
+
+ options.onUpload(id, name);
+
+ if (chunkFiles) {
+ handleFileChunkingUpload(id, retry);
+ }
+ else {
+ handleStandardFileUpload(id);
+ }
+ },
+ cancel: function(id){
+ var xhr = fileState[id].xhr;
+
+ options.onCancel(id, this.getName(id));
+
+ if (xhr) {
+ xhr.onreadystatechange = null;
+ xhr.abort();
+ }
+
+ if (resumeEnabled) {
+ deletePersistedChunkData(id);
+ }
+
+ delete fileState[id];
+ },
+ getResumableFilesData: function() {
+ var matchingCookieNames = [],
+ resumableFilesData = [];
+
+ if (chunkFiles && resumeEnabled) {
+ if (resumeId === undefined) {
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "="));
}
- },
- showConfirm: function(message) {
- if (self._templating.hasDialog("confirm")) {
- return self._templating.showDialog("confirm", message);
- } else {
- return window.confirm(message);
+ else {
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" +
+ cookieItemDelimiter + resumeId + "="));
}
- },
- showPrompt: function(message, defaultValue) {
- if (self._templating.hasDialog("prompt")) {
- return self._templating.showDialog("prompt", message, defaultValue);
- } else {
- return window.prompt(message, defaultValue);
- }
- }
- }, true);
- qq.extend(this._options, o, true);
- this._templating = new qq.Templating({
- log: qq.bind(this.log, this),
- templateIdOrEl: this._options.template,
- containerEl: this._options.element,
- fileContainerEl: this._options.listElement,
- button: this._options.button,
- imageGenerator: this._imageGenerator,
- classes: {
- hide: this._options.classes.hide,
- editable: this._options.classes.editable
- },
- limits: {
- maxThumbs: this._options.thumbnails.maxCount,
- timeBetweenThumbs: this._options.thumbnails.timeBetweenThumbs
- },
- placeholders: {
- waitUntilUpdate: this._options.thumbnails.placeholders.waitUntilResponse,
- thumbnailNotAvailable: this._options.thumbnails.placeholders.notAvailablePath,
- waitingForThumbnail: this._options.thumbnails.placeholders.waitingPath
- },
- text: this._options.text
- });
- if (this._options.workarounds.ios8SafariUploads && qq.ios800() && qq.iosSafari()) {
- this._templating.renderFailure(this._options.messages.unsupportedBrowserIos8Safari);
- } else if (!qq.supportedFeatures.uploading || this._options.cors.expected && !qq.supportedFeatures.uploadCors) {
- this._templating.renderFailure(this._options.messages.unsupportedBrowser);
- } else {
- this._wrapCallbacks();
- this._templating.render();
- this._classes = this._options.classes;
- if (!this._options.button && this._templating.getButton()) {
- this._defaultButtonId = this._createUploadButton({
- element: this._templating.getButton(),
- title: this._options.text.fileInputTitle
- }).getButtonId();
- }
- this._setupClickAndEditEventHandlers();
- if (qq.DragAndDrop && qq.supportedFeatures.fileDrop) {
- this._dnd = this._setupDragAndDrop();
- }
- if (this._options.paste.targetElement && this._options.paste.promptForName) {
- if (qq.PasteSupport) {
- this._setupPastePrompt();
- } else {
- this.log("Paste support module not found.", "error");
- }
- }
- this._totalFilesInBatch = 0;
- this._filesInBatchAddedToUi = 0;
- }
- };
- qq.extend(qq.FineUploader.prototype, qq.basePublicApi);
- qq.extend(qq.FineUploader.prototype, qq.basePrivateApi);
- qq.extend(qq.FineUploader.prototype, qq.uiPublicApi);
- qq.extend(qq.FineUploader.prototype, qq.uiPrivateApi);
- qq.Templating = function(spec) {
- "use strict";
- var FILE_ID_ATTR = "qq-file-id", FILE_CLASS_PREFIX = "qq-file-id-", THUMBNAIL_MAX_SIZE_ATTR = "qq-max-size", THUMBNAIL_SERVER_SCALE_ATTR = "qq-server-scale", HIDE_DROPZONE_ATTR = "qq-hide-dropzone", DROPZPONE_TEXT_ATTR = "qq-drop-area-text", IN_PROGRESS_CLASS = "qq-in-progress", HIDDEN_FOREVER_CLASS = "qq-hidden-forever", fileBatch = {
- content: document.createDocumentFragment(),
- map: {}
- }, isCancelDisabled = false, generatedThumbnails = 0, thumbnailQueueMonitorRunning = false, thumbGenerationQueue = [], thumbnailMaxSize = -1, options = {
- log: null,
- limits: {
- maxThumbs: 0,
- timeBetweenThumbs: 750
- },
- templateIdOrEl: "qq-template",
- containerEl: null,
- fileContainerEl: null,
- button: null,
- imageGenerator: null,
- classes: {
- hide: "qq-hide",
- editable: "qq-editable"
- },
- placeholders: {
- waitUntilUpdate: false,
- thumbnailNotAvailable: null,
- waitingForThumbnail: null
- },
- text: {
- paused: "Paused"
- }
- }, selectorClasses = {
- button: "qq-upload-button-selector",
- alertDialog: "qq-alert-dialog-selector",
- dialogCancelButton: "qq-cancel-button-selector",
- confirmDialog: "qq-confirm-dialog-selector",
- dialogMessage: "qq-dialog-message-selector",
- dialogOkButton: "qq-ok-button-selector",
- promptDialog: "qq-prompt-dialog-selector",
- uploader: "qq-uploader-selector",
- drop: "qq-upload-drop-area-selector",
- list: "qq-upload-list-selector",
- progressBarContainer: "qq-progress-bar-container-selector",
- progressBar: "qq-progress-bar-selector",
- totalProgressBarContainer: "qq-total-progress-bar-container-selector",
- totalProgressBar: "qq-total-progress-bar-selector",
- file: "qq-upload-file-selector",
- spinner: "qq-upload-spinner-selector",
- size: "qq-upload-size-selector",
- cancel: "qq-upload-cancel-selector",
- pause: "qq-upload-pause-selector",
- continueButton: "qq-upload-continue-selector",
- deleteButton: "qq-upload-delete-selector",
- retry: "qq-upload-retry-selector",
- statusText: "qq-upload-status-text-selector",
- editFilenameInput: "qq-edit-filename-selector",
- editNameIcon: "qq-edit-filename-icon-selector",
- dropText: "qq-upload-drop-area-text-selector",
- dropProcessing: "qq-drop-processing-selector",
- dropProcessingSpinner: "qq-drop-processing-spinner-selector",
- thumbnail: "qq-thumbnail-selector"
- }, previewGeneration = {}, cachedThumbnailNotAvailableImg = new qq.Promise(), cachedWaitingForThumbnailImg = new qq.Promise(), log, isEditElementsExist, isRetryElementExist, templateDom, container, fileList, showThumbnails, serverScale, cacheThumbnailPlaceholders = function() {
- var notAvailableUrl = options.placeholders.thumbnailNotAvailable, waitingUrl = options.placeholders.waitingForThumbnail, spec = {
- maxSize: thumbnailMaxSize,
- scale: serverScale
- };
- if (showThumbnails) {
- if (notAvailableUrl) {
- options.imageGenerator.generate(notAvailableUrl, new Image(), spec).then(function(updatedImg) {
- cachedThumbnailNotAvailableImg.success(updatedImg);
- }, function() {
- cachedThumbnailNotAvailableImg.failure();
- log("Problem loading 'not available' placeholder image at " + notAvailableUrl, "error");
+
+ qq.each(matchingCookieNames, function(idx, cookieName) {
+ var cookiesNameParts = cookieName.split(cookieItemDelimiter);
+ var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
+
+ resumableFilesData.push({
+ name: decodeURIComponent(cookiesNameParts[1]),
+ size: cookiesNameParts[2],
+ uuid: cookieValueParts[0],
+ partIdx: cookieValueParts[1]
});
- } else {
- cachedThumbnailNotAvailableImg.failure();
- }
- if (waitingUrl) {
- options.imageGenerator.generate(waitingUrl, new Image(), spec).then(function(updatedImg) {
- cachedWaitingForThumbnailImg.success(updatedImg);
- }, function() {
- cachedWaitingForThumbnailImg.failure();
- log("Problem loading 'waiting for thumbnail' placeholder image at " + waitingUrl, "error");
- });
- } else {
- cachedWaitingForThumbnailImg.failure();
- }
- }
- }, displayWaitingImg = function(thumbnail) {
- var waitingImgPlacement = new qq.Promise();
- cachedWaitingForThumbnailImg.then(function(img) {
- maybeScalePlaceholderViaCss(img, thumbnail);
- if (!thumbnail.src) {
- thumbnail.src = img.src;
- thumbnail.onload = function() {
- thumbnail.onload = null;
- show(thumbnail);
- waitingImgPlacement.success();
- };
- } else {
- waitingImgPlacement.success();
- }
- }, function() {
- hide(thumbnail);
- waitingImgPlacement.success();
- });
- return waitingImgPlacement;
- }, generateNewPreview = function(id, blob, spec) {
- var thumbnail = getThumbnail(id);
- log("Generating new thumbnail for " + id);
- blob.qqThumbnailId = id;
- return options.imageGenerator.generate(blob, thumbnail, spec).then(function() {
- generatedThumbnails++;
- show(thumbnail);
- previewGeneration[id].success();
- }, function() {
- previewGeneration[id].failure();
- if (!options.placeholders.waitUntilUpdate) {
- maybeSetDisplayNotAvailableImg(id, thumbnail);
- }
- });
- }, generateNextQueuedPreview = function() {
- if (thumbGenerationQueue.length) {
- thumbnailQueueMonitorRunning = true;
- var queuedThumbRequest = thumbGenerationQueue.shift();
- if (queuedThumbRequest.update) {
- processUpdateQueuedPreviewRequest(queuedThumbRequest);
- } else {
- processNewQueuedPreviewRequest(queuedThumbRequest);
- }
- } else {
- thumbnailQueueMonitorRunning = false;
- }
- }, getCancel = function(id) {
- return getTemplateEl(getFile(id), selectorClasses.cancel);
- }, getContinue = function(id) {
- return getTemplateEl(getFile(id), selectorClasses.continueButton);
- }, getDialog = function(type) {
- return getTemplateEl(container, selectorClasses[type + "Dialog"]);
- }, getDelete = function(id) {
- return getTemplateEl(getFile(id), selectorClasses.deleteButton);
- }, getDropProcessing = function() {
- return getTemplateEl(container, selectorClasses.dropProcessing);
- }, getEditIcon = function(id) {
- return getTemplateEl(getFile(id), selectorClasses.editNameIcon);
- }, getFile = function(id) {
- return fileBatch.map[id] || qq(fileList).getFirstByClass(FILE_CLASS_PREFIX + id);
- }, getFilename = function(id) {
- return getTemplateEl(getFile(id), selectorClasses.file);
- }, getPause = function(id) {
- return getTemplateEl(getFile(id), selectorClasses.pause);
- }, getProgress = function(id) {
- if (id == null) {
- return getTemplateEl(container, selectorClasses.totalProgressBarContainer) || getTemplateEl(container, selectorClasses.totalProgressBar);
- }
- return getTemplateEl(getFile(id), selectorClasses.progressBarContainer) || getTemplateEl(getFile(id), selectorClasses.progressBar);
- }, getRetry = function(id) {
- return getTemplateEl(getFile(id), selectorClasses.retry);
- }, getSize = function(id) {
- return getTemplateEl(getFile(id), selectorClasses.size);
- }, getSpinner = function(id) {
- return getTemplateEl(getFile(id), selectorClasses.spinner);
- }, getTemplateEl = function(context, cssClass) {
- return context && qq(context).getFirstByClass(cssClass);
- }, getThumbnail = function(id) {
- return showThumbnails && getTemplateEl(getFile(id), selectorClasses.thumbnail);
- }, hide = function(el) {
- el && qq(el).addClass(options.classes.hide);
- }, maybeScalePlaceholderViaCss = function(placeholder, thumbnail) {
- var maxWidth = placeholder.style.maxWidth, maxHeight = placeholder.style.maxHeight;
- if (maxHeight && maxWidth && !thumbnail.style.maxWidth && !thumbnail.style.maxHeight) {
- qq(thumbnail).css({
- maxWidth: maxWidth,
- maxHeight: maxHeight
});
+
+ return resumableFilesData;
}
- }, maybeSetDisplayNotAvailableImg = function(id, thumbnail) {
- var previewing = previewGeneration[id] || new qq.Promise().failure(), notAvailableImgPlacement = new qq.Promise();
- cachedThumbnailNotAvailableImg.then(function(img) {
- previewing.then(function() {
- notAvailableImgPlacement.success();
- }, function() {
- maybeScalePlaceholderViaCss(img, thumbnail);
- thumbnail.onload = function() {
- thumbnail.onload = null;
- notAvailableImgPlacement.success();
- };
- thumbnail.src = img.src;
- show(thumbnail);
- });
- });
- return notAvailableImgPlacement;
- }, parseAndGetTemplate = function() {
- var scriptEl, scriptHtml, fileListNode, tempTemplateEl, fileListEl, defaultButton, dropArea, thumbnail, dropProcessing, dropTextEl, uploaderEl;
- log("Parsing template");
- if (options.templateIdOrEl == null) {
- throw new Error("You MUST specify either a template element or ID!");
- }
- if (qq.isString(options.templateIdOrEl)) {
- scriptEl = document.getElementById(options.templateIdOrEl);
- if (scriptEl === null) {
- throw new Error(qq.format("Cannot find template script at ID '{}'!", options.templateIdOrEl));
- }
- scriptHtml = scriptEl.innerHTML;
- } else {
- if (options.templateIdOrEl.innerHTML === undefined) {
- throw new Error("You have specified an invalid value for the template option! " + "It must be an ID or an Element.");
- }
- scriptHtml = options.templateIdOrEl.innerHTML;
- }
- scriptHtml = qq.trimStr(scriptHtml);
- tempTemplateEl = document.createElement("div");
- tempTemplateEl.appendChild(qq.toElement(scriptHtml));
- uploaderEl = qq(tempTemplateEl).getFirstByClass(selectorClasses.uploader);
- if (options.button) {
- defaultButton = qq(tempTemplateEl).getFirstByClass(selectorClasses.button);
- if (defaultButton) {
- qq(defaultButton).remove();
- }
- }
- if (!qq.DragAndDrop || !qq.supportedFeatures.fileDrop) {
- dropProcessing = qq(tempTemplateEl).getFirstByClass(selectorClasses.dropProcessing);
- if (dropProcessing) {
- qq(dropProcessing).remove();
- }
- }
- dropArea = qq(tempTemplateEl).getFirstByClass(selectorClasses.drop);
- if (dropArea && !qq.DragAndDrop) {
- log("DnD module unavailable.", "info");
- qq(dropArea).remove();
- }
- if (!qq.supportedFeatures.fileDrop) {
- uploaderEl.removeAttribute(DROPZPONE_TEXT_ATTR);
- if (dropArea && qq(dropArea).hasAttribute(HIDE_DROPZONE_ATTR)) {
- qq(dropArea).css({
- display: "none"
- });
- }
- } else if (qq(uploaderEl).hasAttribute(DROPZPONE_TEXT_ATTR) && dropArea) {
- dropTextEl = qq(dropArea).getFirstByClass(selectorClasses.dropText);
- dropTextEl && qq(dropTextEl).remove();
- }
- thumbnail = qq(tempTemplateEl).getFirstByClass(selectorClasses.thumbnail);
- if (!showThumbnails) {
- thumbnail && qq(thumbnail).remove();
- } else if (thumbnail) {
- thumbnailMaxSize = parseInt(thumbnail.getAttribute(THUMBNAIL_MAX_SIZE_ATTR));
- thumbnailMaxSize = thumbnailMaxSize > 0 ? thumbnailMaxSize : null;
- serverScale = qq(thumbnail).hasAttribute(THUMBNAIL_SERVER_SCALE_ATTR);
- }
- showThumbnails = showThumbnails && thumbnail;
- isEditElementsExist = qq(tempTemplateEl).getByClass(selectorClasses.editFilenameInput).length > 0;
- isRetryElementExist = qq(tempTemplateEl).getByClass(selectorClasses.retry).length > 0;
- fileListNode = qq(tempTemplateEl).getFirstByClass(selectorClasses.list);
- if (fileListNode == null) {
- throw new Error("Could not find the file list container in the template!");
- }
- fileListEl = fileListNode.children[0].cloneNode(true);
- fileListNode.innerHTML = "";
- if (tempTemplateEl.getElementsByTagName("DIALOG").length) {
- document.createElement("dialog");
- }
- log("Template parsing complete");
- return {
- template: tempTemplateEl,
- fileTemplate: fileListEl
- };
- }, prependFile = function(el, index, fileList) {
- var parentEl = fileList, beforeEl = parentEl.firstChild;
- if (index > 0) {
- beforeEl = qq(parentEl).children()[index].nextSibling;
- }
- parentEl.insertBefore(el, beforeEl);
- }, processNewQueuedPreviewRequest = function(queuedThumbRequest) {
- var id = queuedThumbRequest.id, optFileOrBlob = queuedThumbRequest.optFileOrBlob, relatedThumbnailId = optFileOrBlob && optFileOrBlob.qqThumbnailId, thumbnail = getThumbnail(id), spec = {
- customResizeFunction: queuedThumbRequest.customResizeFunction,
- maxSize: thumbnailMaxSize,
- orient: true,
- scale: true
- };
- if (qq.supportedFeatures.imagePreviews) {
- if (thumbnail) {
- if (options.limits.maxThumbs && options.limits.maxThumbs <= generatedThumbnails) {
- maybeSetDisplayNotAvailableImg(id, thumbnail);
- generateNextQueuedPreview();
- } else {
- displayWaitingImg(thumbnail).done(function() {
- previewGeneration[id] = new qq.Promise();
- previewGeneration[id].done(function() {
- setTimeout(generateNextQueuedPreview, options.limits.timeBetweenThumbs);
- });
- if (relatedThumbnailId != null) {
- useCachedPreview(id, relatedThumbnailId);
- } else {
- generateNewPreview(id, optFileOrBlob, spec);
- }
- });
- }
- } else {
- generateNextQueuedPreview();
- }
- } else if (thumbnail) {
- displayWaitingImg(thumbnail);
- generateNextQueuedPreview();
- }
- }, processUpdateQueuedPreviewRequest = function(queuedThumbRequest) {
- var id = queuedThumbRequest.id, thumbnailUrl = queuedThumbRequest.thumbnailUrl, showWaitingImg = queuedThumbRequest.showWaitingImg, thumbnail = getThumbnail(id), spec = {
- customResizeFunction: queuedThumbRequest.customResizeFunction,
- scale: serverScale,
- maxSize: thumbnailMaxSize
- };
- if (thumbnail) {
- if (thumbnailUrl) {
- if (options.limits.maxThumbs && options.limits.maxThumbs <= generatedThumbnails) {
- maybeSetDisplayNotAvailableImg(id, thumbnail);
- generateNextQueuedPreview();
- } else {
- if (showWaitingImg) {
- displayWaitingImg(thumbnail);
- }
- return options.imageGenerator.generate(thumbnailUrl, thumbnail, spec).then(function() {
- show(thumbnail);
- generatedThumbnails++;
- setTimeout(generateNextQueuedPreview, options.limits.timeBetweenThumbs);
- }, function() {
- maybeSetDisplayNotAvailableImg(id, thumbnail);
- setTimeout(generateNextQueuedPreview, options.limits.timeBetweenThumbs);
- });
- }
- } else {
- maybeSetDisplayNotAvailableImg(id, thumbnail);
- generateNextQueuedPreview();
- }
- }
- }, setProgressBarWidth = function(id, percent) {
- var bar = getProgress(id), progressBarSelector = id == null ? selectorClasses.totalProgressBar : selectorClasses.progressBar;
- if (bar && !qq(bar).hasClass(progressBarSelector)) {
- bar = qq(bar).getFirstByClass(progressBarSelector);
- }
- if (bar) {
- qq(bar).css({
- width: percent + "%"
- });
- bar.setAttribute("aria-valuenow", percent);
- }
- }, show = function(el) {
- el && qq(el).removeClass(options.classes.hide);
- }, useCachedPreview = function(targetThumbnailId, cachedThumbnailId) {
- var targetThumbnail = getThumbnail(targetThumbnailId), cachedThumbnail = getThumbnail(cachedThumbnailId);
- log(qq.format("ID {} is the same file as ID {}. Will use generated thumbnail from ID {} instead.", targetThumbnailId, cachedThumbnailId, cachedThumbnailId));
- previewGeneration[cachedThumbnailId].then(function() {
- generatedThumbnails++;
- previewGeneration[targetThumbnailId].success();
- log(qq.format("Now using previously generated thumbnail created for ID {} on ID {}.", cachedThumbnailId, targetThumbnailId));
- targetThumbnail.src = cachedThumbnail.src;
- show(targetThumbnail);
- }, function() {
- previewGeneration[targetThumbnailId].failure();
- if (!options.placeholders.waitUntilUpdate) {
- maybeSetDisplayNotAvailableImg(targetThumbnailId, targetThumbnail);
- }
- });
- };
- qq.extend(options, spec);
- log = options.log;
- if (!qq.supportedFeatures.imagePreviews) {
- options.limits.timeBetweenThumbs = 0;
- options.limits.maxThumbs = 0;
- }
- container = options.containerEl;
- showThumbnails = options.imageGenerator !== undefined;
- templateDom = parseAndGetTemplate();
- cacheThumbnailPlaceholders();
- qq.extend(this, {
- render: function() {
- log("Rendering template in DOM.");
- generatedThumbnails = 0;
- container.appendChild(templateDom.template.cloneNode(true));
- hide(getDropProcessing());
- this.hideTotalProgress();
- fileList = options.fileContainerEl || getTemplateEl(container, selectorClasses.list);
- log("Template rendering complete");
- },
- renderFailure: function(message) {
- var cantRenderEl = qq.toElement(message);
- container.innerHTML = "";
- container.appendChild(cantRenderEl);
- },
- reset: function() {
- this.render();
- },
- clearFiles: function() {
- fileList.innerHTML = "";
- },
- disableCancel: function() {
- isCancelDisabled = true;
- },
- addFile: function(id, name, prependInfo, hideForever, batch) {
- var fileEl = templateDom.fileTemplate.cloneNode(true), fileNameEl = getTemplateEl(fileEl, selectorClasses.file), uploaderEl = getTemplateEl(container, selectorClasses.uploader), fileContainer = batch ? fileBatch.content : fileList, thumb;
- if (batch) {
- fileBatch.map[id] = fileEl;
- }
- qq(fileEl).addClass(FILE_CLASS_PREFIX + id);
- uploaderEl.removeAttribute(DROPZPONE_TEXT_ATTR);
- if (fileNameEl) {
- qq(fileNameEl).setText(name);
- fileNameEl.setAttribute("title", name);
- }
- fileEl.setAttribute(FILE_ID_ATTR, id);
- if (prependInfo) {
- prependFile(fileEl, prependInfo.index, fileContainer);
- } else {
- fileContainer.appendChild(fileEl);
- }
- if (hideForever) {
- fileEl.style.display = "none";
- qq(fileEl).addClass(HIDDEN_FOREVER_CLASS);
- } else {
- hide(getProgress(id));
- hide(getSize(id));
- hide(getDelete(id));
- hide(getRetry(id));
- hide(getPause(id));
- hide(getContinue(id));
- if (isCancelDisabled) {
- this.hideCancel(id);
- }
- thumb = getThumbnail(id);
- if (thumb && !thumb.src) {
- cachedWaitingForThumbnailImg.then(function(waitingImg) {
- thumb.src = waitingImg.src;
- if (waitingImg.style.maxHeight && waitingImg.style.maxWidth) {
- qq(thumb).css({
- maxHeight: waitingImg.style.maxHeight,
- maxWidth: waitingImg.style.maxWidth
- });
- }
- show(thumb);
- });
- }
- }
- },
- addFileToCache: function(id, name, prependInfo, hideForever) {
- this.addFile(id, name, prependInfo, hideForever, true);
- },
- addCacheToDom: function() {
- fileList.appendChild(fileBatch.content);
- fileBatch.content = document.createDocumentFragment();
- fileBatch.map = {};
- },
- removeFile: function(id) {
- qq(getFile(id)).remove();
- },
- getFileId: function(el) {
- var currentNode = el;
- if (currentNode) {
- while (currentNode.getAttribute(FILE_ID_ATTR) == null) {
- currentNode = currentNode.parentNode;
- }
- return parseInt(currentNode.getAttribute(FILE_ID_ATTR));
- }
- },
- getFileList: function() {
- return fileList;
- },
- markFilenameEditable: function(id) {
- var filename = getFilename(id);
- filename && qq(filename).addClass(options.classes.editable);
- },
- updateFilename: function(id, name) {
- var filenameEl = getFilename(id);
- if (filenameEl) {
- qq(filenameEl).setText(name);
- filenameEl.setAttribute("title", name);
- }
- },
- hideFilename: function(id) {
- hide(getFilename(id));
- },
- showFilename: function(id) {
- show(getFilename(id));
- },
- isFileName: function(el) {
- return qq(el).hasClass(selectorClasses.file);
- },
- getButton: function() {
- return options.button || getTemplateEl(container, selectorClasses.button);
- },
- hideDropProcessing: function() {
- hide(getDropProcessing());
- },
- showDropProcessing: function() {
- show(getDropProcessing());
- },
- getDropZone: function() {
- return getTemplateEl(container, selectorClasses.drop);
- },
- isEditFilenamePossible: function() {
- return isEditElementsExist;
- },
- hideRetry: function(id) {
- hide(getRetry(id));
- },
- isRetryPossible: function() {
- return isRetryElementExist;
- },
- showRetry: function(id) {
- show(getRetry(id));
- },
- getFileContainer: function(id) {
- return getFile(id);
- },
- showEditIcon: function(id) {
- var icon = getEditIcon(id);
- icon && qq(icon).addClass(options.classes.editable);
- },
- isHiddenForever: function(id) {
- return qq(getFile(id)).hasClass(HIDDEN_FOREVER_CLASS);
- },
- hideEditIcon: function(id) {
- var icon = getEditIcon(id);
- icon && qq(icon).removeClass(options.classes.editable);
- },
- isEditIcon: function(el) {
- return qq(el).hasClass(selectorClasses.editNameIcon, true);
- },
- getEditInput: function(id) {
- return getTemplateEl(getFile(id), selectorClasses.editFilenameInput);
- },
- isEditInput: function(el) {
- return qq(el).hasClass(selectorClasses.editFilenameInput, true);
- },
- updateProgress: function(id, loaded, total) {
- var bar = getProgress(id), percent;
- if (bar && total > 0) {
- percent = Math.round(loaded / total * 100);
- if (percent === 100) {
- hide(bar);
- } else {
- show(bar);
- }
- setProgressBarWidth(id, percent);
- }
- },
- updateTotalProgress: function(loaded, total) {
- this.updateProgress(null, loaded, total);
- },
- hideProgress: function(id) {
- var bar = getProgress(id);
- bar && hide(bar);
- },
- hideTotalProgress: function() {
- this.hideProgress();
- },
- resetProgress: function(id) {
- setProgressBarWidth(id, 0);
- this.hideTotalProgress(id);
- },
- resetTotalProgress: function() {
- this.resetProgress();
- },
- showCancel: function(id) {
- if (!isCancelDisabled) {
- var cancel = getCancel(id);
- cancel && qq(cancel).removeClass(options.classes.hide);
- }
- },
- hideCancel: function(id) {
- hide(getCancel(id));
- },
- isCancel: function(el) {
- return qq(el).hasClass(selectorClasses.cancel, true);
- },
- allowPause: function(id) {
- show(getPause(id));
- hide(getContinue(id));
- },
- uploadPaused: function(id) {
- this.setStatusText(id, options.text.paused);
- this.allowContinueButton(id);
- hide(getSpinner(id));
- },
- hidePause: function(id) {
- hide(getPause(id));
- },
- isPause: function(el) {
- return qq(el).hasClass(selectorClasses.pause, true);
- },
- isContinueButton: function(el) {
- return qq(el).hasClass(selectorClasses.continueButton, true);
- },
- allowContinueButton: function(id) {
- show(getContinue(id));
- hide(getPause(id));
- },
- uploadContinued: function(id) {
- this.setStatusText(id, "");
- this.allowPause(id);
- show(getSpinner(id));
- },
- showDeleteButton: function(id) {
- show(getDelete(id));
- },
- hideDeleteButton: function(id) {
- hide(getDelete(id));
- },
- isDeleteButton: function(el) {
- return qq(el).hasClass(selectorClasses.deleteButton, true);
- },
- isRetry: function(el) {
- return qq(el).hasClass(selectorClasses.retry, true);
- },
- updateSize: function(id, text) {
- var size = getSize(id);
- if (size) {
- show(size);
- qq(size).setText(text);
- }
- },
- setStatusText: function(id, text) {
- var textEl = getTemplateEl(getFile(id), selectorClasses.statusText);
- if (textEl) {
- if (text == null) {
- qq(textEl).clearText();
- } else {
- qq(textEl).setText(text);
- }
- }
- },
- hideSpinner: function(id) {
- qq(getFile(id)).removeClass(IN_PROGRESS_CLASS);
- hide(getSpinner(id));
- },
- showSpinner: function(id) {
- qq(getFile(id)).addClass(IN_PROGRESS_CLASS);
- show(getSpinner(id));
- },
- generatePreview: function(id, optFileOrBlob, customResizeFunction) {
- if (!this.isHiddenForever(id)) {
- thumbGenerationQueue.push({
- id: id,
- customResizeFunction: customResizeFunction,
- optFileOrBlob: optFileOrBlob
- });
- !thumbnailQueueMonitorRunning && generateNextQueuedPreview();
- }
- },
- updateThumbnail: function(id, thumbnailUrl, showWaitingImg, customResizeFunction) {
- if (!this.isHiddenForever(id)) {
- thumbGenerationQueue.push({
- customResizeFunction: customResizeFunction,
- update: true,
- id: id,
- thumbnailUrl: thumbnailUrl,
- showWaitingImg: showWaitingImg
- });
- !thumbnailQueueMonitorRunning && generateNextQueuedPreview();
- }
- },
- hasDialog: function(type) {
- return qq.supportedFeatures.dialogElement && !!getDialog(type);
- },
- showDialog: function(type, message, defaultValue) {
- var dialog = getDialog(type), messageEl = getTemplateEl(dialog, selectorClasses.dialogMessage), inputEl = dialog.getElementsByTagName("INPUT")[0], cancelBtn = getTemplateEl(dialog, selectorClasses.dialogCancelButton), okBtn = getTemplateEl(dialog, selectorClasses.dialogOkButton), promise = new qq.Promise(), closeHandler = function() {
- cancelBtn.removeEventListener("click", cancelClickHandler);
- okBtn && okBtn.removeEventListener("click", okClickHandler);
- promise.failure();
- }, cancelClickHandler = function() {
- cancelBtn.removeEventListener("click", cancelClickHandler);
- dialog.close();
- }, okClickHandler = function() {
- dialog.removeEventListener("close", closeHandler);
- okBtn.removeEventListener("click", okClickHandler);
- dialog.close();
- promise.success(inputEl && inputEl.value);
- };
- dialog.addEventListener("close", closeHandler);
- cancelBtn.addEventListener("click", cancelClickHandler);
- okBtn && okBtn.addEventListener("click", okClickHandler);
- if (inputEl) {
- inputEl.value = defaultValue;
- }
- messageEl.textContent = message;
- dialog.showModal();
- return promise;
- }
- });
- };
- qq.UiEventHandler = function(s, protectedApi) {
- "use strict";
- var disposer = new qq.DisposeSupport(), spec = {
- eventType: "click",
- attachTo: null,
- onHandled: function(target, event) {}
- };
- qq.extend(this, {
- addHandler: function(element) {
- addHandler(element);
- },
- dispose: function() {
- disposer.dispose();
- }
- });
- function addHandler(element) {
- disposer.attach(element, spec.eventType, function(event) {
- event = event || window.event;
- var target = event.target || event.srcElement;
- spec.onHandled(target, event);
- });
- }
- qq.extend(protectedApi, {
- getFileIdFromItem: function(item) {
- return item.qqFileId;
- },
- getDisposeSupport: function() {
- return disposer;
- }
- });
- qq.extend(spec, s);
- if (spec.attachTo) {
- addHandler(spec.attachTo);
+ return [];
}
};
- qq.FileButtonsClickHandler = function(s) {
- "use strict";
- var inheritedInternalApi = {}, spec = {
- templating: null,
- log: function(message, lvl) {},
- onDeleteFile: function(fileId) {},
- onCancel: function(fileId) {},
- onRetry: function(fileId) {},
- onPause: function(fileId) {},
- onContinue: function(fileId) {},
- onGetName: function(fileId) {}
- }, buttonHandlers = {
- cancel: function(id) {
- spec.onCancel(id);
- },
- retry: function(id) {
- spec.onRetry(id);
- },
- deleteButton: function(id) {
- spec.onDeleteFile(id);
- },
- pause: function(id) {
- spec.onPause(id);
- },
- continueButton: function(id) {
- spec.onContinue(id);
- }
- };
- function examineEvent(target, event) {
- qq.each(buttonHandlers, function(buttonType, handler) {
- var firstLetterCapButtonType = buttonType.charAt(0).toUpperCase() + buttonType.slice(1), fileId;
- if (spec.templating["is" + firstLetterCapButtonType](target)) {
- fileId = spec.templating.getFileId(target);
- qq.preventDefault(event);
- spec.log(qq.format("Detected valid file button click event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
- handler(fileId);
- return false;
- }
- });
- }
- qq.extend(spec, s);
- spec.eventType = "click";
- spec.onHandled = examineEvent;
- spec.attachTo = spec.templating.getFileList();
- qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
- };
- qq.FilenameClickHandler = function(s) {
- "use strict";
- var inheritedInternalApi = {}, spec = {
- templating: null,
- log: function(message, lvl) {},
- classes: {
- file: "qq-upload-file",
- editNameIcon: "qq-edit-filename-icon"
- },
- onGetUploadStatus: function(fileId) {},
- onGetName: function(fileId) {}
- };
- qq.extend(spec, s);
- function examineEvent(target, event) {
- if (spec.templating.isFileName(target) || spec.templating.isEditIcon(target)) {
- var fileId = spec.templating.getFileId(target), status = spec.onGetUploadStatus(fileId);
- if (status === qq.status.SUBMITTED) {
- spec.log(qq.format("Detected valid filename click event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
- qq.preventDefault(event);
- inheritedInternalApi.handleFilenameEdit(fileId, target, true);
- }
- }
- }
- spec.eventType = "click";
- spec.onHandled = examineEvent;
- qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
- };
- qq.FilenameInputFocusInHandler = function(s, inheritedInternalApi) {
- "use strict";
- var spec = {
- templating: null,
- onGetUploadStatus: function(fileId) {},
- log: function(message, lvl) {}
- };
- if (!inheritedInternalApi) {
- inheritedInternalApi = {};
- }
- function handleInputFocus(target, event) {
- if (spec.templating.isEditInput(target)) {
- var fileId = spec.templating.getFileId(target), status = spec.onGetUploadStatus(fileId);
- if (status === qq.status.SUBMITTED) {
- spec.log(qq.format("Detected valid filename input focus event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
- inheritedInternalApi.handleFilenameEdit(fileId, target);
- }
- }
- }
- spec.eventType = "focusin";
- spec.onHandled = handleInputFocus;
- qq.extend(spec, s);
- qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
- };
- qq.FilenameInputFocusHandler = function(spec) {
- "use strict";
- spec.eventType = "focus";
- spec.attachTo = null;
- qq.extend(this, new qq.FilenameInputFocusInHandler(spec, {}));
- };
- qq.FilenameEditHandler = function(s, inheritedInternalApi) {
- "use strict";
- var spec = {
- templating: null,
- log: function(message, lvl) {},
- onGetUploadStatus: function(fileId) {},
- onGetName: function(fileId) {},
- onSetName: function(fileId, newName) {},
- onEditingStatusChange: function(fileId, isEditing) {}
- };
- function getFilenameSansExtension(fileId) {
- var filenameSansExt = spec.onGetName(fileId), extIdx = filenameSansExt.lastIndexOf(".");
- if (extIdx > 0) {
- filenameSansExt = filenameSansExt.substr(0, extIdx);
- }
- return filenameSansExt;
- }
- function getOriginalExtension(fileId) {
- var origName = spec.onGetName(fileId);
- return qq.getExtension(origName);
- }
- function handleNameUpdate(newFilenameInputEl, fileId) {
- var newName = newFilenameInputEl.value, origExtension;
- if (newName !== undefined && qq.trimStr(newName).length > 0) {
- origExtension = getOriginalExtension(fileId);
- if (origExtension !== undefined) {
- newName = newName + "." + origExtension;
- }
- spec.onSetName(fileId, newName);
- }
- spec.onEditingStatusChange(fileId, false);
- }
- function registerInputBlurHandler(inputEl, fileId) {
- inheritedInternalApi.getDisposeSupport().attach(inputEl, "blur", function() {
- handleNameUpdate(inputEl, fileId);
- });
- }
- function registerInputEnterKeyHandler(inputEl, fileId) {
- inheritedInternalApi.getDisposeSupport().attach(inputEl, "keyup", function(event) {
- var code = event.keyCode || event.which;
- if (code === 13) {
- handleNameUpdate(inputEl, fileId);
- }
- });
- }
- qq.extend(spec, s);
- spec.attachTo = spec.templating.getFileList();
- qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
- qq.extend(inheritedInternalApi, {
- handleFilenameEdit: function(id, target, focusInput) {
- var newFilenameInputEl = spec.templating.getEditInput(id);
- spec.onEditingStatusChange(id, true);
- newFilenameInputEl.value = getFilenameSansExtension(id);
- if (focusInput) {
- newFilenameInputEl.focus();
- }
- registerInputBlurHandler(newFilenameInputEl, id);
- registerInputEnterKeyHandler(newFilenameInputEl, id);
- }
- });
- };
-})(window);
-//# sourceMappingURL=fine-uploader.js.map
+
+ return api;
+};
diff --git a/services/web/public/stylesheets/app/base.less b/services/web/public/stylesheets/app/base.less
index a78e1a7cfa..7f2d9a55ed 100644
--- a/services/web/public/stylesheets/app/base.less
+++ b/services/web/public/stylesheets/app/base.less
@@ -2,7 +2,7 @@
padding: (@line-height-computed / 4) (@line-height-computed / 2);
background-color: @state-warning-bg;
color: #333;
- border-bottom: 1px solid @toolbar-border-color;
+ border-bottom: 1px solid @common-border-color;
}
.clickable {
diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less
index 75b7121094..e37d829710 100644
--- a/services/web/public/stylesheets/app/editor.less
+++ b/services/web/public/stylesheets/app/editor.less
@@ -26,6 +26,20 @@
}
}
+.editor-menu-icon when (@is-overleaf = true) {
+ &.fa {
+ width: 1em;
+ background: url(/img/ol-brand/overleaf-o-white.svg) center / contain no-repeat;
+
+ &::before {
+ // Disable the font-awesome icon when in Overleaf by replacing it with a
+ // non-breakable space instead (otherwise the browser would render a
+ // zero-width element).
+ content: "\00a0";
+ }
+ }
+}
+
.full-size {
position: absolute;
top: 0;
@@ -250,8 +264,8 @@
.ui-layout-resizer {
width: 6px;
background-color: #f4f4f4;
- border-left: 1px solid @toolbar-border-color;
- border-right: 1px solid @toolbar-border-color;
+ border-left: 1px solid @editor-border-color;
+ border-right: 1px solid @editor-border-color;
.ui-layout-toggler {
color: #999;
font-family: FontAwesome;
diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less
index d0ad76fe9c..c782384cb7 100644
--- a/services/web/public/stylesheets/app/editor/chat.less
+++ b/services/web/public/stylesheets/app/editor/chat.less
@@ -126,12 +126,12 @@
height: @new-message-height;
background-color: @gray-lightest;
padding: @line-height-computed / 4;
- border-top: 1px solid @toolbar-border-color;
+ border-top: 1px solid @editor-border-color;
textarea {
overflow: auto;
resize: none;
border-radius: @border-radius-base;
- border: 1px solid @toolbar-border-color;
+ border: 1px solid @editor-border-color;
height: 100%;
width: 100%;
color: @gray-dark;
diff --git a/services/web/public/stylesheets/app/editor/file-tree.less b/services/web/public/stylesheets/app/editor/file-tree.less
index 7847822bf9..162b90c52f 100644
--- a/services/web/public/stylesheets/app/editor/file-tree.less
+++ b/services/web/public/stylesheets/app/editor/file-tree.less
@@ -1,5 +1,20 @@
+.fake-full-width-bg(@bg-color) {
+ &::before {
+ content: '\00a0';
+ position: absolute;
+ width: 100%;
+ right: 100%;
+ background-color: @bg-color;
+ }
+}
-aside#file-tree {
+#file-tree {
+ .toolbar.toolbar-filetree {
+ .toolbar-small-mixin;
+ .toolbar-alt-mixin;
+ padding: 0 5px;
+ }
+
.file-tree-inner {
position: absolute;
top: 32px;
@@ -7,12 +22,14 @@ aside#file-tree {
left: 0;
right: 0;
overflow-y: auto;
+ background-color: @file-tree-bg;
&.no-toolbar {
top: 0;
}
}
+ // TODO; Consolidate with "Project files" in Overleaf
h3 {
font-size: 1rem;
border-bottom: 1px solid @gray;
@@ -20,10 +37,13 @@ aside#file-tree {
margin: (@line-height-computed / 2);
}
- ul.file-tree-list {
+ ul.file-tree-list when (@is-overleaf = false) {
font-size: 0.8rem;
- margin: 0;
padding: (@line-height-computed / 4) 0;
+ }
+
+ ul.file-tree-list {
+ margin: 0;
overflow-x: hidden;
height: 100%;
@@ -32,7 +52,7 @@ aside#file-tree {
}
li {
- line-height: 2.6;
+ line-height: @file-tree-line-height;
position: relative;
.entity {
@@ -40,29 +60,42 @@ aside#file-tree {
}
.entity-name {
- color: @gray-darker;
+ color: @file-tree-item-color;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
- background-color: @gray-lightest;
+ background-color: @file-tree-item-hover-bg;
+ }
+ &:hover when (@is-overleaf = true) {
+ // When the entity is a subfolder, the DOM element is "indented" via margin-left. This makes the
+ // element not fill the entire file-tree width (as it's spaced from the left-hand side via margin)
+ // and, in consequence, the background gets clipped. The ::before pseudo-selector is used to fill
+ // the empty space.
+ .fake-full-width-bg(@file-tree-item-hover-bg);
}
input {
line-height: 1.6;
}
- &.droppable-hover {
- background-color: fade(@file-tree-droppable-background-color, 60%);
+ &.droppable-hover when (@is-overleaf = false) {
+ background-color: fade(@file-tree-droppable-bg-color, 60%);
}
+
+ &.droppable-hover when (@is-overleaf = true) {
+ background-color: @file-tree-droppable-bg-color;
+ .fake-full-width-bg(@file-tree-droppable-bg-color);
+ }
+
}
i.fa {
- color: @gray-light;
+ color: @file-tree-item-icon-color;
font-size: 14px;
}
i.fa-folder-open, i.fa-folder {
- color: lighten(desaturate(@link-color, 10%), 5%);
+ color: @file-tree-item-folder-color;
font-size: 14px;
}
@@ -70,14 +103,31 @@ aside#file-tree {
width: 24px;
padding: 6px;
font-size: 0.7rem;
- color: @gray
+ color: @file-tree-item-toggle-color;
}
&.multi-selected {
- > .entity > .entity-name {
- background-color: lighten(@brand-info, 40%);
+ > .entity > .entity-name when (@is-overleaf = false) {
+ background-color: @file-tree-multiselect-bg;
&:hover {
- background-color: lighten(@brand-info, 30%);
+ background-color: @file-tree-multiselect-hover-bg;
+ }
+ }
+ > .entity when (@is-overleaf = true) {
+ > .entity-name {
+ > div > i.fa,
+ > i.fa,
+ .entity-menu-toggle i.fa {
+ color: #FFF;
+ }
+ color: #FFF;
+ font-weight: bold;
+ background-color: @file-tree-multiselect-bg;
+ .fake-full-width-bg(@file-tree-multiselect-bg);
+ &:hover {
+ background-color: @file-tree-multiselect-hover-bg;
+ .fake-full-width-bg(@file-tree-multiselect-hover-bg);
+ }
}
}
}
@@ -96,6 +146,7 @@ aside#file-tree {
top: 1px;
left: 44px;
right: 32px;
+ color: @file-tree-item-input-color;
input {
width: 100%;
}
@@ -111,7 +162,7 @@ aside#file-tree {
&:not(.multi-selected) {
ul.file-tree-list li.selected {
- > .entity > .entity-name {
+ > .entity > .entity-name when (@is-overleaf = false) {
color: @link-color;
border-right: 4px solid @link-color;
font-weight: bold;
@@ -123,15 +174,38 @@ aside#file-tree {
display: inline;
}
}
+
+ > .entity when (@is-overleaf = true) {
+ > .entity-name {
+ > div > i.fa,
+ > i.fa,
+ .entity-menu-toggle i.fa {
+ color: #FFF;
+ }
+ background-color: @file-tree-item-selected-bg;
+ font-weight: bold;
+ padding-right: 32px;
+ .fake-full-width-bg(@file-tree-item-selected-bg);
+
+ .entity-menu-toggle {
+ display: inline;
+ }
+ }
+ }
}
}
- ul.droppable-hover {
- background-color: fade(@file-tree-droppable-background-color, 60%);
+ ul.droppable-hover.file-tree-list when (@is-overleaf = false) {
+ background-color: fade(@file-tree-droppable-bg-color, 60%);
+ }
+ ul.droppable-hover.file-tree-list when (@is-overleaf = true) {
+ background-color: @file-tree-droppable-bg-color;
+ .fake-full-width-bg(@file-tree-droppable-bg-color);
}
}
-.editor-dark {
+// TODO check if the OL Beta theme is OK with darker themes.
+.editor-dark when (@is-overleaf = false) {
aside#file-tree {
// background-color: lighten(@editor-dark-background-color, 10%);
@@ -159,6 +233,4 @@ aside#file-tree {
}
}
}
-
-
}
diff --git a/services/web/public/stylesheets/app/editor/history.less b/services/web/public/stylesheets/app/editor/history.less
index 81af8f989f..b6dca4b7cc 100644
--- a/services/web/public/stylesheets/app/editor/history.less
+++ b/services/web/public/stylesheets/app/editor/history.less
@@ -70,7 +70,7 @@
}
aside.change-list {
- border-left: 1px solid @toolbar-border-color;
+ border-left: 1px solid @editor-border-color;
height: 100%;
width: @changesListWidth;
position: absolute;
@@ -91,7 +91,7 @@
.day {
background-color: #fafafa;
- border-bottom: 1px solid @toolbar-border-color;
+ border-bottom: 1px solid @editor-border-color;
padding: 4px;
font-weight: bold;
text-align: center;
@@ -132,7 +132,7 @@
padding: (@line-height-computed / 4);
padding-left: 38px;
min-height: 38px;
- border-bottom: 1px solid @toolbar-border-color;
+ border-bottom: 1px solid @editor-border-color;
cursor: pointer;
&:hover {
background-color: @gray-lightest;
diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less
index 0c98050242..a873cd5e8f 100644
--- a/services/web/public/stylesheets/app/editor/pdf.less
+++ b/services/web/public/stylesheets/app/editor/pdf.less
@@ -1,3 +1,15 @@
+.pdf .toolbar.toolbar-pdf when (@is-overleaf = true) {
+ .toolbar-small-mixin;
+ .toolbar-alt-mixin;
+ border-bottom: 0;
+ padding-right: 5px;
+}
+
+.pdf .toolbar.toolbar-pdf when (@is-overleaf = false) {
+ .toolbar-tall-mixin;
+ padding: 0 (@line-height-computed / 2);
+}
+
.pdf-viewer, .pdf-logs, .pdf-errors, .pdf-uncompiled {
.full-size;
top: 58px;
@@ -13,6 +25,42 @@
}
}
+.btn-recompile-group when (@is-overleaf = true) {
+ align-self: stretch;
+ margin-right: 5px;
+}
+
+.btn-recompile-group when (@is-overleaf = false) {
+ margin-right: (@line-height-computed / 2);
+}
+
+
+.btn-recompile when (@is-overleaf = true) {
+ height: 100%;
+ .btn-primary;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ &:first-child {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+}
+
+.btn-recompile when (@is-overleaf = false) {
+ .btn-info;
+}
+
+.btn-split-screen when (@is-overleaf = false) {
+ .fa {
+ display: none;
+ }
+}
+.btn-split-screen when (@is-overleaf = true) {
+ .fa {
+ display: none;
+ }
+}
+
.pdf-viewer {
iframe {
width: 100%;
@@ -82,31 +130,43 @@
.pdf .toolbar {
.toolbar-right {
- margin-right: @line-height-computed / 2;
a {
&:hover {
- i {
+ i when (@is-overleaf = false) {
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25);
border-color: @gray-dark;
}
}
- i {
+ i when (@is-overleaf = false) {
display: inline-block;
width: 16px;
height: 16px;
border: 1px solid @gray-light;
margin-top: 5px;
}
+
i.full-screen {
border-top-width: 3px;
border-radius: 2px;
}
+
+ i.full-screen when (@is-overleaf = true) {
+ display: none;
+ }
i.split-screen {
width: 7px;
border-top-width: 3px;
border-radius: 2px;
margin-left: 2px;
}
+
+ i.split-screen when (@is-overleaf = true) {
+ display: none;
+ }
+
+ i.fa when (@is-overleaf = false) {
+ display: none;
+ }
}
}
}
diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less
index 15ed39f6a7..5e846be008 100644
--- a/services/web/public/stylesheets/app/editor/review-panel.less
+++ b/services/web/public/stylesheets/app/editor/review-panel.less
@@ -902,6 +902,13 @@
}
}
+.review-icon when (@is-overleaf) {
+ background-position-y: -60px;
+ .toolbar .btn-full-height:hover & {
+ background-position-y: -60px;
+ }
+}
+
.resolved-comments-toggle {
font-size: 14px;
color: lighten(@rp-type-blue, 25%);
diff --git a/services/web/public/stylesheets/app/editor/search.less b/services/web/public/stylesheets/app/editor/search.less
index 93e7e507d6..f31c59dd6f 100644
--- a/services/web/public/stylesheets/app/editor/search.less
+++ b/services/web/public/stylesheets/app/editor/search.less
@@ -1,6 +1,6 @@
.ace_search {
background-color: @gray-lightest;
- border: 1px solid @toolbar-border-color;
+ border: 1px solid @editor-border-color;
border-top: 0 none;
width: 350px;
overflow: hidden;
diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less
index 1bb5f62bdc..5c00f00567 100644
--- a/services/web/public/stylesheets/app/editor/toolbar.less
+++ b/services/web/public/stylesheets/app/editor/toolbar.less
@@ -1,6 +1,8 @@
.toolbar {
+ display: flex;
+ align-items: center;
height: 40px;
- border-bottom: 1px solid @toolbar-border-color;
+ border-bottom: @toolbar-border-bottom;
> a, .toolbar-right > a {
position: relative;
@@ -22,13 +24,16 @@
.toolbar-left > a:not(.btn),
.toolbar-right > a:not(.btn) {
display: inline-block;
- color: @gray-light;
- padding: 4px 10px 5px;
- margin: 1px 2px;
+ color: @toolbar-icon-btn-color;
+ padding: 0 5px;
border-radius: @border-radius-small;
+ &.toolbar-header-back-projects {
+ padding: 5px 10px 4px;
+ margin-bottom: 1px;
+ }
&:hover {
- text-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
- color: @gray-dark;
+ text-shadow: @toolbar-icon-btn-hover-shadow;
+ color: @toolbar-icon-btn-hover-color;
text-decoration: none;
}
&.active, &:active {
@@ -37,7 +42,7 @@
}
color: white;
background-color: @link-color;
- .box-shadow(inset 0 3px 5px rgba(0, 0, 0, 0.225));
+ box-shadow: @toolbar-icon-btn-hover-boxshadow;
&:hover {
color: white;
}
@@ -48,18 +53,18 @@
border: none;
border-radius: 0;
border-right: 1px solid @toolbar-border-color;
- color: @link-color;
+ color: @toolbar-btn-color;
padding: 3px 10px 5px;
font-size: 20px;
&:hover {
- text-shadow: 0 1px 0 rgba(0, 0, 0, 0.15);
- background-color: darken(white, 10%);
- color: @link-hover-color;
+ text-shadow: @toolbar-btn-hover-text-shadow;
+ background-color: @toolbar-btn-hover-bg-color;
+ color: @toolbar-btn-hover-color;
}
&.active, &:active {
- color: white;
- background-color: @link-color;
- .box-shadow(inset 0 3px 5px rgba(0, 0, 0, 0.225));
+ color: @toolbar-btn-active-color;
+ background-color: @toolbar-btn-active-bg-color;
+ box-shadow: @toolbar-btn-active-shadow;
}
.label {
top: 4px;
@@ -72,12 +77,17 @@
}
.toolbar-left {
+ display: flex;
float: left;
text-align: center;
+ align-items: center;
}
.toolbar-right {
- float: right;
+ display: flex;
+ align-items: center;
+ flex-grow: 1;
+ justify-content: flex-end;
.btn-full-height {
border-right: 0;
border-left: 1px solid @toolbar-border-color;
@@ -96,7 +106,8 @@
}
&.toolbar-header {
- box-shadow: 0 0 2px #ccc;
+ background-color: @toolbar-header-bg-color;
+ box-shadow: @toolbar-header-shadow;
position: absolute;
top: 0;
left: 0;
@@ -105,43 +116,30 @@
}
&.toolbar-small {
- height: 32px;
- > a, .toolbar-right > a {
- padding: 2px 4px 1px 4px;
- margin: 0;
- margin-top: 2px;
- }
- > a {
- margin-left: 2px;
- }
- .toolbar-right > a {
- margin-left: 0;
- margin-right: 2px;
- }
+ .toolbar-small-mixin;
}
&.toolbar-tall {
- height: 58px;
- padding-top: 10px;
- > a, .toolbar-right > a {
- padding: 4px 10px 5px;
- }
- > a.btn, .toolbar-right > a.btn {
- margin: 0 (@line-height-computed / 2);
- }
- .btn-group {
- margin: 0 (@line-height-computed / 2);
- > .btn-group {
- margin: 0;
- }
- }
+ .toolbar-small-mixin;
}
&.toolbar-alt {
- background-color: #fafafa;
+ .toolbar-alt-mixin;
}
}
+.toolbar-small-mixin() {
+ height: 32px;
+}
+
+.toolbar-tall-mixin() {
+ height: 58px;
+ padding-top: 10px;
+}
+.toolbar-alt-mixin() {
+ background-color: @toolbar-alt-bg-color;
+}
+
.toolbar-label {
display: none;
margin: 0 4px;
diff --git a/services/web/public/stylesheets/app/list/v1-import-modal.less b/services/web/public/stylesheets/app/list/v1-import-modal.less
new file mode 100644
index 0000000000..64bb9fda37
--- /dev/null
+++ b/services/web/public/stylesheets/app/list/v1-import-modal.less
@@ -0,0 +1,24 @@
+.v1-import-title {
+ text-align: center;
+}
+
+.v1-import-img {
+ width: 100%;
+}
+
+.v1-import-warning {
+ text-align: center;
+ color: #fdce02;
+ font-size: 14em;
+ line-height: 1em;
+}
+
+.v1-import-footer {
+ display: flex;
+ justify-content: space-evenly;
+ text-align: left;
+}
+
+ .v1-import-btn {
+ width: 20rem;
+ }
\ No newline at end of file
diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less
index 1ca170ff89..732b2e7903 100644
--- a/services/web/public/stylesheets/app/project-list.less
+++ b/services/web/public/stylesheets/app/project-list.less
@@ -1,3 +1,5 @@
+@import "./list/v1-import-modal.less";
+
@announcements-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
@keyframes pulse {
@@ -332,6 +334,10 @@ ul.project-list {
.projectName {
margin-right: @line-height-computed / 4;
}
+ .v1ProjectName {
+ margin-right: @line-height-computed / 4;
+ padding: 0;
+ }
.tag-label {
margin-left: @line-height-computed / 4;
diff --git a/services/web/public/stylesheets/app/translations.less b/services/web/public/stylesheets/app/translations.less
index 3b79b94aff..7ae7532b6c 100644
--- a/services/web/public/stylesheets/app/translations.less
+++ b/services/web/public/stylesheets/app/translations.less
@@ -3,7 +3,7 @@
padding: (@line-height-computed / 4) (@line-height-computed / 2);
background-color: @state-warning-bg;
color: #333;
- border-bottom: 1px solid @toolbar-border-color;
+ border-bottom: 1px solid @common-border-color;
text-align:center;
img {
diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less
index 0b5d3bf744..acf6bcc91f 100644
--- a/services/web/public/stylesheets/core/_common-variables.less
+++ b/services/web/public/stylesheets/core/_common-variables.less
@@ -794,7 +794,9 @@
@left-menu-animation-duration: 0.35s;
@toolbar-border-color: @gray-lighter;
-@file-tree-droppable-background-color: rgb(252, 231, 199);
+@common-border-color: @gray-lighter;
+@editor-border-color: @gray-lighter;
+@file-tree-droppable-bg-color: rgb(252, 231, 199);
@editor-dark-background-color: #333;
@editor-dark-toolbar-border-color: #222;
@@ -876,16 +878,45 @@
@folders-untagged-line-height : 1.7;
// Progress bars
-@progress-border-radius : @border-radius-base;
-@progress-border-width : 1px;
-@progress-bar-shadow : inset 0 -1px 0 rgba(0,0,0,.15);
+@progress-border-radius : @border-radius-base;
+@progress-border-width : 1px;
+@progress-bar-shadow : inset 0 -1px 0 rgba(0,0,0,.15);
// Footer
-@footer-link-color : @link-color;
-@footer-link-hover-color : @link-hover-color;
-@footer-bg-color : transparent;
-@footer-padding : 2em;
+@footer-link-color : @link-color;
+@footer-link-hover-color : @link-hover-color;
+@footer-bg-color : transparent;
+@footer-padding : 2em;
+// Editor header
+@toolbar-header-bg-color : transparent;
+@toolbar-header-shadow : 0 0 2px #ccc;
+@toolbar-btn-color : @link-color;
+@toolbar-btn-hover-color : @link-hover-color;
+@toolbar-btn-hover-bg-color : darken(white, 10%);
+@toolbar-btn-hover-text-shadow : 0 1px 0 rgba(0, 0, 0, 0.15);
+@toolbar-btn-active-color : white;
+@toolbar-btn-active-bg-color : @link-color;
+@toolbar-btn-active-shadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
+@toolbar-alt-bg-color : #fafafa;
+@toolbar-icon-btn-color : @gray-light;
+@toolbar-icon-btn-hover-color : @gray-dark;
+@toolbar-icon-btn-hover-shadow : 0 1px 0 rgba(0, 0, 0, 0.25);
+@toolbar-icon-btn-hover-boxshadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
+@toolbar-border-bottom : 1px solid @toolbar-border-color;
+
+// Editor file-tree
+@file-tree-bg : transparent;
+@file-tree-line-height : 2.6;
+@file-tree-item-color : @gray-darker;
+@file-tree-item-toggle-color : @gray;
+@file-tree-item-icon-color : @gray-light;
+@file-tree-item-input-color : inherit;
+@file-tree-item-folder-color : lighten(desaturate(@link-color, 10%), 5%);
+@file-tree-item-hover-bg : @gray-lightest;
+@file-tree-item-selected-bg : transparent;
+@file-tree-multiselect-bg : lighten(@brand-info, 40%);
+@file-tree-multiselect-hover-bg : lighten(@brand-info, 30%);
// Tags
@tag-border-radius : 0.25em;
@tag-bg-color : @label-default-bg;
diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less
index 43646ec542..5c15ad2d3d 100644
--- a/services/web/public/stylesheets/core/ol-variables.less
+++ b/services/web/public/stylesheets/core/ol-variables.less
@@ -8,7 +8,7 @@
@ol-blue-gray-1 : #E4E8EE;
@ol-blue-gray-2 : #9DA7B7;
@ol-blue-gray-3 : #5D6879;
-@ol-blue-gray-4 : #485973;
+@ol-blue-gray-4 : #455265;
@ol-blue-gray-5 : #2C3645;
@ol-blue-gray-6 : #1E2530;
@@ -156,6 +156,36 @@
@footer-link-hover-color : @ol-dark-green;
@footer-padding : 2em 0;
+// Editor header
+@toolbar-header-bg-color : @ol-blue-gray-6;
+@toolbar-header-shadow : none;
+@toolbar-btn-color : #FFF;
+@toolbar-btn-hover-color : #FFF;
+@toolbar-btn-hover-bg-color : @ol-blue-gray-5;
+@toolbar-btn-hover-text-shadow : none;
+@toolbar-btn-active-color : #FFF;
+@toolbar-btn-active-bg-color : @ol-green;
+@toolbar-btn-active-shadow : none;
+@toolbar-border-color : @ol-blue-gray-5;
+@toolbar-alt-bg-color : @ol-blue-gray-5;
+@toolbar-icon-btn-color : #FFF;
+@toolbar-icon-btn-hover-color : #FFF;
+@toolbar-icon-btn-hover-shadow : none;
+@toolbar-icon-btn-hover-boxshadow : none;
+@toolbar-border-bottom : 1px solid @toolbar-border-color;
+// Editor file-tree
+@file-tree-bg : @ol-blue-gray-4;
+@file-tree-line-height : 2.05;
+@file-tree-item-color : #FFF;
+@file-tree-item-input-color : @ol-blue-gray-5;
+@file-tree-item-toggle-color : @ol-blue-gray-2;
+@file-tree-item-icon-color : @ol-blue-gray-2;
+@file-tree-item-folder-color : @ol-blue-gray-2;
+@file-tree-item-hover-bg : @ol-blue-gray-5;
+@file-tree-item-selected-bg : @ol-green;
+@file-tree-multiselect-bg : @ol-blue;
+@file-tree-multiselect-hover-bg : @ol-dark-blue;
+@file-tree-droppable-bg-color : tint(@ol-green, 5%);
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
new file mode 100644
index 0000000000..e54d4fac9d
--- /dev/null
+++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
@@ -0,0 +1,307 @@
+async = require "async"
+expect = require("chai").expect
+mkdirp = require "mkdirp"
+ObjectId = require("mongojs").ObjectId
+Path = require "path"
+fs = require "fs"
+Settings = require "settings-sharelatex"
+_ = require "underscore"
+
+ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js"
+
+MockDocUpdaterApi = require './helpers/MockDocUpdaterApi'
+MockFileStoreApi = require './helpers/MockFileStoreApi'
+MockProjectHistoryApi = require './helpers/MockProjectHistoryApi'
+request = require "./helpers/request"
+User = require "./helpers/User"
+
+describe "ProjectStructureChanges", ->
+ before (done) ->
+ @owner = new User()
+ @owner.login done
+
+ describe "creating a project from the example template", ->
+ before (done) ->
+ MockDocUpdaterApi.clearProjectStructureUpdates()
+ @owner.createProject "example-project", {template: "example"}, (error, project_id) =>
+ throw error if error?
+ @example_project_id = project_id
+ done()
+
+ it "should version creating a doc", ->
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
+ expect(updates.length).to.equal(2)
+ _.each updates, (update) =>
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.docLines).to.be.a('string')
+ expect(_.where(updates, pathname: "/main.tex").length).to.equal 1
+ expect(_.where(updates, pathname: "/references.bib").length).to.equal 1
+
+ it "should version creating a file", ->
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/universe.jpg")
+ expect(update.url).to.be.a('string');
+
+ describe "duplicating a project", ->
+ before (done) ->
+ MockDocUpdaterApi.clearProjectStructureUpdates()
+ @owner.request.post {
+ uri: "/Project/#{@example_project_id}/clone",
+ json:
+ projectName: 'new.tex'
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to add doc #{res.statusCode}")
+ @dup_project_id = body.project_id
+ done()
+
+ it "should version the dosc created", ->
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).docUpdates
+ expect(updates.length).to.equal(2)
+ _.each updates, (update) =>
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.docLines).to.be.a('string')
+ expect(_.where(updates, pathname: "/main.tex").length).to.equal(1)
+ expect(_.where(updates, pathname: "/references.bib").length).to.equal(1)
+
+ it "should version the files created", ->
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/universe.jpg")
+ expect(update.url).to.be.a('string');
+
+ describe "adding a doc", ->
+ before (done) ->
+ MockDocUpdaterApi.clearProjectStructureUpdates()
+
+ ProjectGetter.getProject @example_project_id, (error, projects) =>
+ throw error if error?
+ @owner.request.post {
+ uri: "project/#{@example_project_id}/doc",
+ json:
+ name: 'new.tex'
+ parent_folder_id: projects[0].rootFolder[0]._id
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to add doc #{res.statusCode}")
+ done()
+
+ it "should version the doc added", ->
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/new.tex")
+ expect(update.docLines).to.be.a('string');
+
+ describe "uploading a project", ->
+ before (done) ->
+ MockDocUpdaterApi.clearProjectStructureUpdates()
+
+ zip_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test_project.zip'))
+
+ req = @owner.request.post {
+ uri: "project/new/upload",
+ formData:
+ qqfile: zip_file
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to upload project #{res.statusCode}")
+ @uploaded_project_id = JSON.parse(body).project_id
+ done()
+
+ it "should version the dosc created", ->
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id).docUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/main.tex")
+ expect(update.docLines).to.equal("Test")
+
+ it "should version the files created", ->
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/1pixel.png")
+ expect(update.url).to.be.a('string');
+
+ describe "uploading a file", ->
+ before (done) ->
+ ProjectGetter.getProject @example_project_id, (error, projects) =>
+ throw error if error?
+ @root_folder_id = projects[0].rootFolder[0]._id.toString()
+ done()
+
+ beforeEach () ->
+ MockDocUpdaterApi.clearProjectStructureUpdates()
+
+ it "should version a newly uploaded file", (done) ->
+ image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/1pixel.png'))
+
+ req = @owner.request.post {
+ uri: "project/#{@example_project_id}/upload",
+ qs:
+ folder_id: @root_folder_id
+ formData:
+ qqfile:
+ value: image_file
+ options:
+ filename: '1pixel.png',
+ contentType: 'image/png'
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to upload file #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/1pixel.png")
+ expect(update.url).to.be.a('string');
+ @original_file_url = update.url
+
+ done()
+
+ it "should version a replacement file", (done) ->
+ image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/2pixel.png'))
+
+ req = @owner.request.post {
+ uri: "project/#{@example_project_id}/upload",
+ qs:
+ folder_id: @root_folder_id
+ formData:
+ qqfile:
+ value: image_file
+ options:
+ filename: '1pixel.png',
+ contentType: 'image/png'
+ }, (error, res, body) =>
+ throw error if error?
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to upload file #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/1pixel.png")
+ expect(update.url).to.be.a('string');
+
+ done()
+
+ describe "tpds", ->
+ before (done) ->
+ @tpds_project_name = "tpds-project-#{new ObjectId().toString()}"
+ @owner.createProject @tpds_project_name, (error, project_id) =>
+ throw error if error?
+ @tpds_project_id = project_id
+ mkdirp Settings.path.dumpFolder, done
+
+ beforeEach () ->
+ MockDocUpdaterApi.clearProjectStructureUpdates()
+
+ it "should version adding a doc", (done) ->
+ tex_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test.tex'))
+
+ req = @owner.request.post {
+ uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/test.tex",
+ auth:
+ user: _.keys(Settings.httpAuthUsers)[0]
+ pass: _.values(Settings.httpAuthUsers)[0]
+ sendImmediately: true
+ }
+
+ tex_file.on "error", (err) ->
+ throw err
+
+ req.on "error", (err) ->
+ throw err
+
+ req.on "response", (res) =>
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to upload file #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).docUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/test.tex")
+ expect(update.docLines).to.equal("Test")
+
+ done()
+
+ tex_file.pipe(req)
+
+ it "should version adding a new file", (done) ->
+ image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/1pixel.png'))
+
+ req = @owner.request.post {
+ uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/1pixel.png",
+ auth:
+ user: _.keys(Settings.httpAuthUsers)[0]
+ pass: _.values(Settings.httpAuthUsers)[0]
+ sendImmediately: true
+ }
+
+ image_file.on "error", (err) ->
+ throw err
+
+ req.on "error", (err) ->
+ throw err
+
+ req.on "response", (res) =>
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to upload file #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/1pixel.png")
+ expect(update.url).to.be.a('string');
+
+ done()
+
+ image_file.pipe(req)
+
+ it "should version replacing a file", (done) ->
+ image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/2pixel.png'))
+
+ req = @owner.request.post {
+ uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/1pixel.png",
+ auth:
+ user: _.keys(Settings.httpAuthUsers)[0]
+ pass: _.values(Settings.httpAuthUsers)[0]
+ sendImmediately: true
+ }
+
+ image_file.on "error", (err) ->
+ throw err
+
+ req.on "error", (err) ->
+ throw err
+
+ req.on "response", (res) =>
+ if res.statusCode < 200 || res.statusCode >= 300
+ throw new Error("failed to upload file #{res.statusCode}")
+
+ updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).fileUpdates
+ expect(updates.length).to.equal(1)
+ update = updates[0]
+ expect(update.userId).to.equal(@owner._id)
+ expect(update.pathname).to.equal("/1pixel.png")
+ expect(update.url).to.be.a('string');
+
+ done()
+
+ image_file.pipe(req)
diff --git a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
index aefcd4513a..b00cd6b173 100644
--- a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
@@ -1,11 +1,38 @@
express = require("express")
app = express()
+bodyParser = require "body-parser"
+jsonParser = bodyParser.json()
module.exports = MockDocUpdaterApi =
+ updates: {}
+
+ clearProjectStructureUpdates: () ->
+ @updates = {}
+
+ getProjectStructureUpdates: (project_id) ->
+ @updates[project_id] || { docUpdates: [], fileUpdates: [] }
+
+ addProjectStructureUpdates: (project_id, userId, docUpdates, fileUpdates) ->
+ @updates[project_id] ||= { docUpdates: [], fileUpdates: [] }
+
+ for update in docUpdates
+ update.userId = userId
+ @updates[project_id].docUpdates.push(update)
+
+ for update in fileUpdates
+ update.userId = userId
+ @updates[project_id].fileUpdates.push(update)
+
run: () ->
app.post "/project/:project_id/flush", (req, res, next) =>
res.sendStatus 200
+ app.post "/project/:project_id", jsonParser, (req, res, next) =>
+ project_id = req.params.project_id
+ {userId, docUpdates, fileUpdates} = req.body
+ @addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates)
+ res.sendStatus 200
+
app.listen 3003, (error) ->
throw error if error?
.on "error", (error) ->
diff --git a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
index 2133d40b9f..c5b003ac75 100644
--- a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
@@ -13,6 +13,7 @@ module.exports = MockDocStoreApi =
@docs[project_id][doc_id] = {lines, version, ranges}
@docs[project_id][doc_id].rev ?= 0
@docs[project_id][doc_id].rev += 1
+ @docs[project_id][doc_id]._id = doc_id
res.json {
modified: true
rev: @docs[project_id][doc_id].rev
diff --git a/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee
new file mode 100644
index 0000000000..f3022302f4
--- /dev/null
+++ b/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee
@@ -0,0 +1,20 @@
+express = require("express")
+app = express()
+
+module.exports = MockFileStoreApi =
+ files: {}
+
+ run: () ->
+ app.post "/project/:project_id/file/:file_id", (req, res, next) =>
+ req.on 'data', ->
+
+ req.on 'end', ->
+ res.send 200
+
+ app.listen 3009, (error) ->
+ throw error if error?
+ .on "error", (error) ->
+ console.error "error starting MockFileStoreApi:", error.message
+ process.exit(1)
+
+MockFileStoreApi.run()
diff --git a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee
new file mode 100644
index 0000000000..9027e22468
--- /dev/null
+++ b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee
@@ -0,0 +1,18 @@
+express = require("express")
+app = express()
+
+module.exports = MockProjectHistoryApi =
+ docs: {}
+
+ run: () ->
+ app.post "/project", (req, res, next) =>
+ res.json project: id: 1
+
+ app.listen 3054, (error) ->
+ throw error if error?
+ .on "error", (error) ->
+ console.error "error starting MockProjectHistoryApi:", error.message
+ process.exit(1)
+
+
+MockProjectHistoryApi.run()
diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee
index d9f0b1409a..ad728fb263 100644
--- a/services/web/test/acceptance/coffee/helpers/User.coffee
+++ b/services/web/test/acceptance/coffee/helpers/User.coffee
@@ -1,6 +1,9 @@
request = require("./request")
+_ = require("underscore")
settings = require("settings-sharelatex")
{db, ObjectId} = require("../../../../app/js/infrastructure/mongojs")
+UserModel = require("../../../../app/js/models/User").User
+AuthenticationManager = require("../../../../app/js/Features/Authentication/AuthenticationManager")
count = 0
@@ -17,20 +20,20 @@ class User
login: (callback = (error) ->) ->
@getCsrfToken (error) =>
return callback(error) if error?
- @request.post {
- url: "/register" # Register will log in, but also ensure user exists
- json:
- email: @email
- password: @password
- }, (error, response, body) =>
+ filter = {@email}
+ options = {upsert: true, new: true, setDefaultsOnInsert: true}
+ UserModel.findOneAndUpdate filter, {}, options, (error, user) =>
return callback(error) if error?
- db.users.findOne {email: @email}, (error, user) =>
+ AuthenticationManager.setUserPassword user._id, @password, (error) =>
return callback(error) if error?
@id = user?._id?.toString()
@_id = user?._id?.toString()
@first_name = user?.first_name
@referal_id = user?.referal_id
- callback()
+ @request.post {
+ url: "/login"
+ json: { @email, @password }
+ }, callback
logout: (callback = (error) ->) ->
@getCsrfToken (error) =>
@@ -96,16 +99,21 @@ class User
getProject: (project_id, callback = (error, project)->) ->
db.projects.findOne {_id: ObjectId(project_id.toString())}, callback
- createProject: (name, callback = (error, project_id) ->) ->
+ createProject: (name, options, callback = (error, oroject_id) ->) ->
+ if typeof options == "function"
+ callback = options
+ options = {}
+
@request.post {
url: "/project/new",
- json:
- projectName: name
+ json: Object.assign({projectName: name}, options)
}, (error, response, body) ->
return callback(error) if error?
if !body?.project_id?
- console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body
- callback(null, body.project_id)
+ error = new Error("SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body)
+ callback error
+ else
+ callback(null, body.project_id)
deleteProject: (project_id, callback=(error)) ->
@request.delete {
@@ -161,13 +169,10 @@ class User
getCsrfToken: (callback = (error) ->) ->
@request.get {
- url: "/register"
+ url: "/dev/csrf"
}, (err, response, body) =>
return callback(err) if err?
- csrfMatches = body.match("window.csrfToken = \"(.*?)\";")
- if !csrfMatches?
- return callback(new Error("no csrf token found"))
- @csrfToken = csrfMatches[1]
+ @csrfToken = body
@request = @request.defaults({
headers:
"x-csrf-token": @csrfToken
diff --git a/services/web/test/acceptance/coffee/helpers/redis.coffee b/services/web/test/acceptance/coffee/helpers/redis.coffee
index 9aecf6b387..7c48f97d2e 100644
--- a/services/web/test/acceptance/coffee/helpers/redis.coffee
+++ b/services/web/test/acceptance/coffee/helpers/redis.coffee
@@ -1,5 +1,4 @@
Settings = require('settings-sharelatex')
-redis = require('redis-sharelatex')
logger = require("logger-sharelatex")
Async = require('async')
diff --git a/services/web/test/acceptance/coffee/helpers/request.coffee b/services/web/test/acceptance/coffee/helpers/request.coffee
index 879acd843a..1c7120d141 100644
--- a/services/web/test/acceptance/coffee/helpers/request.coffee
+++ b/services/web/test/acceptance/coffee/helpers/request.coffee
@@ -1,4 +1,4 @@
-BASE_URL = "http://localhost:3000"
+BASE_URL = "http://#{process.env["HTTP_TEST_HOST"] or "localhost"}:3000"
module.exports = require("request").defaults({
baseUrl: BASE_URL,
followRedirect: false
diff --git a/services/web/test/acceptance/files/1pixel.png b/services/web/test/acceptance/files/1pixel.png
new file mode 100644
index 0000000000..a97e79570b
Binary files /dev/null and b/services/web/test/acceptance/files/1pixel.png differ
diff --git a/services/web/test/acceptance/files/2pixel.png b/services/web/test/acceptance/files/2pixel.png
new file mode 100644
index 0000000000..dea591ec24
Binary files /dev/null and b/services/web/test/acceptance/files/2pixel.png differ
diff --git a/services/web/test/acceptance/files/test.tex b/services/web/test/acceptance/files/test.tex
new file mode 100644
index 0000000000..8318c86b35
--- /dev/null
+++ b/services/web/test/acceptance/files/test.tex
@@ -0,0 +1 @@
+Test
\ No newline at end of file
diff --git a/services/web/test/acceptance/files/test_project.zip b/services/web/test/acceptance/files/test_project.zip
new file mode 100644
index 0000000000..eb489458c3
Binary files /dev/null and b/services/web/test/acceptance/files/test_project.zip differ
diff --git a/services/web/test/acceptance/scripts/full-test.sh b/services/web/test/acceptance/scripts/full-test.sh
index ca10c32a0a..9351df81e3 100755
--- a/services/web/test/acceptance/scripts/full-test.sh
+++ b/services/web/test/acceptance/scripts/full-test.sh
@@ -3,7 +3,7 @@
# If you're running on OS X, you probably need to rebuild
# some dependencies in the docker container, before it will start.
#
-# npm rebuild --update-binary
+#npm rebuild --update-binary
echo ">> Starting server..."
diff --git a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
index ec81ae1c2e..14ccaa3a33 100644
--- a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
+++ b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
@@ -390,26 +390,13 @@ describe 'DocumentUpdaterHandler', ->
describe "updateProjectStructure ", ->
beforeEach ->
@user_id = 1234
- @docIdA = new ObjectId()
- @docIdB = new ObjectId()
- @oldDocs = [
- { path: '/old_a', doc: _id: @docIdA }
- { path: '/old_b', doc: _id: @docIdB }
- ]
- # create new instances of the same ObjectIds so that == doens't pass
- @newDocs = [
- { path: '/old_a', doc: _id: new ObjectId(@docIdA.toString()) }
- { path: '/new_b', doc: _id: new ObjectId(@docIdB.toString()) }
- ]
- @oldFiles = []
- @newFiles = []
describe "with project history disabled", ->
beforeEach ->
@settings.apis.project_history.enabled = false
@request.post = sinon.stub()
- @handler.updateProjectStructure @project_id, @user_id, @oldDocs, @newDocs, @oldFiles, @newFiles, @callback
+ @handler.updateProjectStructure @project_id, @user_id, {}, @callback
it 'does not make a web request', ->
@request.post.called.should.equal false
@@ -420,20 +407,85 @@ describe 'DocumentUpdaterHandler', ->
describe "with project history enabled", ->
beforeEach ->
@settings.apis.project_history.enabled = true
+ @url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}"
@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "")
- @handler.updateProjectStructure @project_id, @user_id, @oldDocs, @newDocs, @oldFiles, @newFiles, @callback
- it 'should send the structure update to the document updater', ->
- docUpdates = [
- id: @docIdB,
- pathname: "/old_b"
- newPathname: "/new_b"
- ]
+ describe "when an entity has changed name", ->
+ it 'should send the structure update to the document updater', (done) ->
+ @docIdA = new ObjectId()
+ @docIdB = new ObjectId()
+ @changes = {
+ oldDocs: [
+ { path: '/old_a', doc: _id: @docIdA }
+ { path: '/old_b', doc: _id: @docIdB }
+ ]
+ # create new instances of the same ObjectIds so that == doesn't pass
+ newDocs: [
+ { path: '/old_a', doc: _id: new ObjectId(@docIdA.toString()) }
+ { path: '/new_b', doc: _id: new ObjectId(@docIdB.toString()) }
+ ]
+ }
- url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}"
- @request.post
- .calledWith(url: url, json: {docUpdates, fileUpdates: [], userId: @user_id})
- .should.equal true
+ docUpdates = [
+ id: @docIdB.toString(),
+ pathname: "/old_b"
+ newPathname: "/new_b"
+ ]
+
+ @handler.updateProjectStructure @project_id, @user_id, @changes, () =>
+ @request.post
+ .calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
+ .should.equal true
+ done()
+
+ describe "when a doc has been added", ->
+ it 'should send the structure update to the document updater', (done) ->
+ @docId = new ObjectId()
+ @changes = newDocs: [
+ { path: '/foo', docLines: 'a\nb', doc: _id: @docId }
+ ]
+
+ docUpdates = [
+ id: @docId.toString(),
+ pathname: "/foo"
+ docLines: 'a\nb'
+ url: undefined
+ ]
+
+ @handler.updateProjectStructure @project_id, @user_id, @changes, () =>
+ @request.post
+ .calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
+ .should.equal true
+ done()
+
+ describe "when a file has been added", ->
+ it 'should send the structure update to the document updater', (done) ->
+ @fileId = new ObjectId()
+ @changes = newFiles: [
+ { path: '/bar', url: 'filestore.example.com/file', file: _id: @fileId }
+ ]
+
+ fileUpdates = [
+ id: @fileId.toString(),
+ pathname: "/bar"
+ url: 'filestore.example.com/file'
+ docLines: undefined
+ ]
+
+ @handler.updateProjectStructure @project_id, @user_id, @changes, () =>
+ @request.post
+ .calledWith(url: @url, json: {docUpdates: [], fileUpdates, userId: @user_id})
+ .should.equal true
+ done()
+
+ describe "when a doc has been deleted", ->
+ it 'should do nothing', (done) ->
+ @docId = new ObjectId()
+ @changes = oldDocs: [
+ { path: '/foo', docLines: 'a\nb', doc: _id: @docId }
+ ]
+
+ @handler.updateProjectStructure @project_id, @user_id, @changes, () =>
+ @request.post.called.should.equal false
+ done()
- it "should call the callback with no error", ->
- @callback.calledWith(null).should.equal true
diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
index ae5e8d79e3..4e1af79b46 100644
--- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
+++ b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
@@ -131,24 +131,24 @@ describe "EditorController", ->
@docLines = ["1234","dskl"]
it 'should add the doc using the project entity handler', (done)->
- mock = sinon.mock(@ProjectEntityHandler).expects("addDoc").withArgs(@project_id, @folder_id, @docName, @docLines).callsArg(4)
+ mock = sinon.mock(@ProjectEntityHandler).expects("addDoc").withArgs(@project_id, @folder_id, @docName, @docLines).callsArg(5)
- @EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, ->
+ @EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, @user_id, ->
mock.verify()
done()
it 'should send the update out to the users in the project', (done)->
- @ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(4, null, @doc, @folder_id)
+ @ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(5, null, @doc, @folder_id)
- @EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, =>
+ @EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, @user_id, =>
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "reciveNewDoc", @folder_id, @doc, @source)
.should.equal true
done()
it 'should return the doc to the callback', (done) ->
- @ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(4, null, @doc, @folder_id)
- @EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, (error, doc) =>
+ @ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(5, null, @doc, @folder_id)
+ @EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, @user_id, (error, doc) =>
doc.should.equal @doc
done()
@@ -157,32 +157,29 @@ describe "EditorController", ->
beforeEach ->
@LockManager.getLock.callsArgWith(1)
@LockManager.releaseLock.callsArgWith(1)
- @EditorController.addDocWithoutLock = sinon.stub().callsArgWith(5)
+ @EditorController.addDocWithoutLock = sinon.stub().callsArgWith(6)
it "should call addDocWithoutLock", (done)->
- @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, =>
- @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @docName, @docLines, @source).should.equal true
+ @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, =>
+ @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @docName, @docLines, @source, @user_id).should.equal true
done()
it "should take the lock", (done)->
- @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, =>
+ @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, =>
@LockManager.getLock.calledWith(@project_id).should.equal true
done()
it "should release the lock", (done)->
- @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, =>
+ @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, =>
@LockManager.releaseLock.calledWith(@project_id).should.equal true
done()
it "should error if it can't cat the lock", (done)->
@LockManager.getLock = sinon.stub().callsArgWith(1, "timed out")
- @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, (err)=>
+ @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, (err)=>
expect(err).to.exist
err.should.equal "timed out"
- done()
-
-
-
+ done()
describe 'addFileWithoutLock:', ->
beforeEach ->
@@ -196,23 +193,23 @@ describe "EditorController", ->
@stream = new ArrayBuffer()
it 'should add the folder using the project entity handler', (done)->
- @ProjectEntityHandler.addFile = sinon.stub().callsArgWith(4)
- @EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, =>
- @ProjectEntityHandler.addFile.calledWith(@project_id, @folder_id).should.equal true
+ @ProjectEntityHandler.addFile = sinon.stub().callsArgWith(5)
+ @EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, @user_id, =>
+ @ProjectEntityHandler.addFile.calledWith(@project_id, @folder_id, @fileName, @stream, @user_id).should.equal true
done()
it 'should send the update of a new folder out to the users in the project', (done)->
- @ProjectEntityHandler.addFile = sinon.stub().callsArgWith(4, null, @file, @folder_id)
+ @ProjectEntityHandler.addFile = sinon.stub().callsArgWith(5, null, @file, @folder_id)
- @EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, =>
+ @EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, @user_id, =>
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "reciveNewFile", @folder_id, @file, @source)
.should.equal true
done()
it "should return the file in the callback", (done) ->
- @ProjectEntityHandler.addFile = sinon.stub().callsArgWith(4, null, @file, @folder_id)
- @EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, (error, file) =>
+ @ProjectEntityHandler.addFile = sinon.stub().callsArgWith(5, null, @file, @folder_id)
+ @EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
file.should.equal @file
done()
@@ -222,28 +219,28 @@ describe "EditorController", ->
beforeEach ->
@LockManager.getLock.callsArgWith(1)
@LockManager.releaseLock.callsArgWith(1)
- @EditorController.addFileWithoutLock = sinon.stub().callsArgWith(5)
+ @EditorController.addFileWithoutLock = sinon.stub().callsArgWith(6)
it "should call addFileWithoutLock", (done)->
- @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, (error, file) =>
- @EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @fileName, @stream, @source).should.equal true
+ @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
+ @EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @fileName, @stream, @source, @user_id).should.equal true
done()
it "should take the lock", (done)->
- @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, (error, file) =>
+ @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
@LockManager.getLock.calledWith(@project_id).should.equal true
done()
it "should release the lock", (done)->
- @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, (error, file) =>
+ @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
@LockManager.releaseLock.calledWith(@project_id).should.equal true
done()
it "should error if it can't cat the lock", (done)->
@LockManager.getLock = sinon.stub().callsArgWith(1, "timed out")
- @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, (err, file) =>
- expect(err).to.exist
- err.should.equal "timed out"
+ @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) =>
+ expect(error).to.exist
+ error.should.equal "timed out"
done()
@@ -256,9 +253,9 @@ describe "EditorController", ->
@fsPath = "/folder/file.png"
it 'should send the replace file message to the editor controller', (done)->
- @ProjectEntityHandler.replaceFile = sinon.stub().callsArgWith(3)
- @EditorController.replaceFile @project_id, @file_id, @fsPath, @source, =>
- @ProjectEntityHandler.replaceFile.calledWith(@project_id, @file_id, @fsPath).should.equal true
+ @ProjectEntityHandler.replaceFile = sinon.stub().callsArgWith(4)
+ @EditorController.replaceFile @project_id, @file_id, @fsPath, @source, @user_id, =>
+ @ProjectEntityHandler.replaceFile.calledWith(@project_id, @file_id, @fsPath, @user_id).should.equal true
done()
describe 'addFolderWithoutLock :', ->
diff --git a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
index 2df4af72ec..e05846c5d5 100644
--- a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
+++ b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
@@ -201,7 +201,7 @@ describe "EditorHttpController", ->
@req.body =
name: @name = "doc-name"
parent_folder_id: @parent_folder_id
- @EditorController.addDoc = sinon.stub().callsArgWith(5, null, @doc)
+ @EditorController.addDoc = sinon.stub().callsArgWith(6, null, @doc)
describe "successfully", ->
beforeEach ->
@@ -209,7 +209,7 @@ describe "EditorHttpController", ->
it "should call EditorController.addDoc", ->
@EditorController.addDoc
- .calledWith(@project_id, @parent_folder_id, @name, [], "editor")
+ .calledWith(@project_id, @parent_folder_id, @name, [], "editor", @userId)
.should.equal true
it "should send the doc back as JSON", ->
diff --git a/services/web/test/unit/coffee/FileStore/FileStoreHandlerTests.coffee b/services/web/test/unit/coffee/FileStore/FileStoreHandlerTests.coffee
index 73f32f3a60..90a3e870d1 100644
--- a/services/web/test/unit/coffee/FileStore/FileStoreHandlerTests.coffee
+++ b/services/web/test/unit/coffee/FileStore/FileStoreHandlerTests.coffee
@@ -16,7 +16,7 @@ describe "FileStoreHandler", ->
})
@writeStream =
my:"writeStream"
- on: (type, cb)->
+ on: (type, cb)->
if type == "response"
cb({statusCode: 200})
@readStream = {my:"readStream", on: sinon.stub()}
@@ -38,7 +38,7 @@ describe "FileStoreHandler", ->
@isSafeOnFileSystem = true
it "should create read stream", (done)->
- @fs.createReadStream.returns
+ @fs.createReadStream.returns
pipe:->
on: (type, cb)->
if type == "open"
@@ -49,8 +49,8 @@ describe "FileStoreHandler", ->
it "should pipe the read stream to request", (done)->
@request.returns(@writeStream)
- @fs.createReadStream.returns
- on: (type, cb)->
+ @fs.createReadStream.returns
+ on: (type, cb)->
if type == "open"
cb()
pipe:(o)=>
@@ -59,9 +59,9 @@ describe "FileStoreHandler", ->
@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, =>
it "should pass the correct options to request", (done)->
- @fs.createReadStream.returns
+ @fs.createReadStream.returns
pipe:->
- on: (type, cb)->
+ on: (type, cb)->
if type == "open"
cb()
@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, =>
@@ -70,23 +70,24 @@ describe "FileStoreHandler", ->
done()
it "builds the correct url", (done)->
- @fs.createReadStream.returns
+ @fs.createReadStream.returns
pipe:->
- on: (type, cb)->
+ on: (type, cb)->
if type == "open"
cb()
@handler.uploadFileFromDisk @project_id, @file_id, @fsPath, =>
@handler._buildUrl.calledWith(@project_id, @file_id).should.equal true
done()
- it 'should callback with null', (done) ->
+ it 'should callback with the url', (done) ->
@fs.createReadStream.returns
pipe:->
on: (type, cb)->
if type == "open"
cb()
- @handler.uploadFileFromDisk @project_id, @file_id, @fsPath, (err) =>
+ @handler.uploadFileFromDisk @project_id, @file_id, @fsPath, (err, url) =>
expect(err).to.not.exist
+ expect(url).to.equal(@handler._buildUrl())
done()
describe "symlink", ->
@@ -218,6 +219,11 @@ describe "FileStoreHandler", ->
@handler._buildUrl.calledWith(@newProject_id, @newFile_id).should.equal true
done()
+ it "returns the url", (done)->
+ @request.callsArgWith(1, null)
+ @handler.copyFile @project_id, @file_id, @newProject_id, @newFile_id, (err, url) =>
+ url.should.equal "http://filestore.stubbedBuilder.com"
+ done()
it "should return the err", (done)->
error = "errrror"
diff --git a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
index 5c32db63d3..470b538bd9 100644
--- a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
@@ -33,8 +33,8 @@ describe 'ProjectCreationHandler', ->
constructor:(options)->
{@name} = options
@ProjectEntityHandler =
- addDoc: sinon.stub().callsArgWith(4, null, {_id: docId})
- addFile: sinon.stub().callsArg(4)
+ addDoc: sinon.stub().callsArgWith(5, null, {_id: docId})
+ addFile: sinon.stub().callsArg(5)
setRootDoc: sinon.stub().callsArg(2)
@ProjectDetailsHandler =
validateProjectName: sinon.stub().yields()
@@ -149,7 +149,7 @@ describe 'ProjectCreationHandler', ->
.should.equal true
it 'should insert main.tex', ->
- @ProjectEntityHandler.addDoc.calledWith(project_id, rootFolderId, "main.tex", ["mainbasic.tex", "lines"])
+ @ProjectEntityHandler.addDoc.calledWith(project_id, rootFolderId, "main.tex", ["mainbasic.tex", "lines"], ownerId)
.should.equal true
it 'should set the main doc id', ->
@@ -180,19 +180,20 @@ describe 'ProjectCreationHandler', ->
it 'should insert main.tex', ->
@ProjectEntityHandler.addDoc
- .calledWith(project_id, rootFolderId, "main.tex", ["main.tex", "lines"])
+ .calledWith(project_id, rootFolderId, "main.tex", ["main.tex", "lines"], ownerId)
.should.equal true
it 'should insert references.bib', ->
@ProjectEntityHandler.addDoc
- .calledWith(project_id, rootFolderId, "references.bib", ["references.bib", "lines"])
+ .calledWith(project_id, rootFolderId, "references.bib", ["references.bib", "lines"], ownerId)
.should.equal true
it 'should insert universe.jpg', ->
@ProjectEntityHandler.addFile
.calledWith(
project_id, rootFolderId, "universe.jpg",
- Path.resolve(__dirname + "/../../../../app/templates/project_files/universe.jpg")
+ Path.resolve(__dirname + "/../../../../app/templates/project_files/universe.jpg"),
+ ownerId
)
.should.equal true
diff --git a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
index a1101cfbe0..b489014e7e 100644
--- a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
@@ -64,8 +64,8 @@ describe 'ProjectDuplicator', ->
@projectOptionsHandler =
setCompiler : sinon.stub()
@entityHandler =
- addDocWithProject: sinon.stub().callsArgWith(4, null, {name:"somDoc"})
- copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(4)
+ addDocWithProject: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
+ copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(5)
setRootDoc: sinon.stub()
addFolderWithProject: sinon.stub().callsArgWith(3, null, @newFolder)
@@ -112,13 +112,13 @@ describe 'ProjectDuplicator', ->
done()
it 'should use the same compiler', (done)->
- @entityHandler.addDocWithProject.callsArgWith(4, null, @rootFolder.docs[0])
+ @entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true
done()
it 'should use the same root doc', (done)->
- @entityHandler.addDocWithProject.callsArgWith(4, null, @rootFolder.docs[0])
+ @entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@entityHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true
done()
@@ -139,14 +139,26 @@ describe 'ProjectDuplicator', ->
it 'should copy all the docs', (done)->
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@DocstoreManager.getAllDocs.calledWith(@old_project_id).should.equal true
- @entityHandler.addDocWithProject.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines).should.equal true
- @entityHandler.addDocWithProject.calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines).should.equal true
- @entityHandler.addDocWithProject.calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines).should.equal true
+ @entityHandler.addDocWithProject
+ .calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id)
+ .should.equal true
+ @entityHandler.addDocWithProject
+ .calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines, @owner._id)
+ .should.equal true
+ @entityHandler.addDocWithProject
+ .calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines, @owner._id)
+ .should.equal true
done()
it 'should copy all the files', (done)->
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
- @entityHandler.copyFileFromExistingProjectWithProject.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @project._id, @rootFolder.fileRefs[0]).should.equal true
- @entityHandler.copyFileFromExistingProjectWithProject.calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level1folder.fileRefs[0]).should.equal true
- @entityHandler.copyFileFromExistingProjectWithProject.calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level2folder.fileRefs[0]).should.equal true
+ @entityHandler.copyFileFromExistingProjectWithProject
+ .calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @project._id, @rootFolder.fileRefs[0], @owner._id)
+ .should.equal true
+ @entityHandler.copyFileFromExistingProjectWithProject
+ .calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level1folder.fileRefs[0], @owner._id)
+ .should.equal true
+ @entityHandler.copyFileFromExistingProjectWithProject
+ .calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level2folder.fileRefs[0], @owner._id)
+ .should.equal true
done()
diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
index dc723bf8bd..f62690e226 100644
--- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
@@ -17,9 +17,10 @@ describe 'ProjectEntityHandler', ->
userId = 1234
beforeEach ->
+ @fileUrl = 'filestore.example.com/file'
@FileStoreHandler =
- uploadFileFromDisk:(project_id, fileRef, localImagePath, callback)->callback()
- copyFile: sinon.stub().callsArgWith(4, null)
+ uploadFileFromDisk: sinon.stub().callsArgWith(3, null, @fileUrl)
+ copyFile: sinon.stub().callsArgWith(4, null, @fileUrl)
@tpdsUpdateSender =
addDoc:sinon.stub().callsArg(1)
addFile:sinon.stub().callsArg(1)
@@ -67,6 +68,9 @@ describe 'ProjectEntityHandler', ->
findElement : sinon.stub()
@settings =
maxEntitiesPerProject:200
+ @documentUpdaterHandler =
+ updateProjectStructure: sinon.stub().yields()
+ deleteDoc: sinon.stub().callsArg(2)
@ProjectEntityHandler = SandboxedModule.require modulePath, requires:
'../../models/Project': Project:@ProjectModel
'../../models/Doc': Doc:@DocModel
@@ -75,7 +79,7 @@ describe 'ProjectEntityHandler', ->
'../FileStore/FileStoreHandler':@FileStoreHandler
'../ThirdPartyDataStore/TpdsUpdateSender':@tpdsUpdateSender
'./ProjectLocator': @projectLocator
- '../../Features/DocumentUpdater/DocumentUpdaterHandler':@documentUpdaterHandler = {}
+ '../../Features/DocumentUpdater/DocumentUpdaterHandler':@documentUpdaterHandler
'../Docstore/DocstoreManager': @DocstoreManager = {}
'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->}
'./ProjectUpdateHandler': @projectUpdater
@@ -184,7 +188,6 @@ describe 'ProjectEntityHandler', ->
describe "_cleanUpEntity", ->
beforeEach ->
@entity_id = "4eecaffcbffa66588e000009"
- @documentUpdaterHandler.deleteDoc = sinon.stub().callsArg(2)
@FileStoreHandler.deleteFile = sinon.stub().callsArg(2)
@ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1)
@@ -240,7 +243,6 @@ describe 'ProjectEntityHandler', ->
@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, path: @pathAfterMove)
@ProjectGetter.getProject.callsArgWith(2, null, @project)
@tpdsUpdateSender.moveEntity = sinon.stub()
- @documentUpdaterHandler.updateProjectStructure = sinon.stub().callsArg(6)
@ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub()
@ProjectEntityHandler.getAllEntitiesFromProject
.onFirstCall()
@@ -272,7 +274,7 @@ describe 'ProjectEntityHandler', ->
it "should should send the update to the doc updater", ->
@documentUpdaterHandler.updateProjectStructure
- .calledWith(project_id, userId, @oldDocs, @newDocs, @oldFiles, @newFiles)
+ .calledWith(project_id, userId, {@oldDocs, @newDocs, @oldFiles, @newFiles})
.should.equal true
it 'should remove the element from its current position', ->
@@ -324,7 +326,7 @@ describe 'ProjectEntityHandler', ->
it "should should send the update to the doc updater", ->
@documentUpdaterHandler.updateProjectStructure
- .calledWith(project_id, userId, @oldDocs, @newDocs, @oldFiles, @newFiles)
+ .calledWith(project_id, userId, {@oldDocs, @newDocs, @oldFiles, @newFiles})
.should.equal true
it 'should remove the element from its current position', ->
@@ -455,7 +457,7 @@ describe 'ProjectEntityHandler', ->
@tpdsUpdateSender.addDoc = sinon.stub().callsArg(1)
@DocstoreManager.updateDoc = sinon.stub().yields(null, true, 0)
- @ProjectEntityHandler.addDoc project_id, folder_id, @name, @lines, @callback
+ @ProjectEntityHandler.addDoc project_id, folder_id, @name, @lines, userId, @callback
# Created doc
@doc = @ProjectEntityHandler._putElement.args[0][2]
@@ -484,6 +486,16 @@ describe 'ProjectEntityHandler', ->
.calledWith(project_id, @doc._id.toString(), @lines)
.should.equal true
+ it "should should send the change in project structure to the doc updater", () ->
+ newDocs = [
+ doc: @doc
+ path: @path
+ docLines: @lines.join('\n')
+ ]
+ @documentUpdaterHandler.updateProjectStructure
+ .calledWith(project_id, userId, {newDocs})
+ .should.equal true
+
describe "restoreDoc", ->
beforeEach ->
@name = "doc-name"
@@ -512,7 +524,10 @@ describe 'ProjectEntityHandler', ->
describe 'addFile', ->
fileName = "something.jpg"
beforeEach ->
+ @fileSystemPath = "somehintg"
+ @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem: @fileSystemPath}})
@filePath = "somewhere"
+
it 'should upload it via the FileStoreHandler', (done)->
@FileStoreHandler.uploadFileFromDisk = (passedProject_id, file_id, filePath, callback)=>
file_id.should.equal "file_id"
@@ -520,7 +535,7 @@ describe 'ProjectEntityHandler', ->
filePath.should.equal @filePath
done()
- @ProjectEntityHandler.addFile project_id, folder_id, fileName, @filePath, (err, fileRef, parentFolder)->
+ @ProjectEntityHandler.addFile project_id, folder_id, fileName, @filePath, userId, (err, fileRef, parentFolder)->
it 'should put file into folder by calling put element', (done)->
@ProjectEntityHandler._putElement = (passedProject, passedFolder_id, passedFileRef, passedType, callback)->
@@ -530,11 +545,10 @@ describe 'ProjectEntityHandler', ->
passedType.should.equal 'file'
done()
- @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, (err, fileRef, parentFolder)->
+ @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, (err, fileRef, parentFolder)->
it 'should return doc and parent folder', (done)->
- @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:"somehintg"}})
- @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, (err, fileRef, parentFolder)->
+ @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, (err, fileRef, parentFolder)->
parentFolder.should.equal folder_id
fileRef.name.should.equal fileName
done()
@@ -554,33 +568,45 @@ describe 'ProjectEntityHandler', ->
options.rev.should.equal 0
done()
- @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, (err, fileRef, parentFolder)->
+ @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, (err, fileRef, parentFolder)->
- describe 'replacing a file', ->
+ it "should should send the change in project structure to the doc updater", (done) ->
+ @documentUpdaterHandler.updateProjectStructure = (passed_project_id, passed_user_id, changes) =>
+ passed_project_id.should.equal project_id
+ passed_user_id.should.equal userId
+ { newFiles } = changes
+ newFiles.length.should.equal 1
+ newFile = newFiles[0]
+ newFile.file.name.should.equal fileName
+ newFile.path.should.equal @fileSystemPath
+ newFile.url.should.equal @fileUrl
+ done()
+ @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, () ->
+
+ describe 'replaceFile', ->
beforeEach ->
@projectLocator
@file_id = "file_id_here"
@fsPath = "fs_path_here.png"
- @fileRef = {rev:3, _id:@file_id}
- @filePaths = {fileSystem:"/folder1/file.png", mongo:"folder.1.files.somewhere"}
+ @fileRef = {rev:3, _id: @file_id, name: @fileName = "fileName"}
+ @filePaths = {fileSystem: @fileSystemPath="/folder1/file.png", mongo:"folder.1.files.somewhere"}
@projectLocator.findElement = sinon.stub().callsArgWith(1, null, @fileRef, @filePaths)
- @ProjectModel.update = (_, __, ___, cb)-> cb()
+ @ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3)
@ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project)
it 'should find the file', (done)->
-
- @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
- @projectLocator.findElement.calledWith({element_id:@file_id, type:"file", project_id:project_id}).should.equal true
+ @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
+ @projectLocator.findElement
+ .calledWith({element_id:@file_id, type:"file", project: @project})
+ .should.equal true
done()
it 'should tell the file store handler to upload the file from disk', (done)->
- @FileStoreHandler.uploadFileFromDisk = sinon.stub().callsArgWith(3)
- @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
+ @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
@FileStoreHandler.uploadFileFromDisk.calledWith(project_id, @file_id, @fsPath).should.equal true
done()
-
it 'should send the file to the tpds with an incremented rev', (done)->
@tpdsUpdateSender.addFile = (options)=>
options.project_id.should.equal project_id
@@ -590,26 +616,39 @@ describe 'ProjectEntityHandler', ->
options.rev.should.equal @fileRef.rev + 1
done()
- @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
+ @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
it 'should inc the rev id', (done)->
- @ProjectModel.update = (conditions, update, options, callback)=>
+ @ProjectModel.findOneAndUpdate = (conditions, update, options, callback)=>
conditions._id.should.equal project_id
update.$inc["#{@filePaths.mongo}.rev"].should.equal 1
done()
- @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
+ @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
it 'should update the created at date', (done)->
d = new Date()
- @ProjectModel.update = (conditions, update, options, callback)=>
+ @ProjectModel.findOneAndUpdate = (conditions, update, options, callback)=>
conditions._id.should.equal project_id
differenceInMs = update.$set["#{@filePaths.mongo}.created"].getTime() - d.getTime()
differenceInMs.should.be.below(20)
done()
- @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, =>
+ @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
+ it "should should send the old and new project structure to the doc updater", (done) ->
+ @documentUpdaterHandler.updateProjectStructure = (passed_project_id, passed_user_id, changes) =>
+ passed_project_id.should.equal project_id
+ passed_user_id.should.equal userId
+ { newFiles } = changes
+ newFiles.length.should.equal 1
+ newFile = newFiles[0]
+ newFile.file.name.should.equal @fileName
+ newFile.path.should.equal @fileSystemPath
+ newFile.url.should.equal @fileUrl
+ done()
+
+ @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, =>
describe 'addFolder', ->
folderName = "folder1234"
@@ -943,19 +982,19 @@ describe 'ProjectEntityHandler', ->
@ProjectModel.update.calledWith({_id : @project_id}, {$unset : {rootDoc_id: true}})
.should.equal true
- describe 'copyFileFromExistingProject', ->
+ describe 'copyFileFromExistingProjectWithProject', ->
fileName = "something.jpg"
filePath = "dumpFolder/somewhere/image.jpeg"
oldProject_id = "123kljadas"
oldFileRef = {name:fileName, _id:"oldFileRef"}
- beforeEach ->
- @ProjectGetter.getProject = (project_id, fields, callback)=> callback(null, {name:@project.name, _id:@project._id})
- @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:"somehintg"}})
+ beforeEach ->
+ @fileSystemPath = "somehintg"
+ @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem: @fileSystemPath}})
it 'should copy the file in FileStoreHandler', (done)->
@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:"somehintg"}})
- @ProjectEntityHandler.copyFileFromExistingProject project_id, folder_id, oldProject_id, oldFileRef, (err, fileRef, parentFolder)=>
+ @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)=>
@FileStoreHandler.copyFile.calledWith(oldProject_id, oldFileRef._id, project_id, fileRef._id).should.equal true
done()
@@ -967,10 +1006,10 @@ describe 'ProjectEntityHandler', ->
passedType.should.equal 'file'
done()
- @ProjectEntityHandler.copyFileFromExistingProject project_id, folder_id, oldProject_id, oldFileRef, (err, fileRef, parentFolder)->
+ @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)->
it 'should return doc and parent folder', (done)->
- @ProjectEntityHandler.copyFileFromExistingProject project_id, folder_id, oldProject_id, oldFileRef, (err, fileRef, parentFolder)->
+ @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)->
parentFolder.should.equal folder_id
fileRef.name.should.equal fileName
done()
@@ -990,8 +1029,21 @@ describe 'ProjectEntityHandler', ->
options.rev.should.equal 0
done()
- @ProjectEntityHandler.copyFileFromExistingProject project_id, folder_id, oldProject_id, oldFileRef, (err, fileRef, parentFolder)->
+ @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)->
+ it "should should send the change in project structure to the doc updater", (done) ->
+ @documentUpdaterHandler.updateProjectStructure = (passed_project_id, passed_user_id, changes) =>
+ passed_project_id.should.equal project_id
+ passed_user_id.should.equal userId
+ { newFiles } = changes
+ newFiles.length.should.equal 1
+ newFile = newFiles[0]
+ newFile.file.name.should.equal fileName
+ newFile.path.should.equal @fileSystemPath
+ newFile.url.should.equal @fileUrl
+ done()
+
+ @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)->
describe "renameEntity", ->
beforeEach ->
@@ -1012,12 +1064,12 @@ describe 'ProjectEntityHandler', ->
@projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: @entity_id, name:"old.tex", rev:4 }, @path)
@tpdsUpdateSender.moveEntity = sinon.stub()
@ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3, null, @project)
- @documentUpdaterHandler.updateProjectStructure = sinon.stub().callsArg(6)
+ @documentUpdaterHandler.updateProjectStructure = sinon.stub().yields()
it "should should send the old and new project structure to the doc updater", (done) ->
@ProjectEntityHandler.renameEntity project_id, @entity_id, @entityType, @newName, userId, =>
@documentUpdaterHandler.updateProjectStructure
- .calledWith(project_id, userId, @oldDocs, @newDocs, @oldFiles, @newFiles)
+ .calledWith(project_id, userId, {@oldDocs, @newDocs, @oldFiles, @newFiles})
.should.equal true
done()
diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
index 1b00f20bde..e20419765f 100644
--- a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
+++ b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
@@ -63,11 +63,11 @@ describe 'UpdateMerger :', ->
file_id = "1231"
@projectLocator.findElementByPath = (_, __, cb)->cb(null, {_id:file_id})
@FileTypeManager.isBinary.callsArgWith(2, null, true)
- @updateMerger.p.processFile = sinon.stub().callsArgWith(5)
+ @updateMerger.p.processFile = sinon.stub().callsArgWith(6)
filePath = "/folder/file1.png"
@updateMerger.mergeUpdate @user_id, @project_id, filePath, @update, @source, =>
- @updateMerger.p.processFile.calledWith(@project_id, file_id, @fsPath, filePath, @source).should.equal true
+ @updateMerger.p.processFile.calledWith(@project_id, file_id, @fsPath, filePath, @source, @user_id).should.equal true
@FileTypeManager.isBinary.calledWith(filePath, @fsPath).should.equal true
@fs.unlink.calledWith(@fsPath).should.equal true
done()
@@ -97,7 +97,7 @@ describe 'UpdateMerger :', ->
path = "folder1/folder2/#{docName}"
@editorController.mkdirp = sinon.stub().withArgs(@project_id).callsArgWith(2, null, [folder], folder)
@editorController.addDoc = ->
- mock = sinon.mock(@editorController).expects("addDoc").withArgs(@project_id, folder._id, docName, @splitDocLines, @source).callsArg(5)
+ mock = sinon.mock(@editorController).expects("addDoc").withArgs(@project_id, folder._id, docName, @splitDocLines, @source, @user_id).callsArg(6)
@update.write(@docLines)
@update.end()
@@ -114,22 +114,22 @@ describe 'UpdateMerger :', ->
@folder = _id: @folder_id
@fileName = "file.png"
@fsPath = "fs/path.tex"
- @editorController.addFile = sinon.stub().callsArg(5)
- @editorController.replaceFile = sinon.stub().callsArg(4)
+ @editorController.addFile = sinon.stub().callsArg(6)
+ @editorController.replaceFile = sinon.stub().callsArg(5)
@editorController.deleteEntity = sinon.stub()
@editorController.mkdirp = sinon.stub().withArgs(@project_id).callsArgWith(2, null, [@folder], @folder)
@updateMerger.p.writeStreamToDisk = sinon.stub().withArgs(@project_id, @file_id, @update).callsArgWith(3, null, @fsPath)
it 'should replace file if the file already exists', (done)->
- @updateMerger.p.processFile @project_id, @file_id, @fsPath, @path, @source, =>
+ @updateMerger.p.processFile @project_id, @file_id, @fsPath, @path, @source, @user_id, =>
@editorController.addFile.called.should.equal false
- @editorController.replaceFile.calledWith(@project_id, @file_id, @fsPath, @source).should.equal true
+ @editorController.replaceFile.calledWith(@project_id, @file_id, @fsPath, @source, @user_id).should.equal true
done()
it 'should call add file if the file does not exist', (done)->
- @updateMerger.p.processFile @project_id, undefined, @fsPath, @path, @source, =>
+ @updateMerger.p.processFile @project_id, undefined, @fsPath, @path, @source, @user_id, =>
@editorController.mkdirp.calledWith(@project_id, "folder/").should.equal true
- @editorController.addFile.calledWith(@project_id, @folder_id, @fileName, @fsPath, @source).should.equal true
+ @editorController.addFile.calledWith(@project_id, @folder_id, @fileName, @fsPath, @source, @user_id).should.equal true
@editorController.replaceFile.called.should.equal false
done()
diff --git a/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee b/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee
index a33775446f..0eb17c0d02 100644
--- a/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee
+++ b/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee
@@ -44,14 +44,14 @@ describe "FileSystemImportManager", ->
describe "with replace set to false", ->
beforeEach ->
- @EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
+ @EditorController.addDocWithoutLock = sinon.stub().callsArg(6)
@FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback
it "should read the file from disk", ->
@fs.readFile.calledWith(@path_on_disk, "utf8").should.equal true
it "should insert the doc", ->
- @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
+ @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id)
.should.equal true
describe "with windows line ending", ->
@@ -59,11 +59,11 @@ describe "FileSystemImportManager", ->
@docContent = "one\r\ntwo\r\nthree"
@docLines = ["one", "two", "three"]
@fs.readFile = sinon.stub().callsArgWith(2, null, @docContent)
- @EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
+ @EditorController.addDocWithoutLock = sinon.stub().callsArg(6)
@FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback
it "should strip the \\r characters before adding", ->
- @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
+ @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id)
.should.equal true
describe "with replace set to true", ->
@@ -76,7 +76,7 @@ describe "FileSystemImportManager", ->
}]
}
@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
- @EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
+ @EditorController.addDocWithoutLock = sinon.stub().callsArg(6)
@FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback
it "should look up the folder", ->
@@ -85,7 +85,7 @@ describe "FileSystemImportManager", ->
.should.equal true
it "should insert the doc", ->
- @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
+ @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id)
.should.equal true
describe "when the doc does exist", ->
@@ -114,12 +114,12 @@ describe "FileSystemImportManager", ->
describe "addFile with replace set to false", ->
beforeEach ->
- @EditorController.addFileWithoutLock = sinon.stub().callsArg(5)
+ @EditorController.addFileWithoutLock = sinon.stub().callsArg(6)
@FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true)
@FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback
it "should add the file", ->
- @EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload")
+ @EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload", @user_id)
.should.equal true
describe "addFile with symlink", ->
@@ -144,7 +144,7 @@ describe "FileSystemImportManager", ->
}
@FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true)
@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
- @EditorController.addFileWithoutLock = sinon.stub().callsArg(5)
+ @EditorController.addFileWithoutLock = sinon.stub().callsArg(6)
@FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback
it "should look up the folder", ->
@@ -153,7 +153,7 @@ describe "FileSystemImportManager", ->
.should.equal true
it "should add the file", ->
- @EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload")
+ @EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload", @user_id)
.should.equal true
describe "when the file does exist", ->
@@ -169,7 +169,7 @@ describe "FileSystemImportManager", ->
}
@FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true)
@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
- @EditorController.replaceFile = sinon.stub().callsArg(4)
+ @EditorController.replaceFile = sinon.stub().callsArg(5)
@FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback
it "should look up the folder", ->
@@ -178,7 +178,7 @@ describe "FileSystemImportManager", ->
.should.equal true
it "should replace the file", ->
- @EditorController.replaceFile.calledWith(@project_id, @file_id, @path_on_disk, "upload")
+ @EditorController.replaceFile.calledWith(@project_id, @file_id, @path_on_disk, "upload", @user_id)
.should.equal true
describe "addFolder", ->