diff --git a/core/includes/common.inc b/core/includes/common.inc index 08ce2ab..f05211c 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2088,6 +2088,85 @@ function _format_date_callback(array $matches = NULL, $new_langcode = NULL) { } /** + * Parses a color string. + * + * @param $hex + * A hexadecimal color string to parse. Can be prefixed with '#' and have + * either one or two digits per component. + * @param $alpha + * (Optional) Whether or not to allow an alpha transparency component. + * Defaults to FALSE. + * + * @return + * An array with the keys 'red', 'green', 'blue' and 'alpha' with the + * corresponding integers as values. FALSE if the input is invalid. + * + * @see drupal_rgba_to_hex() + */ +function drupal_hex_to_rgba($hex, $alpha = FALSE) { + // Ignore '#' prefixes. + $hex = ltrim($hex, '#'); + + // Convert shorhands like '#abc' to '#aabbcc'. + if (strlen($hex) <= 4) { + $hex = preg_replace('|([0-9a-z])|i', '\1\1', $hex); + } + + // Parse out the components. + if (!preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-7][0-9a-f])?$/i', $hex, $rgba)) { + return FALSE; + } + + // The input is invalid, if alpha is not allowed but given. + if (!$alpha && isset($rgba[4])) { + return FALSE; + } + + // Return the result array. + return array( + 'red' => hexdec($rgba[1]), + 'green' => hexdec($rgba[2]), + 'blue' => hexdec($rgba[3]), + 'alpha' => isset($rgba[4]) ? hexdec($rgba[4]) : 0, + ); +} + +/** + * Converts a color array to a color string. + * + * @param $rgba + * An array with integer values for the keys 'red', 'green', 'blue' and + * 'alpha'. Components that are not given default to 0. + * + * @return + * A lowercase color string like '#00ff00'. + * + * @see drupal_hex_to_rgba() + */ +function drupal_rgba_to_hex($rgba) { + if (!is_array($rgba)) { + return '#000000'; + } + + $rgba += array( + 'red' => 0, + 'green' => 0, + 'blue' => 0, + 'alpha' => 0, + ); + + $result = '#'; + + foreach (array('red', 'green', 'blue', 'alpha') as $component) { + if ($component != 'alpha' || $rgba[$component]) { + $result .= str_pad(dechex($rgba[$component]), 2, '0', STR_PAD_LEFT); + } + } + + return $result; +} + +/** * @} End of "defgroup format". */ @@ -7028,6 +7107,9 @@ function drupal_common_theme() { 'range' => array( 'render element' => 'element', ), + 'color' => array( + 'render element' => 'element', + ), 'form' => array( 'render element' => 'element', ), diff --git a/core/includes/form.inc b/core/includes/form.inc index 038cfca..a345d7a 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -4086,6 +4086,45 @@ function form_validate_url(&$element, &$form_state) { } /** + * Form element validation handler for #type 'color'. + */ +function form_validate_color($element, &$form_state) { + // Empty means black. + $value = trim($element['#value']); + if ($value === '') { + $value = '#000000'; + } + + // Try to parse the value. + if ($parsed = drupal_hex_to_rgba($value)) { + // Set a normalized value. + form_set_value($element, drupal_rgba_to_hex($parsed), $form_state); + } + else { + form_error($element, t('%name must be a valid color.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title']))); + } +} + +/** + * Returns HTML for a color form element. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #attributes. + * + * @ingroup themeable + */ +function theme_color($variables) { + $element = $variables['element']; + $element['#attributes']['type'] = 'color'; + element_set_attributes($element, array('id', 'name', 'value')); + _form_set_class($element, array('form-color')); + + return '' . drupal_render_children($element); +} + +/** * Returns HTML for a form. * * @param $variables diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php index 5d868a2..574ea9d 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -2268,6 +2268,7 @@ class DrupalWebTestCase extends DrupalTestCase { case 'url': case 'number': case 'range': + case 'color': case 'hidden': case 'password': case 'email': diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 200a335..0a47407 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -425,6 +425,14 @@ function system_element_info() { '#theme' => 'range', '#theme_wrappers' => array('form_element'), ); + $types['color'] = array( + '#input' => TRUE, + '#default_value' => '#000000', + '#process' => array('ajax_process_form'), + '#element_validate' => array('form_validate_color'), + '#theme' => 'color', + '#theme_wrappers' => array('form_element'), + ); $types['machine_name'] = array( '#input' => TRUE, '#default_value' => NULL, diff --git a/core/modules/system/tests/common.test b/core/modules/system/tests/common.test index cc51a01..0a12240 100644 --- a/core/modules/system/tests/common.test +++ b/core/modules/system/tests/common.test @@ -2073,6 +2073,67 @@ class CommonValidNumberStepUnitTestCase extends DrupalUnitTestCase { } /** + * Tests color conversion functions. + */ +class CommonColorConversionTestCase extends DrupalUnitTestCase { + public static function getInfo() { + return array( + 'name' => 'Color conversion', + 'description' => 'Tests color conversion by drupal_hex_to_rgba() and drupal_rgba_to_hex()', + 'group' => 'Common', + ); + } + + /** + * Tests drupal_hex_to_rgba(). + */ + function testDrupalHexToRGBA() { + // Test shorthand conversion without alpha. + $this->assertEqual(drupal_hex_to_rgba('#000'), array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0)); + $this->assertEqual(drupal_hex_to_rgba('#fff'), array('red' => 255, 'green' => 255, 'blue' => 255, 'alpha' => 0)); + $this->assertEqual(drupal_hex_to_rgba('#abc'), array('red' => 170, 'green' => 187, 'blue' => 204, 'alpha' => 0)); + $this->assertEqual(drupal_hex_to_rgba('cba'), array('red' => 204, 'green' => 187, 'blue' => 170, 'alpha' => 0)); + + // Test shorthand conversion with alpha. + $this->assertEqual(drupal_hex_to_rgba('#0007', TRUE), array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 119)); + $this->assertEqual(drupal_hex_to_rgba('#7001', TRUE), array('red' => 119, 'green' => 0, 'blue' => 0, 'alpha' => 17)); + $this->assertEqual(drupal_hex_to_rgba('2000', TRUE), array('red' => 34, 'green' => 0, 'blue' => 0, 'alpha' => 0)); + + // Test conversion without alpha. + $this->assertEqual(drupal_hex_to_rgba('#000000'), array('red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0)); + $this->assertEqual(drupal_hex_to_rgba('#010203'), array('red' => 1, 'green' => 2, 'blue' => 3, 'alpha' => 0)); + + // Test too high alpha values. + $this->assertIdentical(drupal_hex_to_rgba('#0008', TRUE), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('#a00f', TRUE), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('#aa00aaf1', TRUE), FALSE); + + // Test alpha values are invalid if they are not allowed. + $this->assertIdentical(drupal_hex_to_rgba('#1111'), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('#22334455'), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('#0000'), FALSE); + + // Test bogus input. + $this->assertIdentical(drupal_hex_to_rgba('#foo'), FALSE); + $this->assertIdentical(drupal_hex_to_rgba('123456789'), FALSE); + } + + /** + * Tests drupal_rgba_to_hex(). + */ + function testDrupalRGBAToHex() { + // Test conversion without alpha. + $this->assertEqual(drupal_rgba_to_hex(array('red' => 7)), '#070000'); + $this->assertEqual(drupal_rgba_to_hex(array('green' => 255)), '#00ff00'); + $this->assertEqual(drupal_rgba_to_hex(array('red' => 1, 'green' => 2, 'blue' => 3)), '#010203'); + + // Test conversion with alpha. + $this->assertEqual(drupal_rgba_to_hex(array('alpha' => 10)), '#0000000a'); + $this->assertEqual(drupal_rgba_to_hex(array('green' => 15, 'alpha' => 3)), '#000f0003'); + } +} + +/** * Tests writing of data records with drupal_write_record(). */ class CommonDrupalWriteRecordTestCase extends DrupalWebTestCase { diff --git a/core/modules/system/tests/form.test b/core/modules/system/tests/form.test index 8bcce76..4ba445c 100644 --- a/core/modules/system/tests/form.test +++ b/core/modules/system/tests/form.test @@ -366,6 +366,41 @@ class FormsTestCase extends DrupalWebTestCase { } /** + * Tests validation of #type 'color' elements. + */ + function testColorValidation() { + // Keys are inputs, values are expected results. + $values = array( + '' => '#000000', + '#000' => '#000000', + 'AAA' => '#aaaaaa', + '#af0DEE' => '#af0dee', + '#99ccBc' => '#99ccbc', + '#aabbcc' => '#aabbcc', + '123456' => '#123456', + ); + + // Tests that valid values are properly normalized. + foreach ($values as $input => $expected) { + $edit = array( + 'color' => $input, + ); + $result = json_decode($this->drupalPost('form-test/color', $edit, 'Submit')); + $this->assertEqual($result->color, $expected); + } + + // Tests invalid values are rejected. + $values = array('#0008', '#1234', '#ffffff', '#abcdef22', '17', '#uaa'); + foreach ($values as $input) { + $edit = array( + 'color' => $input, + ); + $this->drupalPost('form-test/color', $edit, 'Submit'); + $this->assertRaw(t('%name must be a valid color.', array('%name' => 'Color'))); + } + } + + /** * Test handling of disabled elements. * * @see _form_test_disabled_elements() @@ -408,7 +443,7 @@ class FormsTestCase extends DrupalWebTestCase { // All the elements should be marked as disabled, including the ones below // the disabled container. - $this->assertEqual(count($disabled_elements), 39, 'The correct elements have the disabled property in the HTML code.'); + $this->assertEqual(count($disabled_elements), 40, 'The correct elements have the disabled property in the HTML code.'); $this->drupalPost(NULL, $edit, t('Submit')); $returned_values['hijacked'] = drupal_json_decode($this->content); diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module index c727c51..c82b301 100644 --- a/core/modules/system/tests/modules/form_test/form_test.module +++ b/core/modules/system/tests/modules/form_test/form_test.module @@ -145,6 +145,12 @@ function form_test_menu() { 'page arguments' => array('form_test_number', 'range'), 'access callback' => TRUE, ); + $items['form-test/color'] = array( + 'title' => 'Color', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_color'), + 'access callback' => TRUE, + ); $items['form-test/checkboxes-radios'] = array( 'title' => t('Checkboxes, Radios'), 'page callback' => 'drupal_get_form', @@ -1277,6 +1283,33 @@ function form_test_number($form, &$form_state, $element = 'number') { } /** + * Form constructor for testing #type 'color' elements. + * + * @see form_test_color_submit() + * @ingroup forms + */ +function form_test_color($form, &$form_state) { + $form['color'] = array( + '#type' => 'color', + '#title' => 'Color', + '#description' => 'The default value is green.', + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Submit', + ); + return $form; +} + +/** + * Form submission handler for form_test_color(). + */ +function form_test_color_submit($form, &$form_state) { + drupal_json_output($form_state['values']); + exit; +} + +/** * Builds a form to test the placeholder attribute. */ function form_test_placeholder_test($form, &$form_state) { @@ -1496,6 +1529,16 @@ function _form_test_disabled_elements($form, &$form_state) { ); } + // Color. + $form['color'] = array( + '#type' => 'color', + '#title' => 'color', + '#disabled' => TRUE, + '#default_value' => '#0000ff', + '#test_hijack_value' => '#ff0000', + '#disabled' => TRUE, + ); + // Date. $form['date'] = array( '#type' => 'date', diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css index 7ab3d4e..3563b44 100644 --- a/core/themes/bartik/css/style.css +++ b/core/themes/bartik/css/style.css @@ -1194,6 +1194,7 @@ input.form-email, input.form-url, input.form-search, input.form-number, +input.form-color, textarea.form-textarea, select.form-select { border: 1px solid #ccc; diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css index a3d6773..9e807d5 100644 --- a/core/themes/seven/style.css +++ b/core/themes/seven/style.css @@ -612,6 +612,7 @@ div.teaser-checkbox .form-item, .form-disabled input.form-url, .form-disabled input.form-search, .form-disabled input.form-number, +.form-disabled input.form-color, .form-disabled input.form-file, .form-disabled textarea.form-textarea, .form-disabled select.form-select { @@ -703,6 +704,7 @@ input.form-email, input.form-url, input.form-search, input.form-number, +input.form-color, input.form-file, textarea.form-textarea, select.form-select { @@ -721,6 +723,7 @@ input.form-email:focus, input.form-url:focus, input.form-search:focus, input.form-number:focus, +input.form-color:focus, input.form-file:focus, textarea.form-textarea:focus, select.form-select:focus {