Configurable menubar color
authorColeman Watts <coleman@civicrm.org>
Mon, 8 Apr 2019 01:05:14 +0000 (21:05 -0400)
committerColeman Watts <coleman@civicrm.org>
Mon, 8 Apr 2019 20:58:25 +0000 (16:58 -0400)
CRM/Admin/Form/Preferences/Display.php
CRM/Admin/Form/SettingTrait.php
CRM/Core/Resources.php
CRM/Utils/Color.php
CRM/Utils/Rule.php
CRM/Utils/Type.php
css/crm-menubar.css
settings/Core.setting.php
templates/CRM/Admin/Form/Preferences/Display.tpl
tests/phpunit/CRM/Utils/RuleTest.php

index 7cf91d240baa628ceef98f983d00184324cd9351..5f84883e956bfa8b0e006656d4683c282db098fa 100644 (file)
@@ -52,6 +52,7 @@ class CRM_Admin_Form_Preferences_Display extends CRM_Admin_Form_Preferences {
     'display_name_format' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
     'sort_name_format' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
     'menubar_position' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
+    'menubar_color' => CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
   ];
 
   /**
@@ -105,6 +106,8 @@ class CRM_Admin_Form_Preferences_Display extends CRM_Admin_Form_Preferences {
 
     $this->postProcessCommon();
 
+    \Civi::service('asset_builder')->clear();
+
     // If "Configure CKEditor" button was clicked
     if (!empty($this->_params['ckeditor_config'])) {
       // Suppress the "Saved" status message and redirect to the CKEditor Config page
index 6a056a95f88b27ebcbd0eab7b8c6e2e7f0a62d20..f22470803fe66521fcd3ad141c6d8a02bfce1c92 100644 (file)
@@ -238,6 +238,9 @@ trait CRM_Admin_Form_SettingTrait {
         elseif ($add === 'addYesNo' && ($props['type'] === 'Boolean')) {
           $this->addRadio($setting, ts($props['title']), [1 => 'Yes', 0 => 'No'], NULL, '&nbsp;&nbsp;');
         }
+        elseif ($add === 'add') {
+          $this->add($props['html_type'], $setting, ts($props['title']), $options);
+        }
         else {
           $this->$add($setting, ts($props['title']), $options);
         }
@@ -293,6 +296,7 @@ trait CRM_Admin_Form_SettingTrait {
       'entity_reference' => 'EntityRef',
       'advmultiselect' => 'Element',
     ];
+    $mapping += array_fill_keys(CRM_Core_Form::$html5Types, '');
     return $mapping[$htmlType];
   }
 
index 4c3fcd6ec1a0122814777284dc6b141636e0388d..98b3ba8ae5bd965e6c4a97a3aee5111c93981950 100644 (file)
@@ -844,9 +844,18 @@ class CRM_Core_Resources {
     foreach ($items as $item) {
       $e->content .= file_get_contents(self::singleton()->getPath('civicrm', $item));
     }
+    $color = Civi::settings()->get('menubar_color');
+    if (!CRM_Utils_Rule::color($color)) {
+      $color = Civi::settings()->getDefault('menubar_color');
+    }
     $vars = [
       'resourceBase' => rtrim($config->resourceBase, '/'),
+      'menubarColor' => $color,
+      'semiTransparentMenuColor' => 'rgba(' . implode(', ', CRM_Utils_Color::getRgb($color)) . ', .85)',
+      'highlightColor' => CRM_Utils_Color::getHighlight($color),
+      'textColor' => CRM_Utils_Color::getContrast($color, '#333', '#ddd'),
     ];
+    $vars['highlightTextColor'] = CRM_Utils_Color::getContrast($vars['highlightColor'], '#333', '#ddd');
     foreach ($vars as $var => $val) {
       $e->content = str_replace('$' . $var, $val, $e->content);
     }
index 80bac7f8f898dfe7fccda91c2f401549b1b14a3a..2cceca9ff33468d6027255e2f5361fa96bfc96fc 100644 (file)
@@ -42,15 +42,55 @@ class CRM_Utils_Color {
    * Based on YIQ value.
    *
    * @param string $hexcolor
+   * @param string $black
+   * @param string $white
    * @return string
    */
