From 4677198ed6cdc200cc119c4a159e39116d130b46 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Tue, 28 Jul 2015 11:49:52 -0400 Subject: [PATCH] Add bramble.showUploadFilesDialog() for https://github.com/mozilla/thimble.webmaker.org/issues/723 --- README.md | 2 + src/bramble/client/main.js | 4 + .../BrambleUrlCodeHints/camera/index.js | 3 - .../default/BrambleUrlCodeHints/main.js | 43 +- .../default/BrambleUrlCodeHints/selfie.js | 97 +++++ .../default/UploadFiles/UploadFilesDialog.js | 162 +++++++ .../htmlContent/upload-files-dialog.html | 35 ++ .../UploadFiles/images/upload-cloud-green.svg | 12 + .../UploadFiles/images/upload-cloud.svg | 12 + src/extensions/default/UploadFiles/main.js | 22 + .../default/UploadFiles/styles.less | 113 +++++ .../bramble/lib/RemoteCommandHandler.js | 5 + src/styles/brackets_core_ui_variables.less | 7 +- src/utils/BrambleExtensionLoader.js | 3 +- src/utils/DragAndDrop.js | 408 ++++++++++-------- 15 files changed, 697 insertions(+), 231 deletions(-) create mode 100644 src/extensions/default/BrambleUrlCodeHints/selfie.js create mode 100644 src/extensions/default/UploadFiles/UploadFilesDialog.js create mode 100644 src/extensions/default/UploadFiles/htmlContent/upload-files-dialog.html create mode 100644 src/extensions/default/UploadFiles/images/upload-cloud-green.svg create mode 100644 src/extensions/default/UploadFiles/images/upload-cloud.svg create mode 100644 src/extensions/default/UploadFiles/main.js create mode 100644 src/extensions/default/UploadFiles/styles.less diff --git a/README.md b/README.md index a9ebd47a435..ae6b1908800 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ A standard set of default extensions are always turned on: * brackets-paste-and-indent * BrambleUrlCodeHints * Autosave +* UploadFiles You could disable QuickView and CSSCodeHints by loading Bramble with `?disableExtensions=QuickView,CSSCodeHints` on the URL. @@ -302,6 +303,7 @@ to be notified when the action completes: * `disableWordWrap([callback])` - turns off word wrap for the editor * `showTutorial([callback])` - shows tutorial (i.e., tutorial.html) vs editor contents in preview * `hideTutorial([callback])` - stops showing tutorial (i.e., tutorial.html) and uses editor contents in preview +* `showUploadFilesDialog([callback])` - shows the Upload Files dialog, allowing users to drag-and-drop, upload a file, or take a selfie. ## Bramble Instance Events diff --git a/src/bramble/client/main.js b/src/bramble/client/main.js index b23db01ae46..b5a3a93a94a 100644 --- a/src/bramble/client/main.js +++ b/src/bramble/client/main.js @@ -673,5 +673,9 @@ define([ this._executeRemoteCommand({commandCategory: "bramble", command: "BRAMBLE_HIDE_TUTORIAL"}, callback); }; + BrambleProxy.prototype.showUploadFilesDialog = function(callback) { + this._executeRemoteCommand({commandCategory: "bramble", command: "SHOW_UPLOAD_FILES_DIALOG"}, callback); + }; + return Bramble; }); diff --git a/src/extensions/default/BrambleUrlCodeHints/camera/index.js b/src/extensions/default/BrambleUrlCodeHints/camera/index.js index 2a1628f7176..410ca0afd59 100644 --- a/src/extensions/default/BrambleUrlCodeHints/camera/index.js +++ b/src/extensions/default/BrambleUrlCodeHints/camera/index.js @@ -31,9 +31,6 @@ define(function (require, exports, module) { // do not support this functionality of HTML5 Camera.isSupported = !!getUserMedia; - // TODO: l10n - Camera.selfieLabel = "Take a Selfie..."; - // Initiate the camera by requesting access to the user's webcam Camera.prototype.start = function() { var self = this; diff --git a/src/extensions/default/BrambleUrlCodeHints/main.js b/src/extensions/default/BrambleUrlCodeHints/main.js index 1c94ba9f475..e1363e3a787 100644 --- a/src/extensions/default/BrambleUrlCodeHints/main.js +++ b/src/extensions/default/BrambleUrlCodeHints/main.js @@ -37,11 +37,10 @@ define(function (require, exports, module) { ProjectManager = brackets.getModule("project/ProjectManager"), ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), EditorManager = brackets.getModule("editor/EditorManager"), - StartupState = brackets.getModule("bramble/StartupState"), Path = brackets.getModule("filesystem/impls/filer/FilerUtils").Path, Content = brackets.getModule("filesystem/impls/filer/lib/content"), Camera = require("camera/index"), - CameraDialog = require("camera-dialog"), + Selfie = require("selfie"), Data = require("text!data.json"), @@ -221,27 +220,11 @@ define(function (require, exports, module) { } }); - var highestNumber = 0; - result.forEach(function (item){ - item = item.split("/"); - item = item[item.length-1]; - if(item.indexOf("selfie") === 0) { - // Removes extension from filename - var fileNameParts = /selfie(\d*)\.png/.exec(item); - - var currentNumber = fileNameParts && fileNameParts[1] ? Number(fileNameParts[1]) : 0; - if(currentNumber > highestNumber) { - highestNumber = currentNumber; - } - } - }); - result.sort(); // Possibly adding the "Take Selfie" label to the bottom of results if(isImage && Camera.isSupported) { - result.push(Camera.selfieLabel); - this.selfieFileName = "selfie" + (highestNumber + 1) + ".png"; + result.push(Selfie.label); } return result; @@ -572,9 +555,6 @@ define(function (require, exports, module) { */ BrambleUrlCodeHints.prototype.insertHint = function (completion) { var that = this; - var cameraDialog; - var projectRoot; - var savePath; function insert(text) { var mode = that.editor.getModeForSelection(); @@ -591,18 +571,10 @@ define(function (require, exports, module) { return false; } - if (completion === Camera.selfieLabel) { - // NOTE: we need to deal with Brackets expecting a trailing / on dir names. - projectRoot = StartupState.project("root").replace(/\/?$/, "/"); - savePath = Path.join(projectRoot, this.selfieFileName); - - cameraDialog = new CameraDialog(savePath); - cameraDialog.show() - .done(function(selfieFilePath){ + if (completion === Selfie.label) { + Selfie.takeSelfie() + .done(function(selfieFilePath) { if(selfieFilePath) { - // Give back a path relative to the project's mount root. - selfieFilePath = FileUtils.getRelativeFilename(projectRoot, - selfieFilePath); insert(selfieFilePath); } EditorManager.getActiveEditor().focus(); @@ -610,9 +582,6 @@ define(function (require, exports, module) { .fail(function(err) { EditorManager.getActiveEditor().focus(); console.error("[Selfie error] ", err); - }) - .always(function() { - cameraDialog.close(); }); return false; } @@ -900,4 +869,6 @@ define(function (require, exports, module) { FileSystem.on("change", _clearCachedHints); FileSystem.on("rename", _clearCachedHints); }); + + Selfie.addSelfieCommand(); }); diff --git a/src/extensions/default/BrambleUrlCodeHints/selfie.js b/src/extensions/default/BrambleUrlCodeHints/selfie.js new file mode 100644 index 00000000000..b1d292229a7 --- /dev/null +++ b/src/extensions/default/BrambleUrlCodeHints/selfie.js @@ -0,0 +1,97 @@ +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define, brackets, $ */ + +define(function (require, exports, module) { + "use strict"; + + var CommandManager = brackets.getModule("command/CommandManager"); + var FileUtils = brackets.getModule("file/FileUtils"); + var StartupState = brackets.getModule("bramble/StartupState"); + var Filer = brackets.getModule("filesystem/impls/filer/BracketsFiler"); + var Path = Filer.Path; + var fs = Filer.fs(); + + var CameraDialog = require("camera-dialog"); + + // TODO: l10n + var SELFIE_MENU_LABEL = "Take a Selfie..."; + var SELFIE_FILENAME = "selfie"; + var CMD_SELFIE_TEXT = "Take a Selfie"; + var CMD_SELFIE_ID = "bramble.selfie"; + + /** + * Generate a unique filename for this selfie, taking into account + * that there might be other selfies already in the dir. Use an + * auto-increment on the filename (i.e., selfie1.png, selfie2.png) + */ + function _generateFilename(callback) { + var root = StartupState.project("root"); + fs.readdir(root, function(err, entries) { + if(err) { + return callback(err); + } + + var highest = 0; + entries.forEach(function(entry){ + var filenameParts = /selfie(\d*)\.png/.exec(entry); + var current; + + if(filenameParts) { + current = Number(filenameParts[1]); + if(current > highest) { + highest = current; + } + } + }); + + var filename = SELFIE_FILENAME + (highest + 1) + ".png"; + callback(null, filename); + }); + } + + function takeSelfie() { + var result = new $.Deferred(); + + function showCamera(filename) { + // NOTE: we need to deal with Brackets expecting a trailing / on dir names. + var projectRoot = StartupState.project("root").replace(/\/?$/, "/"); + var savePath = Path.join(projectRoot, filename); + var cameraDialog = new CameraDialog(savePath); + + cameraDialog.show() + .done(function(selfieFilePath){ + if(selfieFilePath) { + // Give back a path relative to the project's mount root. + selfieFilePath = FileUtils.getRelativeFilename(projectRoot, + selfieFilePath); + } + result.resolve(selfieFilePath); + }) + .fail(function(err) { + result.reject(err); + }) + .always(function() { + cameraDialog.close(); + cameraDialog = null; + }); + } + + _generateFilename(function(err, filename) { + if(err) { + console.error("[Selfie error] ", err); + return result.reject(); + } + showCamera(filename); + }); + + return result.promise(); + } + + function addSelfieCommand() { + CommandManager.register(CMD_SELFIE_TEXT, CMD_SELFIE_ID, takeSelfie); + } + + exports.addSelfieCommand = addSelfieCommand; + exports.takeSelfie = takeSelfie; + exports.label = SELFIE_MENU_LABEL; +}); diff --git a/src/extensions/default/UploadFiles/UploadFilesDialog.js b/src/extensions/default/UploadFiles/UploadFilesDialog.js new file mode 100644 index 00000000000..4a7e24c8de5 --- /dev/null +++ b/src/extensions/default/UploadFiles/UploadFilesDialog.js @@ -0,0 +1,162 @@ +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define, brackets, $ */ + +define(function (require, exports, module) { + "use strict"; + + var StartupState = brackets.getModule("bramble/StartupState"); + var Path = brackets.getModule("filesystem/impls/filer/BracketsFiler").Path; + var CommandManager = brackets.getModule("command/CommandManager"); + var CMD_OPEN = brackets.getModule("command/Commands").CMD_OPEN; + var Dialogs = brackets.getModule("widgets/Dialogs"); + var DragAndDrop = brackets.getModule("utils/DragAndDrop"); + var KeyEvent = brackets.getModule("utils/KeyEvent"); + + var dialogHTML = require("text!htmlContent/upload-files-dialog.html"); + + function FileInput() { + $(document.body) + .append($('')); + } + FileInput.prototype.getFiles = function() { + return this.getElem$()[0].files; + }; + FileInput.prototype.getElem$ = function() { + return $(".upload-files-input-elem"); + }; + FileInput.prototype.remove = function() { + this.getElem$().remove(); + }; + + + function FileUploadDialog() { + this.fileInput = new FileInput(); + this.deferred = new $.Deferred(); + } + FileUploadDialog.prototype.show = function() { + var self = this; + var deferred = self.deferred; + + // We ignore the promise returned by showModalDialogUsingTemplate, since we're managing the + // lifecycle of the dialog ourselves. + Dialogs.showModalDialogUsingTemplate(Mustache.render(dialogHTML), false); + + var $dlg = $(".upload-files-dialog.instance"); + var $dragFilesAreaDiv = $dlg.find(".drag-files-area"); + var $uploadFilesDiv = $dlg.find(".uploading-files"); + var $fromComputerButton = $dlg.find(".dialog-button[data-button-id='from-computer']"); + var $takeSelfieButton = $dlg.find(".dialog-button[data-button-id='take-selfie']"); + var $cancelButton = $dlg.find(".dialog-button[data-button-id='cancel']"); + var $dropZoneDiv = $dlg.find(".drop-zone"); + + // Hide the uploadingFiles div until a drop event + $uploadFilesDiv.hide(); + + $fromComputerButton.one("click", self._handleFromComputer.bind(self)); + $takeSelfieButton.one("click", self._handleTakeSelfie.bind(self)); + $cancelButton.one("click", function() { + self.hide(); + self.destroy(); + }); + $(window.document.body).on("keyup.installDialog", self._handleKeyUp.bind(self)); + + // Hook up drag-and-drop handling + DragAndDrop.attachHandlers({ + elem: $dropZoneDiv[0], + ondragover: function() { + if(self._dragover) { + return; + } + self._dragover = true; + $dragFilesAreaDiv.addClass("drag-over"); + }, + ondragleave: function() { + delete self._dragover; + $dragFilesAreaDiv.removeClass("drag-over"); + }, + ondrop: function() { + // Turn off the other buttons + $fromComputerButton.off("click", self._handleFromComputer.bind(self)); + $takeSelfieButton.off("click", self._handleTakeSelfie.bind(self)); + + // Switch to the upload spinner + $dragFilesAreaDiv.hide(); + $uploadFilesDiv.show(); + }, + onfilesdone: function() { + self.hide(); + self.destroy(); + }, + autoRemoveHandlers: true + }); + + return deferred.promise(); + }; + FileUploadDialog.prototype._handleKeyUp = function(e) { + var self = this; + + // Dismiss dialog on ESC + if (e.keyCode === KeyEvent.DOM_VK_ESCAPE) { + self.hide(); + self.destroy(); + self.deferred.resolve(); + } + }; + FileUploadDialog.prototype._handleTakeSelfie = function() { + var self = this; + var deferred = self.deferred; + + self.hide(); + self.destroy(); + + // Take a selfie, then show the image in the editor. + CommandManager.execute("bramble.selfie") + .done(function(filename) { + // Get the absolute path to the new file and open + filename = Path.join(StartupState.project("root"), filename); + CommandManager.execute(CMD_OPEN, { fullPath: filename }) + .then(deferred.resolve, deferred.reject); + }) + .fail(deferred.reject); + }; + FileUploadDialog.prototype._handleFromComputer = function() { + var self = this; + var deferred = self.deferred; + + self.hide(); + + function _processFiles(e) { + var files = self.fileInput.getFiles(); + DragAndDrop.processFiles(files, function(err) { + self.destroy(); + + if(err) { + deferred.reject(); + } else { + deferred.resolve(); + } + }); + } + + // Trigger the added previously to show and process files. + var input = self.fileInput.getElem$(); + input.on("change", _processFiles); + input.click(); + }; + FileUploadDialog.prototype.hide = function() { + var self = this; + $(window.document.body).off("keyup.installDialog", self._handleKeyUp); + Dialogs.cancelModalDialogIfOpen("upload-files-dialog"); + }; + FileUploadDialog.prototype.destroy = function() { + this.fileInput.remove(); + }; + + + function show() { + var uploadDialog = new FileUploadDialog(); + return uploadDialog.show(); + } + + exports.show = show; +}); diff --git a/src/extensions/default/UploadFiles/htmlContent/upload-files-dialog.html b/src/extensions/default/UploadFiles/htmlContent/upload-files-dialog.html new file mode 100644 index 00000000000..e59d1e0df39 --- /dev/null +++ b/src/extensions/default/UploadFiles/htmlContent/upload-files-dialog.html @@ -0,0 +1,35 @@ + + diff --git a/src/extensions/default/UploadFiles/images/upload-cloud-green.svg b/src/extensions/default/UploadFiles/images/upload-cloud-green.svg new file mode 100644 index 00000000000..4dd8f0793af --- /dev/null +++ b/src/extensions/default/UploadFiles/images/upload-cloud-green.svg @@ -0,0 +1,12 @@ + + + + upload-cloud-green + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/extensions/default/UploadFiles/images/upload-cloud.svg b/src/extensions/default/UploadFiles/images/upload-cloud.svg new file mode 100644 index 00000000000..aea81cf52f5 --- /dev/null +++ b/src/extensions/default/UploadFiles/images/upload-cloud.svg @@ -0,0 +1,12 @@ + + + + upload-cloud + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/extensions/default/UploadFiles/main.js b/src/extensions/default/UploadFiles/main.js new file mode 100644 index 00000000000..b2f6a1f30ca --- /dev/null +++ b/src/extensions/default/UploadFiles/main.js @@ -0,0 +1,22 @@ +define(function (require, exports, module) { + "use strict"; + + var CommandManager = brackets.getModule("command/CommandManager"); + var ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); + + var UploadFilesDialog = require("UploadFilesDialog"); + + var CMD_UPLOAD_FILES_TEXT = "Show Upload Files Dialog"; + var CMD_UPLOAD_FILES_ID = "bramble.showUploadFiles"; + + function showUploadFiles() { + return UploadFilesDialog.show(); + } + + function addCommand() { + CommandManager.register(CMD_UPLOAD_FILES_TEXT, CMD_UPLOAD_FILES_ID, showUploadFiles); + } + + ExtensionUtils.loadStyleSheet(module, "styles.less"); + addCommand(); +}); diff --git a/src/extensions/default/UploadFiles/styles.less b/src/extensions/default/UploadFiles/styles.less new file mode 100644 index 00000000000..ab58a72222a --- /dev/null +++ b/src/extensions/default/UploadFiles/styles.less @@ -0,0 +1,113 @@ +@extpath: "extensions/default/UploadFiles/images"; + +.animation (@name, @duration: 300ms, @ease: ease, @count: 1, @delay: 0) { + -webkit-animation: @name @duration @ease @count @delay ; + -moz-animation: @name @duration @ease @count @delay ; + -ms-animation: @name @duration @ease @count @delay ; + animation: @name @duration @ease @count @delay ; +} + +.drag-files-area { + height: 290px; + border: dashed 2px #AEAEAE; + color: #969696; + position: relative; + + .drop-zone { + position: absolute; + height: 100%; + width: 100%; + } + + .drop-instructions { + display: none; + } + + &.drag-over { + border: dashed 2px #5EBF7C; + background-color: rgba(94,191,124,.1); + + .drop-instructions { + display: block; + color: #26723D; + background-image: ~"url(@{extpath}/upload-cloud-green.svg)"; + } + + .drag-instructions { + display: none; + } + } + + .drag-instructions, .drop-instructions { + position: absolute; + padding-top: 60px; + width: 200px; + top: 100px; + text-align: center; + width: 100%; + background-image: ~"url(@{extpath}/upload-cloud.svg)"; + background-repeat: no-repeat; + background-position: center 0; + background-size: 70px; + } +} + +.uploading-files { + height: 290px; + + .upload-indicator { + text-align: center; + position: relative; + top: 95px; + + .title { + color: #BCBCBC; + font-size: 16px; + margin: 0 0 10px 0; + } + } + + .upload-spinner { + height:75px; + width: 75px; + position: relative; + background-image: ~"url(@{extpath}/upload-cloud.svg)"; + background-position: center; + background-repeat: no-repeat; + overflow: hidden; + margin-bottom: 15px; + display: block; + margin: 0 auto 15px auto; + + .circle { + position: absolute; + box-sizing: border-box; + border-radius: 50%; + border: solid 2px rgba(0,0,0,.1); + width: 100%; + height: 100%; + } + + .mask { + height: 54%; + left: 30%; + width: 40%; + top: -5%; + border-top: solid 10px rgb(223, 226, 226); /* match Brackets dialog background */ + position: absolute; + box-sizing: border-box; + transform-origin: bottom; + animation: uploadSpinner 2s linear infinite; + } + + } +} + +@keyframes uploadSpinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/extensions/default/bramble/lib/RemoteCommandHandler.js b/src/extensions/default/bramble/lib/RemoteCommandHandler.js index 71f6d7e85f1..4a3eb108e7c 100644 --- a/src/extensions/default/bramble/lib/RemoteCommandHandler.js +++ b/src/extensions/default/bramble/lib/RemoteCommandHandler.js @@ -109,6 +109,11 @@ define(function (require, exports, module) { case "BRAMBLE_HIDE_TUTORIAL": Tutorial.setOverride(false); break; + case "SHOW_UPLOAD_FILES_DIALOG": + // Show dialog, see extensions/default/UploadFiles + skipCallback = true; + CommandManager.execute("bramble.showUploadFiles").always(callback); + break; case "RESIZE": // The host window was resized, update all panes WorkspaceManager.recomputeLayout(true); diff --git a/src/styles/brackets_core_ui_variables.less b/src/styles/brackets_core_ui_variables.less index 973ec3ab36d..482bc3069f4 100644 --- a/src/styles/brackets_core_ui_variables.less +++ b/src/styles/brackets_core_ui_variables.less @@ -104,9 +104,10 @@ @bc-input-bg: #fff; // Primary Button -@bc-primary-btn-bg: #288edf; -@bc-primary-btn-bg-down: #0380e8; -@bc-primary-btn-border: #1474bf; +/* XXXBramble, green vs. blue */ +@bc-primary-btn-bg: #47ad66; +@bc-primary-btn-bg-down: #316B41; +@bc-primary-btn-border: #316B41; // Secondary Button @bc-secondary-btn-bg: #91cc41; diff --git a/src/utils/BrambleExtensionLoader.js b/src/utils/BrambleExtensionLoader.js index ff54c6c32f1..2c7a03715a4 100644 --- a/src/utils/BrambleExtensionLoader.js +++ b/src/utils/BrambleExtensionLoader.js @@ -30,7 +30,8 @@ define(function (require, exports, module) { "bramble", "Autosave", "brackets-paste-and-indent", - "BrambleUrlCodeHints" + "BrambleUrlCodeHints", + "UploadFiles" ]; /** diff --git a/src/utils/DragAndDrop.js b/src/utils/DragAndDrop.js index af106b9afb2..62ca3aeeb6f 100644 --- a/src/utils/DragAndDrop.js +++ b/src/utils/DragAndDrop.js @@ -159,17 +159,44 @@ define(function (require, exports, module) { * Attaches global drag & drop handlers to this window. This enables dropping files/folders to open them, and also * protects the Brackets app from being replaced by the browser trying to load the dropped file in its place. */ - function attachHandlers() { + function attachHandlers(options) { + // XXXBramble: we want to reuse this code for the UploadFiles extension + // so we add support for passing exra options here. + options = options || {}; + options.elem = options.elem || window.document.body; + // Support optional events hooks + var noop = function(){}; + options.ondragover = options.ondragover || noop; + options.ondragleave = options.ondragleave || noop; + options.ondrop = options.ondrop || noop; + options.onfilesdone = options.onfilesdone || noop; + + // XXXBramble: extra dragleave event for UI updates in UploadFiles + function handleDragLeave(event) { + event = event.originalEvent || event; + event.stopPropagation(); + event.preventDefault(); + + options.ondragleave(event); + } function handleDragOver(event) { event = event.originalEvent || event; event.stopPropagation(); event.preventDefault(); + options.ondragover(event); + var dropEffect = "none"; - // Don't allow drag-and-drop of files/folders when a modal dialog is showing. - if ($(".modal.instance").length === 0 && isValidDrop(event.dataTransfer.types)) { - dropEffect = "copy"; + // XXXBramble: we want to reuse this in the UploadFiles modal, so treat body differently + if(isValidDrop(event.dataTransfer.types)) { + if(options.elem === window.document.body) { + if($(".modal.instance").length === 0) { + dropEffect = "copy"; + } + } else { + dropEffect = "copy"; + } } event.dataTransfer.dropEffect = dropEffect; } @@ -179,224 +206,229 @@ define(function (require, exports, module) { event.stopPropagation(); event.preventDefault(); - var pathList = []; - var errorList = []; + options.ondrop(event); - function shouldOpenFile(filename, encoding) { - var ext = Path.extname(filename).replace(/^\./, ""); - var language = LanguageManager.getLanguageForExtension(ext); - var id = language && language.getId(); - var isImage = id === "image" || id === "svg"; - - return isImage || encoding === "utf8"; + var files = event.dataTransfer.files; + processFiles(files, function() { + options.onfilesdone(); + + if(options.autoRemoveHandlers) { + var elem = options.elem; + $(elem) + .off("dragover", handleDragOver) + .off("dragleave", handleDragLeave) + .off("drop", handleDrop); + + elem.removeEventListener("dragover", codeMirrorDragOverHandler, true); + elem.removeEventListener("dragleave", codeMirrorDragLeaveHandler, true); + elem.removeEventListener("drop", codeMirrorDropHandler, true); + } + }); + } + + // For most of the window, only respond if nothing more specific in the UI has already grabbed the event (e.g. + // the Extension Manager drop-to-install zone, or an extension with a drop-to-upload zone in its panel) + $(options.elem) + .on("dragover", handleDragOver) + .on("dragleave", handleDragLeave) + .on("drop", handleDrop); + + // Over CodeMirror specifically, always pre-empt CodeMirror's drag event handling if files are being dragged - CM stops + // propagation on any drag event it sees, even when it's not a text drag/drop. But allow CM to handle all non-file drag + // events. See bug #10617. + var codeMirrorDragOverHandler = function (event) { + if ($(event.target).closest(".CodeMirror").length) { + handleDragOver(event); + } + }; + var codeMirrorDropHandler = function (event) { + if ($(event.target).closest(".CodeMirror").length) { + handleDrop(event); + } + }; + var codeMirrorDragLeaveHandler = function (event) { + if ($(event.target).closest(".CodeMirror").length) { + handleDragLeave(event); } + }; + options.elem.addEventListener("dragover", codeMirrorDragOverHandler, true); + options.elem.addEventListener("dragleave", codeMirrorDragLeaveHandler, true); + options.elem.addEventListener("drop", codeMirrorDropHandler, true); + } + + // XXXBramble: given a list of dropped files, write them into the fs, unzipping zip files. + function processFiles(files, callback) { + var pathList = []; + var errorList = []; - function handleRegularFile(deferred, file, filename, buffer, encoding) { - file.write(buffer, {encoding: encoding}, function(err) { - if (err) { - deferred.reject(err); - return; - } + if (!(files && files.length)) { + return callback(); + } - // See if this file is worth trying to open in the editor or not - if(shouldOpenFile(filename, encoding)) { - pathList.push(filename); - } + function shouldOpenFile(filename, encoding) { + var ext = Path.extname(filename).replace(/^\./, ""); + var language = LanguageManager.getLanguageForExtension(ext); + var id = language && language.getId(); + var isImage = id === "image" || id === "svg"; - deferred.resolve(); - }); - } + return isImage || encoding === "utf8"; + } - function handleZipFile(deferred, file, filename, buffer, encoding) { - var basename = Path.basename(filename); - var message = "

