From 1889d803c21e702dcf73d13072e3effa4b633358 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sun, 7 Apr 2019 11:13:47 -0400 Subject: [PATCH 1/3] Use asset-builder to render menubar css --- CRM/Core/Resources.php | 37 ++++++++++++++++++++++++++++++++----- css/crm-menubar.css | 4 ++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/CRM/Core/Resources.php b/CRM/Core/Resources.php index 49110577174c..4c3fcd6ec1a0 100644 --- a/CRM/Core/Resources.php +++ b/CRM/Core/Resources.php @@ -24,6 +24,7 @@ | see the CiviCRM license FAQ at http://civicrm.org/licensing | +--------------------------------------------------------------------+ */ +use Civi\Core\Event\GenericHookEvent; /** * This class facilitates the loading of resources @@ -581,6 +582,7 @@ public function resetCacheCode() { * @return CRM_Core_Resources */ public function addCoreResources($region = 'html-header') { + Civi::dispatcher()->addListener('hook_civicrm_buildAsset', [$this, 'renderMenubarStylesheet']); if (!isset($this->addedCoreResources[$region]) && !self::isAjaxMode()) { $this->addedCoreResources[$region] = TRUE; $config = CRM_Core_Config::singleton(); @@ -760,14 +762,10 @@ public function coreResourceList($region) { $position = Civi::settings()->get('menubar_position') ?: 'over-cms-menu'; } if ($position !== 'none') { - $cms = strtolower($config->userFramework); - $cms = $cms === 'drupal' ? 'drupal7' : $cms; $items[] = 'bower_components/smartmenus/dist/jquery.smartmenus.min.js'; $items[] = 'bower_components/smartmenus/dist/addons/keyboard/jquery.smartmenus.keyboard.min.js'; $items[] = 'js/crm.menubar.js'; - $items[] = 'bower_components/smartmenus/dist/css/sm-core-css.css'; - $items[] = 'css/crm-menubar.css'; - $items[] = "css/menubar-$cms.css"; + $items[] = Civi::service('asset_builder')->getUrl('crm-menubar.css'); $items[] = [ 'menubar' => [ 'position' => $position, @@ -825,6 +823,35 @@ public static function isAjaxMode() { return (strpos($url, 'civicrm/ajax') === 0) || (strpos($url, 'civicrm/angular') === 0); } + /** + * @param GenericHookEvent $e + * @see \CRM_Utils_Hook::buildAsset() + */ + public static function renderMenubarStylesheet(GenericHookEvent $e) { + if ($e->asset !== 'crm-menubar.css') { + return; + } + $e->mimeType = 'text/css'; + $e->content = ''; + $config = CRM_Core_Config::singleton(); + $cms = strtolower($config->userFramework); + $cms = $cms === 'drupal' ? 'drupal7' : $cms; + $items = [ + 'bower_components/smartmenus/dist/css/sm-core-css.css', + 'css/crm-menubar.css', + "css/menubar-$cms.css", + ]; + foreach ($items as $item) { + $e->content .= file_get_contents(self::singleton()->getPath('civicrm', $item)); + } + $vars = [ + 'resourceBase' => rtrim($config->resourceBase, '/'), + ]; + foreach ($vars as $var => $val) { + $e->content = str_replace('$' . $var, $val, $e->content); + } + } + /** * Provide a list of available entityRef filters. * diff --git a/css/crm-menubar.css b/css/crm-menubar.css index 06800779a664..25aa00ca5cbf 100644 --- a/css/crm-menubar.css +++ b/css/crm-menubar.css @@ -163,7 +163,7 @@ ul.crm-quickSearch-results.ui-state-disabled { } #civicrm-menu-nav .crm-logo-sm { - background: url(../i/logo_sm.png) no-repeat; + background: url($resourceBase/i/logo_sm.png) no-repeat; display: inline-block; width: 16px; height: 16px; @@ -316,7 +316,7 @@ body.crm-menubar-over-cms-menu #crm-menubar-toggle-position a i { left: 5px; width: 18px; height: 18px; - background: url(../i/logo_lg.png) no-repeat; + background: url($resourceBase/i/logo_lg.png) no-repeat; background-size: 18px; top: 6px; } From 8a52ae342ef766d2f35e8527484fc5d3c2ba708d Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sun, 7 Apr 2019 21:05:14 -0400 Subject: [PATCH 2/3] Configurable menubar color --- CRM/Admin/Form/Preferences/Display.php | 3 ++ CRM/Admin/Form/SettingTrait.php | 4 ++ CRM/Core/Resources.php | 9 ++++ CRM/Utils/Color.php | 52 ++++++++++++++++--- CRM/Utils/Rule.php | 10 ++++ CRM/Utils/Type.php | 7 +++ css/crm-menubar.css | 30 +++++------ settings/Core.setting.php | 17 +++++- .../CRM/Admin/Form/Preferences/Display.tpl | 6 +++ tests/phpunit/CRM/Utils/RuleTest.php | 30 +++++++++++ 10 files changed, 144 insertions(+), 24 deletions(-) diff --git a/CRM/Admin/Form/Preferences/Display.php b/CRM/Admin/Form/Preferences/Display.php index 7cf91d240baa..5f84883e956b 100644 --- a/CRM/Admin/Form/Preferences/Display.php +++ b/CRM/Admin/Form/Preferences/Display.php @@ -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 @@ public function postProcess() { $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 diff --git a/CRM/Admin/Form/SettingTrait.php b/CRM/Admin/Form/SettingTrait.php index 6a056a95f88b..f22470803fe6 100644 --- a/CRM/Admin/Form/SettingTrait.php +++ b/CRM/Admin/Form/SettingTrait.php @@ -238,6 +238,9 @@ protected function addFieldsDefinedInSettingsMetadata() { elseif ($add === 'addYesNo' && ($props['type'] === 'Boolean')) { $this->addRadio($setting, ts($props['title']), [1 => 'Yes', 0 => 'No'], NULL, '  '); } + 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 @@ protected function getQuickFormType($spec) { 'entity_reference' => 'EntityRef', 'advmultiselect' => 'Element', ]; + $mapping += array_fill_keys(CRM_Core_Form::$html5Types, ''); return $mapping[$htmlType]; } diff --git a/CRM/Core/Resources.php b/CRM/Core/Resources.php index 4c3fcd6ec1a0..98b3ba8ae5bd 100644 --- a/CRM/Core/Resources.php +++ b/CRM/Core/Resources.php @@ -844,9 +844,18 @@ public static function renderMenubarStylesheet(GenericHookEvent $e) { 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); } diff --git a/CRM/Utils/Color.php b/CRM/Utils/Color.php index 80bac7f8f898..2cceca9ff334 100644 --- a/CRM/Utils/Color.php +++ b/CRM/Utils/Color.php @@ -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)); } } diff --git a/CRM/Utils/Rule.php b/CRM/Utils/Rule.php index 33a3d52bc093..50d72d38352e 100644 --- a/CRM/Utils/Rule.php +++ b/CRM/Utils/Rule.php @@ -540,6 +540,16 @@ public static function numberOfDigit($value, $noOfDigit) { return preg_match('/^\d{' . $noOfDigit . '}$/', $value) ? TRUE : FALSE; } + /** + * Strict validation of 6-digit hex color notation per html5 + * + * @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. * diff --git a/CRM/Utils/Type.php b/CRM/Utils/Type.php index 5baf25099023..e2786712a420 100644 --- a/CRM/Utils/Type.php +++ b/CRM/Utils/Type.php @@ -434,6 +434,7 @@ public static function validate($data, $type, $abort = TRUE, $name = 'One of par 'ExtensionKey', 'Json', 'Alphanumeric', + 'Color', ]; if (!in_array($type, $possibleTypes)) { if ($isThrowException) { @@ -554,6 +555,12 @@ public static function validate($data, $type, $abort = TRUE, $name = 'One of par return $data; } break; + + case 'Color': + if (CRM_Utils_Rule::color($data)) { + return $data; + } + break; } if ($abort) { diff --git a/css/crm-menubar.css b/css/crm-menubar.css index 25aa00ca5cbf..aa53639ccc1e 100644 --- a/css/crm-menubar.css +++ b/css/crm-menubar.css @@ -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; } @@ -42,12 +41,12 @@ #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; diff --git a/settings/Core.setting.php b/settings/Core.setting.php index 8d4cdc09686f..e90952c6a20f 100644 --- a/settings/Core.setting.php +++ b/settings/Core.setting.php @@ -1024,7 +1024,7 @@ '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 @@ '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', + ), ); diff --git a/templates/CRM/Admin/Form/Preferences/Display.tpl b/templates/CRM/Admin/Form/Preferences/Display.tpl index 02f0f38f7e08..1c2a61ea4503 100644 --- a/templates/CRM/Admin/Form/Preferences/Display.tpl +++ b/templates/CRM/Admin/Form/Preferences/Display.tpl @@ -216,6 +216,12 @@
{ts}Default position for the CiviCRM menubar.{/ts}
+ + {$form.menubar_color.label} + + {$form.menubar_color.html} + +
{include file="CRM/common/formButtons.tpl" location="bottom"}
diff --git a/tests/phpunit/CRM/Utils/RuleTest.php b/tests/phpunit/CRM/Utils/RuleTest.php index 188089faaa1e..0189c9c13f23 100644 --- a/tests/phpunit/CRM/Utils/RuleTest.php +++ b/tests/phpunit/CRM/Utils/RuleTest.php @@ -112,6 +112,36 @@ public function moneyDataProvider() { ); } + /** + * @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 + ['#ffffff00', FALSE], + ['#fff', FALSE], + ['##000000', FALSE], + ['ffffff', FALSE], + ['red', FALSE], + ['#orange', FALSE], + ['', FALSE], + ['rgb(255, 255, 255)', FALSE], + ]; + } + /** * @return array */ From 08a3a5199c0f3437b15279a08c946147b686cbff Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 8 Apr 2019 16:20:36 -0700 Subject: [PATCH 3/3] Pass menubar preference as a param. Simplify cache mechanics. (#8) Ex: If an admin uses an API call (CLI/REST) to change the menubar color, then they don't need to follow-up with a cache-clear. The new setting just goes live. Ex: If a customization (via `civicrm.settings.php` or via extension) decides on the color scheme programmatically (e.g. per-domain or per-role or per-user-preference), then they don't need to clear cache. Multiple color schemes can coexist. --- CRM/Admin/Form/Preferences/Display.php | 2 -- CRM/Core/Resources.php | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CRM/Admin/Form/Preferences/Display.php b/CRM/Admin/Form/Preferences/Display.php index 5f84883e956b..9ba36825cdf9 100644 --- a/CRM/Admin/Form/Preferences/Display.php +++ b/CRM/Admin/Form/Preferences/Display.php @@ -106,8 +106,6 @@ public function postProcess() { $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 diff --git a/CRM/Core/Resources.php b/CRM/Core/Resources.php index 98b3ba8ae5bd..55d8461d8fd4 100644 --- a/CRM/Core/Resources.php +++ b/CRM/Core/Resources.php @@ -765,7 +765,9 @@ public function coreResourceList($region) { $items[] = 'bower_components/smartmenus/dist/jquery.smartmenus.min.js'; $items[] = 'bower_components/smartmenus/dist/addons/keyboard/jquery.smartmenus.keyboard.min.js'; $items[] = 'js/crm.menubar.js'; - $items[] = Civi::service('asset_builder')->getUrl('crm-menubar.css'); + $items[] = Civi::service('asset_builder')->getUrl('crm-menubar.css', [ + 'color' => Civi::settings()->get('menubar_color'), + ]); $items[] = [ 'menubar' => [ 'position' => $position, @@ -844,7 +846,7 @@ public static function renderMenubarStylesheet(GenericHookEvent $e) { foreach ($items as $item) { $e->content .= file_get_contents(self::singleton()->getPath('civicrm', $item)); } - $color = Civi::settings()->get('menubar_color'); + $color = $e->params['color']; if (!CRM_Utils_Rule::color($color)) { $color = Civi::settings()->getDefault('menubar_color'); }