-  public static function getContrast($hexcolor) {
-    $hexcolor = trim($hexcolor, ' #');
-    $r = hexdec(substr($hexcolor, 0, 2));
-    $g = hexdec(substr($hexcolor, 2, 2));
-    $b = hexdec(substr($hexcolor, 4, 2));
+  public static function getContrast($hexcolor, $black = 'black', $white = 'white') {
+    list($r, $g, $b) = self::getRgb($hexcolor);
     $yiq = (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
-    return ($yiq >= 128) ? 'black' : 'white';
+    return ($yiq >= 128) ? $black : $white;
+  }
+
+  /**
+   * Convert hex color to decimal
+   *
+   * @param string $hexcolor
+   * @return array
+   *   [red, green, blue]
+   */
+  public static function getRgb($hexcolor) {
+    $hexcolor = trim($hexcolor, ' #');
+    if (strlen($hexcolor) === 3) {
+      $hexcolor = $hexcolor[0] . $hexcolor[0] . $hexcolor[1] . $hexcolor[1] . $hexcolor[2] . $hexcolor[2];
+    }
+    return [
+      hexdec(substr($hexcolor, 0, 2)),
+      hexdec(substr($hexcolor, 2, 2)),
+      hexdec(substr($hexcolor, 4, 2)),
+    ];
+  }
+
+  /**
+   * Calculate a highlight color from a base color
+   *
+   * @param $hexcolor
+   * @return string
+   */
+  public static function getHighlight($hexcolor) {
+    $rgb = CRM_Utils_Color::getRgb($hexcolor);
+    $avg = array_sum($rgb) / 3;
+    foreach ($rgb as &$v) {
+      if ($avg > 242) {
+        // For very bright values, lower the brightness
+        $v -= 50;
+      }
+      else {
+        // Bump up brightness on a nonlinear curve - darker colors get more of a boost
+        $v = min(255, intval((-.0035 * ($v - 242) ** 2) + 260));
+      }
+    }
+    return '#' . implode(array_map('dechex', $rgb));
   }
 
 }
index 33a3d52bc093bb1c839826ee80669f0138250738..50d72d38352e74e4d484241548f7464638a20c15 100644 (file)
@@ -540,6 +540,16 @@ class CRM_Utils_Rule {
     return preg_match('/^\d{' . $noOfDigit . '}$/', $value) ? TRUE : FALSE;
   }
 
+  /**
+   * Strict validation of 6-digit hex color notation per html5 <input type="color">
+   *
+   * @param $value
+   * @return bool
+   */
+  public static function color($value) {
+    return (bool) preg_match('/^#([\da-fA-F]{6})$/', $value);
+  }
+
   /**
    * Strip thousand separator from a money string.
    *
index 5baf250990239b664268f805abbafac7b7ed013e..e2786712a420786e263ea190cd925d0c160516db 100644 (file)
@@ -434,6 +434,7 @@ class CRM_Utils_Type {
       'ExtensionKey',
       'Json',
       'Alphanumeric',
+      'Color',
     ];
     if (!in_array($type, $possibleTypes)) {
       if ($isThrowException) {
@@ -554,6 +555,12 @@ class CRM_Utils_Type {
           return $data;
         }
         break;
+
+      case 'Color':
+        if (CRM_Utils_Rule::color($data)) {
+          return $data;
+        }
+        break;
     }
 
     if ($abort) {
index 25aa00ca5cbf01a7626d9169cd92eeffcc9ceb04..aa53639ccc1e17d94351141a9f8a34e4deb1b28a 100644 (file)
@@ -6,8 +6,8 @@
   font-size: 13px;
 }
 #civicrm-menu {
-  background-color: #f2f2f2;
-       width: 100%;
+  background-color: $menubarColor;
+  width: 100%;
   z-index: 500;
   height: auto;
   margin: 0;
@@ -22,7 +22,6 @@
 #civicrm-menu li a {
   padding: 12px 8px;
   text-decoration: none;
-  color: #333;
   box-shadow: none;
   border: none;
 }
 #civicrm-menu li a:hover,
 #civicrm-menu li a.highlighted {
   text-decoration: none;
-  background-color: #fff;
+  background-color: $highlightColor;
+  color: $highlightTextColor;
 }
 #civicrm-menu li li .sub-arrow:before {
   content: "\f0da";
        font-family: 'FontAwesome';
-  color: #666;
        float: right;
        margin-right: -25px;
 }
@@ -88,7 +87,7 @@
   cursor: pointer;
   color: transparent;
   -webkit-tap-highlight-color: rgba(0,0,0,0);
-  background-color: #333;
+  background-color: #1b1b1b;
 }
 
 /* responsive icon */
@@ -174,10 +173,10 @@ ul.crm-quickSearch-results.ui-state-disabled {
   float: right;
 }
 #civicrm-menu #crm-menubar-toggle-position a i {
-  color: #888;
   margin: 0;
-  border-top: 2px solid #888;
+  border-top: 2px solid $textColor;
   font-size: 11px;
+  opacity: .8;
 }
 body.crm-menubar-over-cms-menu #crm-menubar-toggle-position a i {
   transform: rotate(180deg);
