From 83f18492152ab7f3a342821f62e9fa674ce49188 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 18 Aug 2019 21:32:58 -0700 Subject: [PATCH] GUACAMOLE-630: Migrate guacInputColor to singleton instance of Pickr. --- .../app/form/directives/guacInputColor.js | 123 ++------ .../app/form/services/colorPickerService.js | 268 ++++++++++++++++++ .../app/form/templates/guacInputColor.html | 3 +- 3 files changed, 289 insertions(+), 105 deletions(-) create mode 100644 guacamole/src/main/webapp/app/form/services/colorPickerService.js diff --git a/guacamole/src/main/webapp/app/form/directives/guacInputColor.js b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js index 3b010cc7d..1762c3172 100644 --- a/guacamole/src/main/webapp/app/form/directives/guacInputColor.js +++ b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js @@ -18,11 +18,11 @@ */ /** - * A directive which implements a color input field, leveraging the "Pickr" - * color picker. If the "Picker" color picker cannot be used because it relies - * on JavaScript features not supported by the browser (Internet Explorer), a - * "guacInputColorUnavailable" event will be emitted up the scope, and this + * A directive which implements a color input field. If the underlying color + * picker implementation cannot be used due to a lack of browser support, this * directive will become read-only, functioning essentially as a color preview. + * + * @see colorPickerService */ angular.module('form').directive('guacInputColor', [function guacInputColor() { @@ -59,17 +59,12 @@ angular.module('form').directive('guacInputColor', [function guacInputColor() { function guacInputColorController($scope, $element, $injector) { // Required services - var $q = $injector.get('$q'); - var $translate = $injector.get('$translate'); + var colorPickerService = $injector.get('colorPickerService'); /** - * Whether the color picker ("Pickr") cannot be used. In general, all - * browsers should support Pickr with the exception of Internet - * Explorer. - * - * @type Boolean + * @borrows colorPickerService.isAvailable() */ - $scope.colorPickerUnavailable = false; + $scope.isColorPickerAvailable = colorPickerService.isAvailable; /** * Returns whether the color currently selected is "dark" in the sense @@ -102,98 +97,18 @@ angular.module('form').directive('guacInputColor', [function guacInputColor() { }; - // Init color picker after required translation strings are available - $q.all({ - 'save' : $translate('APP.ACTION_SAVE'), - 'cancel' : $translate('APP.ACTION_CANCEL') - }).then(function stringsRetrieved(strings) { - - try { - - /** - * An instance of the "Pickr" color picker, bound to the underlying - * element of this directive. - * - * @type Pickr - */ - var pickr = Pickr.create({ - - // Bind color picker to the underlying element of this directive - el : $element[0], - - // Wrap color picker dialog in Guacamole-specific class for - // sake of additional styling - appClass : 'guac-input-color-picker', - - // Display color details as hex - defaultRepresentation : 'HEX', - - // Use "monolith" theme, as a nice balance between "nano" (does - // not work in Internet Explorer) and "classic" (too big) - theme : 'monolith', - - // Leverage the container element as the button which shows the - // picker, relying on our own styling for that button - useAsButton : true, - appendToBody : true, - - // Do not include opacity controls - lockOpacity : true, - - // Include a selection of palette entries for convenience and - // reference - swatches : $scope.palette || [], - - components: { - - // Include hue and color preview controls - preview : true, - hue : true, - - // Display only a text color input field and the save and - // cancel buttons (no clear button) - interaction: { - input : true, - save : true, - cancel : true - } - - }, - - // Use translation strings for buttons - strings : strings - - }); - - // Hide color picker after user clicks "cancel" - pickr.on('cancel', function colorChangeCanceled() { - pickr.hide(); - }); - - // Keep model in sync with changes to the color picker - pickr.on('save', function colorChanged(color) { - $scope.$evalAsync(function updateModel() { - $scope.model = color.toHEXA().toString(); - }); - }); - - // Keep color picker in sync with changes to the model - pickr.on('init', function pickrReady(color) { - $scope.$watch('model', function modelChanged(model) { - pickr.setColor(model); - }); - }); - - } - - // If the "Pickr" color picker cannot be loaded (Internet Explorer), - // let the scope above us know - catch (e) { - $scope.colorPickerUnavailable = true; - $scope.$emit('guacInputColorUnavailable', e); - } - - }, angular.noop); + /** + * Prompts the user to choose a color by displaying a color selection + * dialog. If the user chooses a color, this directive's model is + * automatically updated. If the user cancels the dialog, the model is + * left untouched. + */ + $scope.selectColor = function selectColor() { + colorPickerService.selectColor($element[0], $scope.model, $scope.palette) + .then(function colorSelected(color) { + $scope.model = color; + }, angular.noop); + }; }]; diff --git a/guacamole/src/main/webapp/app/form/services/colorPickerService.js b/guacamole/src/main/webapp/app/form/services/colorPickerService.js new file mode 100644 index 000000000..cb9e63f81 --- /dev/null +++ b/guacamole/src/main/webapp/app/form/services/colorPickerService.js @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * A service for prompting the user to choose a color using the "Pickr" color + * picker. As the Pickr color picker might not be available if the JavaScript + * features it requires are not supported by the browser (Internet Explorer), + * the isAvailable() function should be used to test for usability. + */ +angular.module('form').provider('colorPickerService', function colorPickerServiceProvider() { + + /** + * A singleton instance of the "Pickr" color picker, shared by all users of + * this service. Pickr does not initialize synchronously, nor is it + * supported by all browsers. If Pickr is not yet initialized, or is + * unsupported, this will be null. + * + * @type {Pickr} + */ + var pickr = null; + + /** + * Whether Pickr has completed initialization. + * + * @type {Boolean} + */ + var pickrInitComplete = false; + + /** + * The HTML element to provide to Pickr as the root element. + * + * @type {HTMLDivElement} + */ + var pickerContainer = document.createElement('div'); + pickerContainer.className = 'shared-color-picker'; + + /** + * An instance of Deferred which represents an active request for the + * user to choose a color. The promise associated with the Deferred will + * be resolved with the chosen color once a color is chosen, and rejected + * if the request is cancelled or Pickr is not available. If no request is + * active, this will be null. + * + * @type {Deferred} + */ + var activeRequest = null; + + /** + * Resolves the current active request with the given color value. If no + * color value is provided, the active request is rejected. If no request + * is active, this function has no effect. + * + * @param {String} [color] + * The color value to resolve the active request with. + */ + var completeActiveRequest = function completeActiveRequest(color) { + if (activeRequest) { + + // Hide color picker, if shown + pickr.hide(); + + // Resolve/reject active request depending on value provided + if (color) + activeRequest.resolve(color); + else + activeRequest.reject(); + + // No active request + activeRequest = null; + + } + }; + + try { + pickr = Pickr.create({ + + // Bind color picker to the container element + el : pickerContainer, + + // Wrap color picker dialog in Guacamole-specific class for + // sake of additional styling + appClass : 'guac-input-color-picker', + + 'default' : '#000000', + + // Display color details as hex + defaultRepresentation : 'HEX', + + // Use "monolith" theme, as a nice balance between "nano" (does + // not work in Internet Explorer) and "classic" (too big) + theme : 'monolith', + + // Leverage the container element as the button which shows the + // picker, relying on our own styling for that button + useAsButton : true, + appendToBody : true, + + // Do not include opacity controls + lockOpacity : true, + + // Include a selection of palette entries for convenience and + // reference + swatches : [], + + components: { + + // Include hue and color preview controls + preview : true, + hue : true, + + // Display only a text color input field and the save and + // cancel buttons (no clear button) + interaction: { + input : true, + save : true, + cancel : true + } + + } + + }); + + // Hide color picker after user clicks "cancel" + pickr.on('cancel', function colorChangeCanceled() { + completeActiveRequest(); + }); + + // Keep model in sync with changes to the color picker + pickr.on('save', function colorChanged(color) { + completeActiveRequest(color.toHEXA().toString()); + activeRequest = null; + }); + + // Keep color picker in sync with changes to the model + pickr.on('init', function pickrReady() { + pickrInitComplete = true; + }); + } + catch (e) { + // If the "Pickr" color picker cannot be loaded (Internet Explorer), + // the available flag will remain set to false + } + + // Factory method required by provider + this.$get = ['$injector', function colorPickerServiceFactory($injector) { + + // Required services + var $q = $injector.get('$q'); + var $translate = $injector.get('$translate'); + + var service = {}; + + /** + * Promise which is resolved when Pickr initialization has completed + * and rejected if Pickr cannot be used. + * + * @type {Promise} + */ + var pickrPromise = (function getPickr() { + + var deferred = $q.defer(); + + // Resolve promise when Pickr has completed initialization + if (pickrInitComplete) + deferred.resolve(); + else if (pickr) + pickr.on('init', deferred.resolve); + + // Reject promise if Pickr cannot be used at all + else + deferred.reject(); + + return deferred.promise; + + })(); + + /** + * Returns whether the underlying color picker (Pickr) can be used by + * calling selectColor(). If the browser cannot support the color + * picker, false is returned. + * + * @returns {Boolean} + * true if the underlying color picker can be used by calling + * selectColor(), false otherwise. + */ + service.isAvailable = function isAvailable() { + return !!pickr; + }; + + /** + * Prompts the user to choose a color, returning the color chosen via a + * Promise. + * + * @param {Element} element + * The element that the user interacted with to indicate their + * desire to choose a color. + * + * @param {String} current + * The color that should be selected by default, in standard + * 6-digit hexadecimal RGB format, including "#" prefix. + * + * @param {String[]} [palette] + * An array of color choices which should be exposed to the user + * within the color chooser for convenience. Each color must be in + * standard 6-digit hexadecimal RGB format, including "#" prefix. + * + * @returns {Promise.} + * A Promise which is resolved with the color chosen by the user, + * in standard 6-digit hexadecimal RGB format with "#" prefix, and + * rejected if the selection operation was cancelled or the color + * picker cannot be used. + */ + service.selectColor = function selectColor(element, current, palette) { + + // Show picker once the relevant translation strings have been + // retrieved and Pickr is ready for use + return $q.all({ + 'saveString' : $translate('APP.ACTION_SAVE'), + 'cancelString' : $translate('APP.ACTION_CANCEL'), + 'pickr' : pickrPromise + }).then(function dependenciesReady(deps) { + + // Cancel any active request + completeActiveRequest(); + + // Reset state of color picker to provided parameters + pickr.setColor(current); + element.appendChild(pickerContainer); + + // Assign translated strings to button text + var pickrRoot = pickr.getRoot(); + pickrRoot.interaction.save.value = deps.saveString; + pickrRoot.interaction.cancel.value = deps.cancelString; + + // Replace all color swatches with the palette of colors given + while (pickr.removeSwatch(0)) {} + angular.forEach(palette, pickr.addSwatch.bind(pickr)); + + // Show color picker and wait for user to complete selection + activeRequest = $q.defer(); + pickr.show(); + return activeRequest.promise; + + }); + + }; + + return service; + + }]; + +}); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/form/templates/guacInputColor.html b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html index fc6e675f5..eae1f6699 100644 --- a/guacamole/src/main/webapp/app/form/templates/guacInputColor.html +++ b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html @@ -1,8 +1,9 @@