Do you want to extract the contents of the zip file: " + - basename + "?

" + - "

NOTE: This can take some time.

"; + function handleRegularFile(deferred, file, filename, buffer, encoding) { + file.write(buffer, {encoding: encoding}, function(err) { + if (err) { + deferred.reject(err); + return; + } - // TODO: l10n, UX audit - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_INFO, - "Unzip file", - message, - [ - { - className : Dialogs.DIALOG_BTN_CLASS_NORMAL, - id : Dialogs.DIALOG_BTN_CANCEL, - text : "No" - }, - { - className : Dialogs.DIALOG_BTN_CLASS_PRIMARY, - id : Dialogs.DIALOG_BTN_OK, - text : "Yes" - } - ] - ) - .done(function(id) { - if (id === Dialogs.DIALOG_BTN_OK) { - // TODO: we need to give a spinner or something to indicate we're working - unzip(buffer, function(err) { - if (err) { - deferred.reject(err); - return; - } - - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_INFO, - "Unzip Completed Successfully", - "Successfully unzipped " + basename + "." - ).then(deferred.resolve, deferred.reject); - }); - } else { - handleRegularFile(deferred, file, filename, buffer, encoding); - } - }); - } + // See if this file is worth trying to open in the editor or not + if(shouldOpenFile(filename, encoding)) { + pathList.push(filename); + } - function prepareDropPaths(fileList) { - // Convert FileList object to an Array with all image files first, then CSS - // followed by HTML files at the end, since we need to write any .css, .js, etc. - // resources first such that Blob URLs can be generated for these resources - // prior to rewriting an HTML file. - function rateFileByType(filename) { - var ext = Path.extname(filename); - - // We want to end up with: [images, ..., js, ..., css, html] - // since CSS can include images, and HTML can include CSS or JS. - // We also treat .md like an HTML file, since we render them. - if(Content.isHTML(ext) || Content.isMarkdown(ext)) { - return 10; - } else if(Content.isCSS(ext)) { - return 8; - } else if(Content.isImage(ext)) { - return 1; - } - return 3; + deferred.resolve(); + }); + } + + function handleZipFile(deferred, file, filename, buffer, encoding) { + var basename = Path.basename(filename); + + unzip(buffer, function(err) { + if (err) { + deferred.reject(err); + return; } - return _.toArray(fileList).sort(function(a,b) { - a = rateFileByType(a.name); - b = rateFileByType(b.name); + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_INFO, + "Unzip Completed Successfully", + "Successfully unzipped " + basename + "." + ).getPromise().then(deferred.resolve, deferred.reject); + }); + } - if(a < b) { - return -1; - } - if(a > b) { - return 1; - } - return 0; - }); + /** + * Determine whether we want to import this file at all. If it's too large + * or not a mime type we care about, reject it. + */ + function rejectImport(item) { + if (item.size > byteLimit) { + return new Error("file exceeds maximum supported size"); } - /** - * Determine whether we want to import this file at all. If it's too large - * or not a mime type we care about, reject it. - */ - function rejectImport(item) { - if (item.size > byteLimit) { - return new Error("file exceeds maximum supported size"); - } + // If we don't know about this language type, or the OS doesn't think + // it's text, reject it. + var ext = Path.extname(item.name).replace(/^\./, ""); + var languageIsSupported = !!LanguageManager.getLanguageForExtension(ext); + var typeIsText = Content.isTextType(item.type); - // If we don't know about this language type, or the OS doesn't think - // it's text, reject it. - var ext = Path.extname(item.name).replace(/^\./, ""); - var languageIsSupported = !!LanguageManager.getLanguageForExtension(ext); - var typeIsText = Content.isTextType(item.type); + if (languageIsSupported || typeIsText) { + return null; + } + return new Error("unsupported file type"); + } - if (languageIsSupported || typeIsText) { - return null; + function prepareDropPaths(fileList) { + // Convert FileList object to an Array with all image files first, then CSS + // followed by HTML files at the end, since we need to write any .css, .js, etc. + // resources first such that Blob URLs can be generated for these resources + // prior to rewriting an HTML file. + function rateFileByType(filename) { + var ext = Path.extname(filename); + + // We want to end up with: [images, ..., js, ..., css, html] + // since CSS can include images, and HTML can include CSS or JS. + // We also treat .md like an HTML file, since we render them. + if(Content.isHTML(ext) || Content.isMarkdown(ext)) { + return 10; + } else if(Content.isCSS(ext)) { + return 8; + } else if(Content.isImage(ext)) { + return 1; } - return new Error("unsupported file type"); + return 3; } - function maybeImportFile(item) { - var deferred = new $.Deferred(); - var reader = new FileReader(); - - // Check whether we want to import this file at all before we start. - var wasRejected = rejectImport(item); - if (wasRejected) { - setTimeout(function(){ - errorList.push({path: item.name, error: wasRejected.message}); - deferred.reject(wasRejected); - }, 5); - return deferred.promise(); - } + return _.toArray(fileList).sort(function(a,b) { + a = rateFileByType(a.name); + b = rateFileByType(b.name); - reader.onload = function(e) { - delete reader.onload; + if(a < b) { + return -1; + } + if(a > b) { + return 1; + } + return 0; + }); + } - var filename = Path.join(StartupState.project("root"), item.name); - var file = FileSystem.getFileForPath(filename); + function maybeImportFile(item) { + var deferred = new $.Deferred(); + var reader = new FileReader(); + + // Check whether we want to import this file at all before we start. + var wasRejected = rejectImport(item); + if (wasRejected) { + setTimeout(function(){ + errorList.push({path: item.name, error: wasRejected.message}); + deferred.reject(wasRejected); + }, 5); + return deferred.promise(); + } - // Create a Filer Buffer, and determine the proper encoding. We - // use the extension, and also the OS provided mime type for clues. - var buffer = new Filer.Buffer(e.target.result); - var utf8FromExt = Content.isUTF8Encoded(Path.extname(filename)); - var utf8FromOS = Content.isTextType(item.type); - var encoding = utf8FromExt || utf8FromOS ? 'utf8' : null; - if(encoding === 'utf8') { - buffer = buffer.toString(); - } + reader.onload = function(e) { + delete reader.onload; - // Special-case .zip files, so we can offer to extract the contents - if(Path.extname(filename) === ".zip") { - handleZipFile(deferred, file, filename, buffer, encoding); - } else { - handleRegularFile(deferred, file, filename, buffer, encoding); - } - }; + var filename = Path.join(StartupState.project("root"), item.name); + var file = FileSystem.getFileForPath(filename); - // Deal with error cases, for example, trying to drop a folder vs. file - reader.onerror = function(e) { - delete reader.onerror; + // Create a Filer Buffer, and determine the proper encoding. We + // use the extension, and also the OS provided mime type for clues. + var buffer = new Filer.Buffer(e.target.result); + var utf8FromExt = Content.isUTF8Encoded(Path.extname(filename)); + var utf8FromOS = Content.isTextType(item.type); + var encoding = utf8FromExt || utf8FromOS ? 'utf8' : null; + if(encoding === 'utf8') { + buffer = buffer.toString(); + } - errorList.push({path: item.name, error: e.target.error.message}); - deferred.reject(e.target.error); - }; - reader.readAsArrayBuffer(item); + // Special-case .zip files, so we can offer to extract the contents + if(Path.extname(filename) === ".zip") { + handleZipFile(deferred, file, filename, buffer, encoding); + } else { + handleRegularFile(deferred, file, filename, buffer, encoding); + } + }; - return deferred.promise(); - } + // Deal with error cases, for example, trying to drop a folder vs. file + reader.onerror = function(e) { + delete reader.onerror; - var files = event.dataTransfer.files; + errorList.push({path: item.name, error: e.target.error.message}); + deferred.reject(e.target.error); + }; + reader.readAsArrayBuffer(item); - if (files && files.length) { - Async.doSequentially(prepareDropPaths(files), maybeImportFile, false) - .done(function() { - openDroppedFiles(pathList); - }) - .fail(function() { - _showErrorDialog(errorList); - }); - } + return deferred.promise(); } - - // For most of the window, only respond if nothing more specific in the UI has already grabbed the event (e.g. - // the Extension Manager drop-to-install zone, or an extension with a drop-to-upload zone in its panel) - $(window.document.body) - .on("dragover", handleDragOver) - .on("drop", handleDrop); - - // Over CodeMirror specifically, always pre-empt CodeMirror's drag event handling if files are being dragged - CM stops - // propagation on any drag event it sees, even when it's not a text drag/drop. But allow CM to handle all non-file drag - // events. See bug #10617. - window.document.body.addEventListener("dragover", function (event) { - if ($(event.target).closest(".CodeMirror").length) { - handleDragOver(event); - } - }, true); - window.document.body.addEventListener("drop", function (event) { - if ($(event.target).closest(".CodeMirror").length) { - handleDrop(event); - } - }, true); + + Async.doSequentially(prepareDropPaths(files), maybeImportFile, false) + .done(function() { + openDroppedFiles(pathList); + callback(null, pathList); + }) + .fail(function() { + _showErrorDialog(errorList); + callback(errorList); + }); } - CommandManager.register(Strings.CMD_OPEN_DROPPED_FILES, Commands.FILE_OPEN_DROPPED_FILES, openDroppedFiles); // Export public API exports.attachHandlers = attachHandlers; exports.isValidDrop = isValidDrop; exports.openDroppedFiles = openDroppedFiles; + exports.processFiles = processFiles; });