@@ -215,10 +214,14 @@ body.crm-menubar-over-cms-menu #crm-menubar-toggle-position a i {
   }
 
   #civicrm-menu ul {
-    background-color: #fff;
     box-shadow: 0px 0px 2px 0 rgba(0,0,0,0.3);
   }
 
+  #civicrm-menu li a {
+    background-color: $semiTransparentMenuColor;
+    color: $textColor;
+  }
+
   #civicrm-menu > li > a {
     height: 40px;
   }
@@ -227,13 +230,6 @@ body.crm-menubar-over-cms-menu #crm-menubar-toggle-position a i {
     z-index: 200000;
   }
 
-  #civicrm-menu ul li a:focus,
-  #civicrm-menu ul li a:hover,
-  #civicrm-menu ul li a.highlighted {
-    background-color: #f2f2f2;
-    color: #222;
-  }
-
   body.crm-menubar-over-cms-menu #civicrm-menu,
   body.crm-menubar-below-cms-menu #civicrm-menu {
     position: fixed;
@@ -256,7 +252,7 @@ body.crm-menubar-over-cms-menu #crm-menubar-toggle-position a i {
   }
   #civicrm-menu {
     z-index: 100000;
-    background-color: #333;
+    background-color: #1b1b1b;
   }
        #civicrm-menu ul {
                background-color: #444;
index 8d4cdc09686fce91b2824ec40dba6f6945a509d6..e90952c6a20f1b86368df229d5f4423733db5265 100644 (file)
@@ -1024,7 +1024,7 @@ return array(
     'type' => 'String',
     'html_type' => 'select',
     'default' => 'over-cms-menu',
-    'add' => '5.9',
+    'add' => '5.12',
     'title' => ts('Menubar position'),
     'is_domain' => 1,
     'is_contact' => 0,
@@ -1037,4 +1037,19 @@ return array(
       'none' => ts('None - disable menu'),
     ),
   ),
+  'menubar_color' => array(
+    'group_name' => 'CiviCRM Preferences',
+    'group' => 'core',
+    'name' => 'menubar_color',
+    'type' => 'String',
+    'html_type' => 'color',
+    'default' => '#1b1b1b',
+    'add' => '5.13',
+    'title' => ts('Menubar color'),
+    'is_domain' => 1,
+    'is_contact' => 0,
+    'description' => ts('Color of the CiviCRM main menu.'),
+    'help_text' => NULL,
+    'validate_callback' => 'CRM_Utils_Rule::color',
+  ),
 );
index 02f0f38f7e087c400737883a6b81b1e0dbdd43de..1c2a61ea4503103fd2f0a8665624155df3148dc6 100644 (file)
         <div class="description">{ts}Default position for the CiviCRM menubar.{/ts}</div>
       </td>
     </tr>
+    <tr class="crm-preferences-display-form-block_menubar_color">
+      <td class="label">{$form.menubar_color.label}</td>
+      <td>
+        {$form.menubar_color.html}
+      </td>
+    </tr>
   </table>
   <div class="crm-submit-buttons">{include file="CRM/common/formButtons.tpl" location="bottom"}</div>
 </div>
index 188089faaa1ecd4f9771ee11df54b9fd60e3ebae..0189c9c13f23bc5d3184466be55efb0b6eb50859 100644 (file)
@@ -112,6 +112,36 @@ class CRM_Utils_RuleTest extends CiviUnitTestCase {
     );
   }
 
+  /**
+   * @dataProvider colorDataProvider
+   * @param $inputData
+   * @param $expectedResult
+   */
+  public function testColor($inputData, $expectedResult) {
+    $this->assertEquals($expectedResult, CRM_Utils_Rule::color($inputData));
+  }
+
+  /**
+   * @return array
+   */
+  public function colorDataProvider() {
+    return [
+      ['#000000', TRUE],
+      ['#ffffff', TRUE],
+      ['#123456', TRUE],
+      ['#00aaff', TRUE],
+      // Some of these are valid css colors but we reject anything that doesn't conform to the html5 spec for <input type="color">
+      ['#ffffff00', FALSE],
+      ['#fff', FALSE],
+      ['##000000', FALSE],
+      ['ffffff', FALSE],
+      ['red', FALSE],
+      ['#orange', FALSE],
+      ['', FALSE],
+      ['rgb(255, 255, 255)', FALSE],
+    ];
+  }
+
   /**
    * @return array
    */