GUACAMOLE-630: Merge use singleton instance of Pickr for all color input fields.
diff --git a/guacamole/src/main/webapp/app/form/directives/guacInputColor.js b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
index 3b010cc..1762c31 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 @@
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 @@
};
- // 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 0000000..cb9e63f
--- /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.<String>}
+ * 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/services/formService.js b/guacamole/src/main/webapp/app/form/services/formService.js
index cc0a240..4198c10 100644
--- a/guacamole/src/main/webapp/app/form/services/formService.js
+++ b/guacamole/src/main/webapp/app/form/services/formService.js
@@ -220,11 +220,46 @@
var $q = $injector.get('$q');
var $templateRequest = $injector.get('$templateRequest');
+ /**
+ * Map of module name to the injector instance created for that module.
+ *
+ * @type {Object.<String, injector>}
+ */
+ var injectors = {};
+
var service = {};
service.fieldTypes = provider.fieldTypes;
/**
+ * Given the name of a module, returns an injector instance which
+ * injects dependencies within that module. A new injector may be
+ * created and initialized if no such injector has yet been requested.
+ * If the injector available to formService already includes the
+ * requested module, that injector will simply be returned.
+ *
+ * @param {String} module
+ * The name of the module to produce an injector for.
+ *
+ * @returns {injector}
+ * An injector instance which injects dependencies for the given
+ * module.
+ */
+ var getInjector = function getInjector(module) {
+
+ // Use the formService's injector if possible
+ if ($injector.modules[module])
+ return $injector;
+
+ // If the formService's injector does not include the requested
+ // module, create the necessary injector, reusing that injector for
+ // future calls
+ injectors[module] = injectors[module] || angular.injector(['ng', module]);
+ return injectors[module];
+
+ };
+
+ /**
* Compiles and links the field associated with the given name to the given
* scope, producing a distinct and independent DOM Element which functions
* as an instance of that field. The scope object provided must include at
@@ -300,7 +335,7 @@
// Populate scope using defined controller
if (fieldType.module && fieldType.controller) {
- var $controller = angular.injector(['ng', fieldType.module]).get('$controller');
+ var $controller = getInjector(fieldType.module).get('$controller');
$controller(fieldType.controller, {
'$scope' : scope,
'$element' : angular.element(fieldContainer.childNodes)
diff --git a/guacamole/src/main/webapp/app/form/templates/guacInputColor.html b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html
index fc6e675..eae1f66 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 @@
<div class="guac-input-color"
ng-class="{
'dark' : isDark(),
- 'read-only' : colorPickerUnavailable
+ 'read-only' : !isColorPickerAvailable()
}"
+ ng-click="selectColor()"
ng-style="{
'background-color' : model
}">