/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/extensibility/InstallExtensionDialog.js
491 строка
19 KB
ficristo
Move most inline jslint directives to config files
12 авг 2016, 10:22
12 авг 2016, 10:22
074382a
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*unittests: Install Extension Dialog*/ define(function (require, exports, module) { "use strict"; var Dialogs = require("widgets/Dialogs"), File = require("filesystem/File"), StringUtils = require("utils/StringUtils"), Strings = require("strings"), FileSystem = require("filesystem/FileSystem"), KeyEvent = require("utils/KeyEvent"), Package = require("extensibility/Package"), NativeApp = require("utils/NativeApp"), InstallDialogTemplate = require("text!htmlContent/install-extension-dialog.html"), Mustache = require("thirdparty/mustache/mustache"); var STATE_CLOSED = 0, STATE_START = 1, STATE_VALID_URL = 2, STATE_INSTALLING = 3, STATE_INSTALLED = 4, STATE_INSTALL_FAILED = 5, STATE_CANCELING_INSTALL = 6, STATE_CANCELING_HUNG = 7, STATE_INSTALL_CANCELED = 8, STATE_ALREADY_INSTALLED = 9, STATE_OVERWRITE_CONFIRMED = 10, STATE_NEEDS_UPDATE = 11; /** * Creates a new extension installer dialog. * @constructor * @param {{install: function(url), cancel: function()}} installer The installer backend to use. */ function InstallExtensionDialog(installer, _isUpdate) { this._installer = installer; this._state = STATE_CLOSED; this._installResult = null; this._isUpdate = _isUpdate; // Timeout before we allow user to leave STATE_INSTALL_CANCELING without waiting for a resolution // (per-instance so we can poke it for unit testing) this._cancelTimeout = 10 * 1000; } /** * The dialog root. * @type {jQuery} */ InstallExtensionDialog.prototype.$dlg = null; /** * The url input field. * @type {jQuery} */ InstallExtensionDialog.prototype.$url = null; /** * The ok button. * @type {jQuery} */ InstallExtensionDialog.prototype.$okButton = null; /** * The cancel button. * @type {jQuery} */ InstallExtensionDialog.prototype.$cancelButton = null; /** * The area containing the url input label and field. * @type {jQuery} */ InstallExtensionDialog.prototype.$inputArea = null; /** * The area containing the installation message and spinner. * @type {jQuery} */ InstallExtensionDialog.prototype.$msgArea = null; /** * The span containing the installation message. * @type {jQuery} */ InstallExtensionDialog.prototype.$msg = null; /** * The "Browse Extensions" button. * @type {jQuery} */ InstallExtensionDialog.prototype.$browseExtensionsButton = null; /** * A deferred that's resolved/rejected when the dialog is closed and * something has/hasn't been installed successfully. * @type {$.Deferred} */ InstallExtensionDialog.prototype._dialogDeferred = null; /** * installer The installer backend for this dialog. * @type {{install: function(url), cancel: function()}} */ InstallExtensionDialog.prototype._installer = null; /** * The current state of the dialog; one of the STATE_* constants above. * @type {number} */ InstallExtensionDialog.prototype._state = null; /** * @private * Transitions the dialog into a new state as the installation proceeds. * @param {number} newState The state to transition into; one of the STATE_* variables. */ InstallExtensionDialog.prototype._enterState = function (newState) { var url, msg, self = this, prevState = this._state; // Store the new state up front in case some of the processing below ends up changing // the state again immediately. this._state = newState; switch (newState) { case STATE_START: // This should match the default appearance of the dialog when it first opens. this.$msg.find(".spinner").remove(); this.$msgArea.hide(); this.$inputArea.show(); this.$okButton .prop("disabled", true) .text(Strings.INSTALL); break; case STATE_VALID_URL: this.$okButton.prop("disabled", false); break; case STATE_INSTALLING: url = this.$url.val().trim(); this.$inputArea.hide(); this.$browseExtensionsButton.hide(); this.$msg.text(StringUtils.format(Strings.INSTALLING_FROM, url)) .append("<span class='spinner inline spin'/>"); this.$msgArea.show(); this.$okButton.prop("disabled", true); this._installer.install(url) .done(function (result) { self._installResult = result; if (result.installationStatus === Package.InstallationStatuses.ALREADY_INSTALLED || result.installationStatus === Package.InstallationStatuses.OLDER_VERSION || result.installationStatus === Package.InstallationStatuses.SAME_VERSION) { self._enterState(STATE_ALREADY_INSTALLED); } else if (result.installationStatus === Package.InstallationStatuses.NEEDS_UPDATE) { self._enterState(STATE_NEEDS_UPDATE); } else { self._enterState(STATE_INSTALLED); } }) .fail(function (err) { // If the "failure" is actually a user-requested cancel, don't show an error UI if (err === "CANCELED") { console.assert(self._state === STATE_CANCELING_INSTALL || self._state === STATE_CANCELING_HUNG); self._enterState(STATE_INSTALL_CANCELED); } else { self._errorMessage = Package.formatError(err); self._enterState(STATE_INSTALL_FAILED); } }); break; case STATE_CANCELING_INSTALL: // This should call back the STATE_INSTALLING fail() handler above, unless it's too late to cancel // in which case we'll still jump to STATE_INSTALLED after this this.$cancelButton.prop("disabled", true); this.$msg.text(Strings.CANCELING_INSTALL); this._installer.cancel(); window.setTimeout(function () { if (self._state === STATE_CANCELING_INSTALL) { self._enterState(STATE_CANCELING_HUNG); } }, this._cancelTimeout); break; case STATE_CANCELING_HUNG: this.$msg.text(Strings.CANCELING_HUNG); this.$okButton .removeAttr("disabled") .text(Strings.CLOSE); break; case STATE_INSTALLED: case STATE_INSTALL_FAILED: case STATE_INSTALL_CANCELED: case STATE_NEEDS_UPDATE: if (newState === STATE_INSTALLED) { msg = Strings.INSTALL_SUCCEEDED; } else if (newState === STATE_INSTALL_FAILED) { msg = Strings.INSTALL_FAILED; } else if (newState === STATE_NEEDS_UPDATE) { msg = Strings.EXTENSION_UPDATE_INSTALLED; } else { msg = Strings.INSTALL_CANCELED; } this.$msg.html($("<strong/>").text(msg)); if (this._errorMessage) { this.$msg.append($("<p/>").text(this._errorMessage)); } this.$okButton .removeAttr("disabled") .text(Strings.CLOSE); this.$cancelButton.hide(); break; case STATE_ALREADY_INSTALLED: var installResult = this._installResult; var status = installResult.installationStatus; var msgText = Strings["EXTENSION_" + status]; if (status === Package.InstallationStatuses.OLDER_VERSION) { msgText = StringUtils.format(msgText, installResult.metadata.version, installResult.installedVersion); } this.$msg.text(msgText); this.$okButton .prop("disabled", false) .text(Strings.OVERWRITE); break; case STATE_OVERWRITE_CONFIRMED: this._enterState(STATE_CLOSED); break; case STATE_CLOSED: $(window.document.body).off(".installDialog"); // Only resolve as successful if we actually installed something. Dialogs.cancelModalDialogIfOpen("install-extension-dialog"); if (prevState === STATE_INSTALLED || prevState === STATE_NEEDS_UPDATE || prevState === STATE_OVERWRITE_CONFIRMED) { this._dialogDeferred.resolve(this._installResult); } else { this._dialogDeferred.reject(); } break; } }; /** * @private * Handle a click on the Cancel button, which either cancels an ongoing installation (leaving * the dialog open), or closes the dialog if no installation is in progress. */ InstallExtensionDialog.prototype._handleCancel = function () { if (this._state === STATE_INSTALLING) { this._enterState(STATE_CANCELING_INSTALL); } else if (this._state === STATE_ALREADY_INSTALLED) { // If we were prompting the user about overwriting a previous installation, // and the user cancels, we can delete the downloaded file. if (this._installResult && this._installResult.localPath && !this._installResult.keepFile) { var filename = this._installResult.localPath; FileSystem.getFileForPath(filename).unlink(); } this._enterState(STATE_CLOSED); } else if (this._state !== STATE_CANCELING_INSTALL) { this._enterState(STATE_CLOSED); } }; /** * @private * Handle a click on the default button, which is "Install" while we're waiting for the * user to enter a URL, and "Close" once we've successfully finished installation. */ InstallExtensionDialog.prototype._handleOk = function () { if (this._state === STATE_INSTALLED || this._state === STATE_INSTALL_FAILED || this._state === STATE_INSTALL_CANCELED || this._state === STATE_CANCELING_HUNG || this._state === STATE_NEEDS_UPDATE) { // In these end states, this is a "Close" button: just close the dialog and indicate // success. this._enterState(STATE_CLOSED); } else if (this._state === STATE_VALID_URL) { this._enterState(STATE_INSTALLING); } else if (this._state === STATE_ALREADY_INSTALLED) { this._enterState(STATE_OVERWRITE_CONFIRMED); } }; /** * @private * Handle key up events on the document. We use this to detect the Esc key. */ InstallExtensionDialog.prototype._handleKeyUp = function (e) { if (e.keyCode === KeyEvent.DOM_VK_ESCAPE) { this._handleCancel(); } }; /** * @private * Handle typing in the URL field. */ InstallExtensionDialog.prototype._handleUrlInput = function (e) { var url = this.$url.val().trim(), valid = (url !== ""); if (!valid && this._state === STATE_VALID_URL) { this._enterState(STATE_START); } else if (valid && this._state === STATE_START) { this._enterState(STATE_VALID_URL); } }; /** * @private * Closes the dialog if it's not already closed. For unit testing only. */ InstallExtensionDialog.prototype._close = function () { if (this._state !== STATE_CLOSED) { this._enterState(STATE_CLOSED); } }; /** * Initialize and show the dialog. * @param {string=} urlToInstall If specified, immediately starts installing the given file as if the user had * specified it. * @return {$.Promise} A promise object that will be resolved when the selected extension * has finished installing, or rejected if the dialog is cancelled. */ InstallExtensionDialog.prototype.show = function (urlToInstall) { if (this._state !== STATE_CLOSED) { // Somehow the dialog got invoked twice. Just ignore this. return this._dialogDeferred.promise(); } var context = { Strings: Strings, isUpdate: this._isUpdate, includeBrowseExtensions: !!brackets.config.extension_listing_url }; // We ignore the promise returned by showModalDialogUsingTemplate, since we're managing the // lifecycle of the dialog ourselves. Dialogs.showModalDialogUsingTemplate(Mustache.render(InstallDialogTemplate, context), false); this.$dlg = $(".install-extension-dialog.instance"); this.$url = this.$dlg.find(".url").focus(); this.$okButton = this.$dlg.find(".dialog-button[data-button-id='ok']"); this.$cancelButton = this.$dlg.find(".dialog-button[data-button-id='cancel']"); this.$inputArea = this.$dlg.find(".input-field"); this.$msgArea = this.$dlg.find(".message-field"); this.$msg = this.$msgArea.find(".message"); this.$browseExtensionsButton = this.$dlg.find(".browse-extensions"); this.$okButton.on("click", this._handleOk.bind(this)); this.$cancelButton.on("click", this._handleCancel.bind(this)); this.$url.on("input", this._handleUrlInput.bind(this)); this.$browseExtensionsButton.on("click", function () { NativeApp.openURLInDefaultBrowser(brackets.config.extension_listing_url); }); $(window.document.body).on("keyup.installDialog", this._handleKeyUp.bind(this)); this._enterState(STATE_START); if (urlToInstall) { // Act as if the user had manually entered the URL. this.$url.val(urlToInstall); this._enterState(STATE_VALID_URL); this._enterState(STATE_INSTALLING); } this._dialogDeferred = new $.Deferred(); return this._dialogDeferred.promise(); }; /** Mediates between this module and the Package extension-installation utils. Mockable for unit-testing. */ function InstallerFacade(isLocalFile) { this._isLocalFile = isLocalFile; } InstallerFacade.prototype.install = function (url) { if (this.pendingInstall) { console.error("Extension installation already pending"); return new $.Deferred().reject("DOWNLOAD_ID_IN_USE").promise(); } if (this._isLocalFile) { var deferred = new $.Deferred(); this.pendingInstall = { promise: deferred.promise(), cancel: function () { // Can't cancel local zip installs } }; Package.installFromPath(url).then(function (installationResult) { // Flag to keep zip files for local file installation installationResult.keepFile = true; deferred.resolve(installationResult); }, deferred.reject); } else { this.pendingInstall = Package.installFromURL(url); } // Store now since we'll null pendingInstall immediately if the promise was resolved synchronously var promise = this.pendingInstall.promise; var self = this; this.pendingInstall.promise.always(function () { self.pendingInstall = null; }); return promise; }; InstallerFacade.prototype.cancel = function () { this.pendingInstall.cancel(); }; /** * @private * Show a dialog that allows the user to enter the URL of an extension ZIP file to install. * @return {$.Promise} A promise object that will be resolved when the selected extension * has finished installing, or rejected if the dialog is cancelled. */ function showDialog() { var dlg = new InstallExtensionDialog(new InstallerFacade()); return dlg.show(); } /** * @private * Show the installation dialog and automatically begin installing the given URL. * @param {(string|File)=} urlOrFileToInstall If specified, immediately starts installing the given file as if the user had * specified it. * @return {$.Promise} A promise object that will be resolved when the selected extension * has finished installing, or rejected if the dialog is cancelled. */ function installUsingDialog(urlOrFileToInstall, _isUpdate) { var isLocalFile = (urlOrFileToInstall instanceof File), dlg = new InstallExtensionDialog(new InstallerFacade(isLocalFile), _isUpdate); return dlg.show(urlOrFileToInstall.fullPath || urlOrFileToInstall); } /** * @private * Show the update dialog and automatically begin downloading the update from the given URL. * @param {string} urlToUpdate URL to download * @return {$.Promise} A promise object that will be resolved when the selected extension * has finished downloading, or rejected if the dialog is cancelled. */ function updateUsingDialog(urlToUpdate) { return installUsingDialog(urlToUpdate, true); } exports.showDialog = showDialog; exports.installUsingDialog = installUsingDialog; exports.updateUsingDialog = updateUsingDialog; // Exposed for unit testing only exports._Dialog = InstallExtensionDialog; });