diff --git a/application/themehandler.class.inc.php b/application/themehandler.class.inc.php index c300fae0e..99931a724 100644 --- a/application/themehandler.class.inc.php +++ b/application/themehandler.class.inc.php @@ -25,6 +25,7 @@ */ class ThemeHandler { + private static $oCompileCSSService; /** * Return default theme name and parameters * @@ -108,7 +109,7 @@ class ThemeHandler SetupUtils::builddir($sDefaultThemeDirPath); } - static::CompileTheme($sThemeId, $aDefaultTheme['parameters']); + static::CompileTheme($sThemeId, false, $aDefaultTheme['parameters']); } // Return absolute url to theme compiled css @@ -121,13 +122,14 @@ class ThemeHandler * 2) The produced CSS file exists and its signature is equal to the expected signature (imports, stylesheets, variables) * * @param string $sThemeId + * @param bool $bSetup : indicated whether compilation is performed in setup context (true) or when loading a page/theme (false) * @param array|null $aThemeParameters Parameters (variables, imports, stylesheets) for the theme, if not passed, will be retrieved from compiled DM * @param array|null $aImportsPaths Paths where imports can be found. Must end with '/' * @param string|null $sWorkingPath Path of the folder used during compilation. Must end with a '/' * * @throws \CoreException */ - public static function CompileTheme($sThemeId, $aThemeParameters = null, $aImportsPaths = null, $sWorkingPath = null) + public static function CompileTheme($sThemeId, $bSetup=false, $aThemeParameters = null, $aImportsPaths = null, $sWorkingPath = null) { // Default working path if($sWorkingPath === null) @@ -150,6 +152,11 @@ class ThemeHandler // Save parameters if passed... (typically during DM compilation) if(is_array($aThemeParameters)) { + if (!is_dir($sThemeFolderPath)) + { + mkdir($sWorkingPath.'/branding/'); + mkdir($sWorkingPath.'/branding/themes/'); + } file_put_contents($sThemeFolderPath.'/theme-parameters.json', json_encode($aThemeParameters)); } // ... Otherwise, retrieve them from compiled DM (typically when switching current theme in the config. file) @@ -185,33 +192,52 @@ class ThemeHandler // Checking if our compiled css is outdated $iFilemetime = @filemtime($sThemeCssPath); - if (!file_exists($sThemeCssPath) || (is_writable($sThemeFolderPath) && ($iFilemetime < $iStyleLastModified))) + $bFileExists = file_exists($sThemeCssPath); + $bVarSignatureChanged=false; + if ($bFileExists && $bSetup) + { + $sPrecompiledSignature = static::GetSignature($sThemeCssPath); + //check variable signature has changed which is independant from any file modification + if (!empty($sPrecompiledSignature)){ + $sPreviousVariableSignature = static::GetVarSignature($sPrecompiledSignature); + $sCurrentVariableSignature = md5(json_encode($aThemeParameters['variables'])); + $bVarSignatureChanged= ($sPreviousVariableSignature!==$sCurrentVariableSignature); + } + } + + if (!$bFileExists || $bVarSignatureChanged || (is_writable($sThemeFolderPath) && ($iFilemetime < $iStyleLastModified))) { // Dates don't match. Second chance: check if the already compiled stylesheet exists and is consistent based on its signature $sActualSignature = static::ComputeSignature($aThemeParameters, $aImportsPaths); - if (file_exists($sThemeCssPath)) + + if ($bFileExists && !$bSetup) { $sPrecompiledSignature = static::GetSignature($sThemeCssPath); - if($sActualSignature == $sPrecompiledSignature) - { - touch($sThemeCssPath); // Stylesheet is up to date, mark it as more recent to speedup next time - } + } + + if (!empty($sPrecompiledSignature) && $sActualSignature == $sPrecompiledSignature) + { + touch($sThemeCssPath); // Stylesheet is up to date, mark it as more recent to speedup next time } else { // Alas, we really need to recompile // Add the signature to the generated CSS file so that the file can be used as a precompiled stylesheet if needed $sSignatureComment = -<<CompileCSSFromSASS($sTmpThemeScssContent, $aImportsPaths, + $aThemeParameters['variables']); file_put_contents($sThemeCssPath, $sSignatureComment.$sTmpThemeCssContent); } } @@ -222,12 +248,14 @@ CSS * 1) one MD5 of all the variables/values (JSON encoded) * 2) the MD5 of each stylesheet file * 3) the MD5 of each import file - * + * * @param string[] $aThemeParameters * @param string[] $aImportsPaths + * * @return string + * @throws \Exception */ - private static function ComputeSignature($aThemeParameters, $aImportsPaths) + public static function ComputeSignature($aThemeParameters, $aImportsPaths) { $aSignature = array( 'variables' => md5(json_encode($aThemeParameters['variables'])), @@ -251,14 +279,15 @@ CSS /** * Extract the signature for a generated CSS file. The signature MUST be alone one line immediately * followed (on the next line) by the === SIGNATURE END === pattern - * + * * Note the signature can be place anywhere in the CSS file (obviously inside a CSS comment !) but the * function will be faster if the signature is at the beginning of the file (since the file is scanned from the start) - * - * @param string $sFile + * + * @param $sFilepath + * * @return string */ - private static function GetSignature($sFilepath) + public static function GetSignature($sFilepath) { $sPreviousLine = ''; $hFile = @fopen($sFilepath, "r"); @@ -276,6 +305,16 @@ CSS return $sPreviousLine; } + public static function GetVarSignature($JsonSignature) + { + $aJsonArray = json_decode($JsonSignature, true); + if (array_key_exists('variables', $aJsonArray)) + { + return $aJsonArray['variables']; + } + return false; + } + /** * Find the given file in the list of ImportsPaths directory * @param string $sFile @@ -295,5 +334,25 @@ CSS } return ''; // Not found, fail silently, maybe the SCSS compiler knowns better... } + + public static function mockCompileCSSService($oCompileCSSServiceMock) + { + self::$oCompileCSSService = $oCompileCSSServiceMock; + } +} + +class CompileCSSService +{ + /** + * CompileCSSService constructor. + */ + public function __construct() + { + } + + public function CompileCSSFromSASS($sSassContent, $aImportPaths = array(), $aVariables = array()){ + return utils::CompileCSSFromSASS($sSassContent, $aImportPaths, $aVariables); + } + } diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index bedf93c6c..cc056e408 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -2759,7 +2759,7 @@ EOF; { $this->Log("Precompiled file not found: '$sPrecompiledFile'"); } - ThemeHandler::CompileTheme($sThemeId, $aThemeParameters, $aImportsPaths, $sTempTargetDir); + ThemeHandler::CompileTheme($sThemeId, true, $aThemeParameters, $aImportsPaths, $sTempTargetDir); } $this->Log(sprintf('Themes compilation took: %.3f ms for %d themes.', (microtime(true) - $fStart)*1000.0, count($aThemes))); } diff --git a/test/application/ThemeHandlerTest.php b/test/application/ThemeHandlerTest.php new file mode 100644 index 000000000..c72a5a98e --- /dev/null +++ b/test/application/ThemeHandlerTest.php @@ -0,0 +1,333 @@ +compileCSSServiceMock = $this->createMock('CompileCSSService'); + ThemeHandler::mockCompileCSSService($this->compileCSSServiceMock); + + $this->tmpDir=$this->tmpdir(); + + if (!is_dir($this->tmpDir ."/branding")) + { + @mkdir($this->tmpDir."/branding"); + } + @mkdir($this->tmpDir."/branding/themes/"); + @mkdir($this->tmpDir."/branding/themes/basque-red"); + $this->cssPath = $this->tmpDir . '/branding/themes/basque-red/main.css'; + $this->jsonThemeParamFile = $this->tmpDir . '/branding/themes/basque-red/theme-parameters.json'; + $this->recurse_copy(APPROOT."/test/application/theme-handler/expected/css", $this->tmpDir."/branding/css"); + } + + function tmpdir() { + $tmpfile=tempnam(sys_get_temp_dir(),''); + if (file_exists($tmpfile)) + { + unlink($tmpfile); + } + mkdir($tmpfile); + if (is_dir($tmpfile)) + { + return $tmpfile; + } + + return sys_get_temp_dir(); + } + + public function recurse_copy($src,$dst) { + $dir = opendir($src); + @mkdir($dst); + while(false !== ( $file = readdir($dir)) ) { + if (( $file != '.' ) && ( $file != '..' )) { + if ( is_dir($src . '/' . $file) ) { + $this->recurse_copy($src . '/' . $file,$dst . '/' . $file); + } + else { + copy($src . '/' . $file,$dst . '/' . $file); + } + } + } + closedir($dir); + } + + public function testGetSignature() + { + $sig = ThemeHandler::GetSignature(APPROOT.'test/application/theme-handler/expected/themes/basque-red/main.css'); + $expect_sig=<<assertEquals($expect_sig,$sig); + } + + public function testGetVarSignature() + { + $sig=<<assertEquals("37c31105548fce44fecca5cb34e455c9",$var_sig); + } + + /** + * @param bool $readFromParamAttributeFromJson + * + * @throws \CoreException + * @dataProvider CompileThemesProviderWithoutCss + */ + public function testCompileThemeWithoutCssFile_FocusOnParamAttribute($readFromParamAttributeFromJson=false) + { + $expectJsonFilePath = APPROOT.'test/application/theme-handler/expected/themes/basque-red/theme-parameters.json'; + $expectedThemeParamJson = file_get_contents($expectJsonFilePath); + $aThemeParameters = json_decode($expectedThemeParamJson, true); + if (is_file($this->jsonThemeParamFile)) + { + unlink($this->jsonThemeParamFile); + } + if (is_file($this->cssPath)) + { + unlink($this->cssPath); + } + + $this->compileCSSServiceMock->expects($this->exactly(1)) + ->method("CompileCSSFromSASS") + ->willReturn("====CSSCOMPILEDCONTENT===="); + + if($readFromParamAttributeFromJson) + { + copy($expectJsonFilePath, $this->jsonThemeParamFile); + ThemeHandler::CompileTheme('basque-red', true, null, array($this->tmpDir.'/branding/themes/'), $this->tmpDir); + } + else + { + ThemeHandler::CompileTheme('basque-red', true, $aThemeParameters, array($this->tmpDir.'/branding/themes/'), $this->tmpDir); + } + $this->assertTrue(is_file($this->cssPath)); + $this->assertEquals($expectedThemeParamJson, file_get_contents($this->jsonThemeParamFile)); + $this->assertEquals(file_get_contents(APPROOT . 'test/application/theme-handler/expected/themes/basque-red/main.css'), file_get_contents($this->cssPath)); + } + + public function CompileThemesProviderWithoutCss() + { + return array( + "pass ParamAttributes and Save them in Json" => array(false), + "use them from saved json" => array(true) + ); + } + + /** + * @param $ThemeParametersJson + * + * @param int $CompileCount + * + * @throws \CoreException + * @dataProvider CompileThemesProviderEmptyArray + */ + public function testCompileThemesEmptyArray($ThemeParametersJson, $CompileCount=0) + { + $cssPath = $this->tmpDir . '/branding/themes/basque-red/main.css'; + copy(APPROOT . 'test/application/theme-handler/expected/themes/basque-red/main.css', $cssPath); + + $this->compileCSSServiceMock->expects($this->exactly($CompileCount)) + ->method("CompileCSSFromSASS") + ->willReturn("====CSSCOMPILEDCONTENT===="); + + ThemeHandler::CompileTheme('basque-red', true, json_decode($ThemeParametersJson, true), array($this->tmpDir.'/branding/themes/'), $this->tmpDir); + } + + public function CompileThemesProviderEmptyArray() + { + $emptyImports = '{"variables":{"brand-primary":"#C53030","hover-background-color":"#F6F6F6","icons-filter":"grayscale(1)","search-form-container-bg-color":"#4A5568"},"imports":[],"stylesheets":{"jqueryui":"..\/css\/ui-lightness\/jqueryui.scss","main":"..\/css\/light-grey.scss"}}'; + $emptyStyleSheets='{"variables":{"brand-primary":"#C53030","hover-background-color":"#F6F6F6","icons-filter":"grayscale(1)","search-form-container-bg-color":"#4A5568"},"imports":{"css-variables":"..\/css\/css-variables.scss"},"stylesheets":[]}'; + $emptyVars='{"variables":[],"imports":{"css-variables":"..\/css\/css-variables.scss"},"stylesheets":{"jqueryui":"..\/css\/ui-lightness\/jqueryui.scss","main":"..\/css\/light-grey.scss"}}'; + return array( + "empty imports" => array($emptyImports), + "empty styles" => array($emptyStyleSheets), + "empty vars" => array($emptyVars, 1), + ); + } + + + /** + * @param $ThemeParametersJson + * @param $CompileCSSFromSASSCount + * @param int $missingFile + * @param int $filesTouchedRecently + * @param int $fileMd5sumModified + * @param null $fileToTest + * + * @param null $expected_maincss_path + * + * @throws \CoreException + * @dataProvider CompileThemesProvider + */ + public function testCompileThemes($ThemeParametersJson, $CompileCSSFromSASSCount, $missingFile=0, $filesTouchedRecently=0, $fileMd5sumModified=0, $fileToTest=null, $expected_maincss_path=null, $bSetup=true) + { + $fileToTest=$this->tmpDir.'/'.$fileToTest; + $cssPath = $this->tmpDir . '/branding/themes/basque-red/main.css'; + copy(APPROOT . 'test/application/theme-handler/expected/themes/basque-red/main.css', $cssPath); + + if ($missingFile==1) + { + unlink($fileToTest); + } + + if ($filesTouchedRecently==1) + { + sleep(1); + touch($fileToTest); + } + + if ($fileMd5sumModified==1) + { + sleep(1); + file_put_contents($fileToTest, "###\n".file_get_contents($fileToTest)); + } + + $this->compileCSSServiceMock->expects($this->exactly($CompileCSSFromSASSCount)) + ->method("CompileCSSFromSASS") + ->willReturn("====CSSCOMPILEDCONTENT===="); + + ThemeHandler::CompileTheme('basque-red', $bSetup, json_decode($ThemeParametersJson, true), array($this->tmpDir.'/branding/themes/'), $this->tmpDir); + + if ($CompileCSSFromSASSCount==1) + { + $this->assertEquals(file_get_contents(APPROOT . $expected_maincss_path), file_get_contents($cssPath)); + } + } + + /** + * @return array + */ + public function CompileThemesProvider() + { + $modifiedVariableThemeParameterJson='{"variables":{"brand-primary1":"#C53030","hover-background-color":"#F6F6F6","icons-filter":"grayscale(1)","search-form-container-bg-color":"#4A5568"},"imports":{"css-variables":"..\/css\/css-variables.scss"},"stylesheets":{"jqueryui":"..\/css\/ui-lightness\/jqueryui.scss","main":"..\/css\/light-grey.scss"}}'; + $initialThemeParamJson='{"variables":{"brand-primary":"#C53030","hover-background-color":"#F6F6F6","icons-filter":"grayscale(1)","search-form-container-bg-color":"#4A5568"},"imports":{"css-variables":"..\/css\/css-variables.scss"},"stylesheets":{"jqueryui":"..\/css\/ui-lightness\/jqueryui.scss","main":"..\/css\/light-grey.scss"}}'; + $import_file_path = '/branding/css/css-variables.scss'; + $importmodified_maincss="test/application/theme-handler/expected/themes/basque-red/main_importmodified.css"; + $varchanged_maincss="test/application/theme-handler/expected/themes/basque-red/main_varchanged.css"; + $stylesheet_maincss="test/application/theme-handler/expected/themes/basque-red/main_stylesheet.css"; + $stylesheet_file_path = '/branding/css/light-grey.scss'; + return array( + "setup context: variables list modified without any file touched" => array($modifiedVariableThemeParameterJson, 1,0,0,0,$import_file_path, $varchanged_maincss), + "setup context: variables list modified with files touched" => array($modifiedVariableThemeParameterJson, 1,0,1,0,$import_file_path, $varchanged_maincss, false), + "itop page/theme loading; variables list modified sans touch de fichier" => array($modifiedVariableThemeParameterJson, 0,0,0,0,$import_file_path, $varchanged_maincss, false), + //imports + "import file missing" => array($initialThemeParamJson, 0, 1, 0, 0, $import_file_path), + "import file touched" => array($initialThemeParamJson, 0, 0, 1, 0, $import_file_path), + "import file modified" => array($initialThemeParamJson, 1, 0, 0, 1, $import_file_path, $importmodified_maincss), + //stylesheets + "stylesheets file missing" => array($initialThemeParamJson, 0, 1, 0, 0, $stylesheet_file_path), + "stylesheets file touched" => array($initialThemeParamJson, 0, 0, 1, 0, $stylesheet_file_path), + "stylesheets file modified" => array($initialThemeParamJson, 1, 0, 0, 1, $stylesheet_file_path, $stylesheet_maincss) + ); + } + + /** + * @param $xmlDataCusto + * @dataProvider providePrecompiledStyleSheets + * @throws \Exception + */ + public function testValidatePrecompiledStyles($xmlDataCusto) + { + echo "=== datamodel custo: $xmlDataCusto\n"; + $oDom = new MFDocument(); + $oDom->load($xmlDataCusto); + /**DOMNodeList **/$oThemeNodes=$oDom->GetNodes("/itop_design/branding/themes/theme"); + $this->assertNotNull($oThemeNodes); + + // Parsing themes from DM + foreach($oThemeNodes as $oTheme) + { + $sPrecompiledStylesheet = $oTheme->GetChildText('precompiled_stylesheet', ''); + if (empty($sPrecompiledStylesheet)) + { + continue; + } + + $sThemeId = $oTheme->getAttribute('id'); + + echo "=== theme: $sThemeId ===\n"; + $precompiledSig= ThemeHandler::GetSignature(dirname(__FILE__)."/../../datamodels/2.x/".$sPrecompiledStylesheet); + echo " precompiled signature: $precompiledSig\n"; + $this->assertFalse(empty($precompiledSig), "Signature in precompiled theme '".$sThemeId."' is not retrievable (cf precompiledsheet $sPrecompiledStylesheet / datamodel $xmlDataCusto)"); + + $aThemeParameters = array( + 'variables' => array(), + 'imports' => array(), + 'stylesheets' => array(), + 'precompiled_stylesheet' => '', + ); + + $aThemeParameters['precompiled_stylesheet'] = $sPrecompiledStylesheet; + /** @var \DOMNodeList $oVariables */ + $oVariables = $oTheme->GetNodes('variables/variable'); + foreach($oVariables as $oVariable) + { + $sVariableId = $oVariable->getAttribute('id'); + $aThemeParameters['variables'][$sVariableId] = $oVariable->GetText(); + } + + /** @var \DOMNodeList $oImports */ + $oImports = $oTheme->GetNodes('imports/import'); + foreach($oImports as $oImport) + { + $sImportId = $oImport->getAttribute('id'); + $aThemeParameters['imports'][$sImportId] = $oImport->GetText(); + } + + /** @var \DOMNodeList $oStylesheets */ + $oStylesheets = $oTheme->GetNodes('stylesheets/stylesheet'); + foreach($oStylesheets as $oStylesheet) + { + $sStylesheetId = $oStylesheet->getAttribute('id'); + $aThemeParameters['stylesheets'][$sStylesheetId] = $oStylesheet->GetText(); + } + $compiled_json_sig = ThemeHandler::ComputeSignature($aThemeParameters, array(APPROOT.'datamodels')); + echo " current signature: $compiled_json_sig\n"; + $this->assertEquals($precompiledSig, $compiled_json_sig, "Precompiled signature does not match currently compiled one on theme '".$sThemeId."' (cf precompiledsheet $sPrecompiledStylesheet / datamodel $xmlDataCusto)"); + } + + } + + public function providePrecompiledStyleSheets() + { + $datamodelfiles=glob(dirname(__FILE__)."/../../datamodels/2.x/**/datamodel*.xml"); + $test_set = array(); + + foreach ($datamodelfiles as $datamodelfile) + { + if (is_file($datamodelfile) && + $datamodelfile=="/var/www/html/iTop/test/application/../../datamodels/2.x/itop-config-mgmt/datamodel.itop-config-mgmt.xml") + { + $content=file_get_contents($datamodelfile); + if (strpos($content, "precompiled_stylesheet")!==false) + { + $test_set[$datamodelfile]=array($datamodelfile); + } + } + } + + return $test_set; + } + +} \ No newline at end of file diff --git a/test/application/theme-handler/expected/css/css-variables.scss b/test/application/theme-handler/expected/css/css-variables.scss new file mode 100644 index 000000000..524c792b0 --- /dev/null +++ b/test/application/theme-handler/expected/css/css-variables.scss @@ -0,0 +1,126 @@ +/*! + * Copyright (C) 2013-2020 Combodo SARL + * + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + */ + +// Beware the version number MUST be enclosed with quotes otherwise v2.3.0 becomes v2 0.3 .0 +$version: "v2.7.0-1"; +$approot-relative: "../../../../../" !default; // relative to env-***/branding/themes/***/main.css + +// Base colors +$gray-base: #000 !default; +$gray-darker: lighten($gray-base, 13.5%) !default; // #222 +$gray-dark: #444 !default; +$gray: #777 !default; +$gray-light: #808080 !default; +$gray-lighter: #ddd !default; +$gray-extra-light: #F1F1F1 !default; + +$white: #FFFFFF !default; + +$combodo-orange: #EA7D1E !default; +$combodo-dark-gray: #585653 !default; + +$combodo-orange-dark: darken($combodo-orange, 13.8%) !default; +$combodo-orange-darker: darken($combodo-orange, 18%) !default; +$combodo-dark-gray-dark: darken($combodo-dark-gray, 13.5%) !default; +$combodo-dark-gray-darker: darken($combodo-dark-gray, 18%) !default; + +// Brand colors +// - Bases +$brand-primary: $combodo-orange !default; +$brand-secondary: $combodo-dark-gray !default; +// - Shades +$brand-primary-lightest: lighten($brand-primary, 15%) !default; +$brand-primary-lighter: lighten($brand-primary, 10%) !default; +$brand-primary-light: lighten($brand-primary, 6%) !default; +$brand-primary-dark: darken($brand-primary, 6%) !default; +$brand-primary-darker: darken($brand-primary, 10%) !default; +$brand-primary-darkest: darken($brand-primary, 15%) !default; +$brand-secondary-lightest: lighten($brand-secondary, 15%) !default; +$brand-secondary-lighter: lighten($brand-secondary, 10%) !default; +$brand-secondary-light: lighten($brand-secondary, 6%) !default; +$brand-secondary-dark: darken($brand-secondary, 6%) !default; +$brand-secondary-darker: darken($brand-secondary, 10%) !default; +$brand-secondary-darkest: darken($brand-secondary, 15%) !default; + +// Vars +$highlight-color: $brand-primary !default; +$grey-color: #555555 !default; +$complement-color: #1c94c4 !default; +$complement-light: #d6e8ef !default; +$frame-background-color: $gray-extra-light !default; +$text-color: #000 !default; +$box-radius: 0px !default; +$box-shadow-regular: 0 1px 1px rgba(0, 0, 0, 0.15) !default; +$body-background-color : $white !default; + +$hyperlink-color: $complement-color !default; +$hyperlink-text-decoration: none !default; + +//////////// +// Search // +$search-form-container-color: $white !default; +$search-form-container-bg-color: $complement-color !default; +// +$search-criteria-box-color: #2D2D2D !default; +$search-criteria-box-picto-color: $brand-primary !default; +$search-criteria-box-bg-color: #EEEEEE !default; +$search-criteria-box-hover-color: $white !default; +$search-criteria-box-border-color: #CCCCCC !default; +$search-criteria-box-border: 1px solid $search-criteria-box-border-color !default; +$search-criteria-box-radius: 1px !default; +$search-criteria-more-less-details-color: #3F7294 !default; +// +$search-add-criteria-box-color: $search-criteria-box-color !default; +$search-add-criteria-box-bg-color: $white !default; +$search-add-criteria-box-hover-color: $gray-extra-light !default; +// +$search-button-box-color: $brand-primary !default; +$search-button-box-bg-color: $white !default; +$search-button-box-bg-hover-color: $gray-extra-light !default; + +///////////// +// Buttons // +///////////// +// Toggle button +$toggle-button-bg-color: #CCCCCC !default; +$toggle-button-bg-checked-color: $brand-primary !default; +$toggle-button-slider-bg-color: #FFFFFF !default; + +// Console elements +$summary-details-background: $grey-color !default; +$main-header-background: $frame-background-color !default; +$table-even-background: $frame-background-color !default; +$table-hover-background: #fdf5d0 !default; +$popup-menu-highlight-color: $highlight-color !default; +$popup-menu-text-color: #000 !default; +$popup-menu-background-color: #fff !default; +$popup-menu-text-higlight-color: #fff !default; +$breadcrumb-color: $grey-color !default; +$breadcrumb-highlight-color: $highlight-color !default; + +// jQuery UI widgets vars +$primary-text-color: #333333 !default; +$secondary-text-color: $grey-color !default; +$error-text-color: $white !default; +$highlight-text-color: #363636 !default; +$hover-background-color: #fde17c !default; +$border-highlight-color: $brand-primary-dark !default; +$highlight-item-color: $white !default; +$content-color: #eeeeee !default; +$default-font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif !default; +$icons-filter: hue-rotate(0deg) !default; diff --git a/test/application/theme-handler/expected/css/light-grey.scss b/test/application/theme-handler/expected/css/light-grey.scss new file mode 100644 index 000000000..864323835 --- /dev/null +++ b/test/application/theme-handler/expected/css/light-grey.scss @@ -0,0 +1,3893 @@ +/*! + * Copyright (C) 2013-2020 Combodo SARL + * + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + */ + +//@import 'css-variables.scss'; + +/* CSS Document */ +body { + font-family: Tahoma, Verdana, Arial, Helvetica; + font-size: 10pt; + background-color: $body-background-color; + color: $text-color; + margin: 0; /* Remove body margin/padding */ + padding: 0; + overflow: hidden; /* Remove scroll bars on browser window */ +} + +body.printable-version { + margin:1.5em; + overflow:auto; +} + +/* to prevent flicker, hide the pane's content until it's ready */ +.ui-layout-center, .ui-layout-north, .ui-layout-south { + display: none; +} +.ui-layout-content { + padding-left: 10px; +} +.ui-layout-content .ui-tabs-nav li { + /* Overriding jQuery UI theme to see active tab better */ + margin-bottom: 2px; + + &.ui-tabs-active{ + padding-bottom: 3px; + } +} + +.raw_output { + font-family: Courier-New, Courier, Arial, Helvetica; + font-size: 8pt; + background-color: #eeeeee; + color: $text-color; + border: 1px dashed $text-color; + padding: 0.25em; + margin-top: 1em; +} + +h1 { + font-family: Tahoma, Verdana, Arial, Helvetica; + color: $text-color; + font-weight: bold; + font-size: 12pt; +} +h2 { + font-family: Tahoma, Verdana, Arial, Helvetica; + color: $text-color; + font-weight: normal; + font-size: 12pt; +} +h3 { + font-family: Tahoma, Verdana, Arial, Helvetica; + color: $text-color; + font-weight: normal; + font-size: 10pt; +} + +label { + cursor: pointer; +} + +.hilite, .hilite a, .hilite a:visited { + color: $highlight-color; + text-decoration: none; +} +table.datatable { + width: 100%; + border: 0; + padding: 0; +} +td.menucontainer { + text-align: right; +} +table.listResults { + padding: 0px; + border-top: 3px solid $frame-background-color; + border-left: 3px solid $frame-background-color; + border-bottom: 3px solid #e6e6e1; + border-right: 3px solid #e6e6e1; + width: 100%; + background-color: $white; +} + +table.listResults td { + padding: 2px; +} + +table.listResults td .view-image { + // Counteract the forced dimensions (usefull for displaying in the details view) + width: inherit !important; + height: inherit !important; + img { + max-width: 48px !important; + max-height: 48px !important; + display: block; + margin-left: auto; + margin-right: auto; + } +} + +table.listResults > tbody > tr.selected > * { +} + +table.listResults > tbody > tr > * { + transition: background-color 400ms linear; +} + +table.listResults > tbody > tr:hover > * { + cursor: pointer; +} + +table.listResults > tbody > tr.selected:hover > * { + /* hover on lines is currently done toggling td.hover, and having a rule for links */ + background-color: $brand-primary-lightest; + color: $text-color; +} + +table.listResults > tbody > tr:hover > * { + /* hover on lines is currently done toggling td.hover, and having a rule for links */ + background-color: $table-hover-background; + color: $text-color; +} + +.edit-image { + .view-image { + display: inline-block; + + img[src=""], + img[src="null"] { + // Hiding "broken" image when src is not set + visibility: hidden; + } + + &.dirty { + // The image will be modified when saving the changes + + &.compat { + // Browser not supporting FileReader + background-image: url($approot-relative + "css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png?v=" + $version); + img { + opacity: 0.3; + } + } + } + } + + .edit-buttons { + display: inline-block; + vertical-align: top; + margin-top: 4px; + margin-left: 3px; + + .button { + cursor: pointer; + margin-bottom: 3px; + padding: 2px; + background-color: $highlight-color; + + &.disabled { + cursor: default; + background-color: $grey-color; + opacity: 0.3; + } + .ui-icon { + background-image: url($approot-relative + "css/ui-lightness/images/ui-icons_ffffff_256x240.png?v=" + $version); + } + } + } + + .file-input { + display: block; + } +} + +/* Center the image both horizontally and vertically, withing a box which size is fixed (depends on the attribute definition) */ +.details .view-image { + text-align: center; + padding: 2px; + border: 2px solid $gray-lighter; + border-radius: 6px; + + img { + display: inline-block; + vertical-align: middle; + max-width: 90% !important; + max-height: 90% !important; + cursor: zoom-in; + } + .helper-middle { + // Helper to center the image (requires a span dedicated to this) + display: inline-block; + height: 100%; + vertical-align: middle; + } +} + +table.listContainer { + border: 0; + padding: 0; + margin: 0; + width: 100%; + clear: both; +} + +tr.containerHeader, tr.containerHeader td { + background: transparent; +} + +tr.even td, .wizContainer tr.even td { + background-color: $table-even-background; +} +tr.red_even td, .wizContainer tr.red_even td { + background-color: #f97e75; + color: $white; +} +tr.red td, .wizContainer tr.red td { + background-color: #f9a397; + color: $white; +} +tr.orange_even td, .wizContainer tr.orange_even td { + background-color: #f4d07a; +} +tr.orange td, .wizContainer tr.orange td { + background-color: #f4e96c; +} +tr.green_even td, .wizContainer tr.green_even td { + background-color: #bee5a3; +} +tr.green td, .wizContainer tr.green td { + background-color: #b3e5b4; +} + +tr td.hover, tr.even td.hover, .hover a, .hover a:visited, .hover a:hover, .wizContainer tr.even td.hover, .wizContainer tr td.hover { + //background-color: #fdf5d0; + color: $text-color; +} + +th { + font-family: Tahoma, Verdana, Arial, Helvetica; + font-size: 8pt; + color: $complement-color; + height:20px; + background: $frame-background-color bottom repeat-x; +} + +th.header { + cursor: pointer; + background-repeat: no-repeat; + background-position: center right; + background-repeat: no-repeat; + padding-right: 16px; // some space for the asc/desc icons +} + +th.headerSortUp { + text-decoration: underline; + cursor: pointer; + padding-right: 5px; + + &::after { + font-family: "Font Awesome 5 Free"; + text-align: right; + content: '\f0d7'; + color: $complement-color; + float: right; + } +} + +th.headerSortDown { + text-decoration: underline; + cursor: pointer; + padding-right: 5px; + + &::after { + font-family: "Font Awesome 5 Free"; + text-align: right; + content: '\f0d8'; + color: $complement-color; + float: right; + } +} + +td { + font-family: Tahoma, Verdana, Arial, Helvetica; + font-size: 12px; + color:#696969; + nobackground-color: #ffffff; + padding: 0px; +} + +tr.clicked td { + font-family: Tahoma, Verdana, Arial, Helvetica; + font-size: smaller; + background-color: #ffcfe8; +} + +td.label { + vertical-align: top; +} +td.label span { + font-family: Tahoma, Verdana, Arial, Helvetica; + font-size: 12px; + color: $text-color; + padding: 5px; + padding-right: 10px; + font-weight:bold; + vertical-align: top; + text-align: right; + display: block; +} +fieldset td.label span { + padding: 3px; + padding-right: 10px; +} +fieldset { + margin-top: 3px; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + border-radius: 6px; + border-style: solid; + border-color: #ddd; +} + +legend { + font-family: Tahoma, Verdana, Arial, Helvetica; + font-size: 12px; + padding:8px; + color: #fff; + background-color: $complement-color; + font-weight: bold; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + border-radius: 6px; +} +legend.transparent { + background: transparent; + color: #333333; + font-size: 1em; + font-weight: normal; + padding: 0; +} +.ui-widget-content td legend a, .ui-widget-content td legend a:hover, .ui-widget-content td legend a:visited { + color: #fff; +} + +.ui-widget-content td a, p a, p a:visited, td a, td a:visited { + text-decoration:none; + color: $complement-color; +} +.ui-widget-content td a.cke_button, .ui-widget-content td a.cke_toolbox_collapser, .ui-widget-content td a.cke_combo_button, cke_dialog a { + padding-left: 0; + background-image: none; +} + +.ui-widget-content td a:hover, p a:hover, td a:hover { + text-decoration:underline; + color:$highlight-color; +} +.cke_reset_all *:hover { + text-decoration: none; + color: $text-color; +} +table.cke_dialog_contents a.cke_dialog_ui_button_ok { + color: $text-color; + border-color: $highlight-color; + background: $highlight-color; +} +.cke_notifications_area { + display: none; +} +.hljs { + padding: 0.9em !important; + box-shadow: 0 0px 3px 2px inset rgba(0, 0, 0, 0.4); + border-radius: 3px; +} +td a.no-arrow, td a.no-arrow:visited, .SearchDrawer a.no-arrow, .SearchDrawer a.no-arrow:visited { + text-decoration:none; + color: $text-color; + padding-left:0px; + background: inherit; +} +td a.no-arrow:hover { + text-decoration:underline; + color:#d81515; + padding-left:0px; + background: inherit; +} +td a, +td a:visited{ + &:hover{ + .text_decoration{ + color: darken($highlight-color, 6%); + } + } + + .text_decoration{ + vertical-align: baseline; + text-decoration: none; + color: $highlight-color; + margin-right: 8px; + transition: color 0.2s ease-in-out; + } +} + +a.small_action { + font-family: Tahoma, Verdana, Arial, Helvetica; + font-size: 8pt; + color: $text-color; + text-decoration:none; +} +.display_block { + padding:0.25em; +} +.actions_details { + float:right; + margin-top:10px; + margin-right:10px; + padding-left: 5px; + padding-top: 2px; + padding-bottom: 2px; + background: $highlight-color; +} +.actions_details span{ + background: url($approot-relative + "images/actions_right.png?v=" + $version) no-repeat right; + color: #fff; + font-weight: bold; + padding-top: 2px; + padding-bottom: 2px; + padding-right: 12px; +} +.actions_details a { + text-decoration:none; +} +.loading { + noborder: 1px dashed #CCC; + background: #b9c1c8; + padding:0.25em; +} + +input.textSearch { + border:1px solid $gray-base; + font-family:Tahoma,Verdana, Arial, Helvetica, sans-serif; + font-size: 12px; + color: $text-color; +} + +.ac_input { + border: 1px solid #7f9db9; + background: #fff url($approot-relative + "images/ac-background.gif?v=" + $version) no-repeat right; +} +/* By Rom */ +.csvimport_createobj { + color: #AA0000; + background-color:#EEEEEE; +} +.csvimport_error { + font-weight: bold; + color: #FF0000; + background-color:#EEEEEE; +} +.csvimport_warning { + color: #CC8888; + background-color:#EEEEEE; +} +.csvimport_ok { + color: $text-color; + background-color:#BBFFBB; +} +.csvimport_reconkey { + font-style: italic; + color: #888888; + background-color: $white; +} +.csvimport_extreconkey { + color: #888888; + background-color: $white; +} +#accordion { + display:none; +} + +#accordion h3 { + padding: 10px; +} + +.ui-accordion-content ul { + list-style:none; + list-style-image: none; + padding-left:16px; + margin-top: 8px; +} + +.ui-accordion-content li.submenu { + margin-top: 8px; +} + +.ui-accordion-content ul ul { + padding: 8px 0px 8px 8px; + margin:0; + list-style:none; + list-style-image: none; + border: 0; +} + +.nothing { + noborder-top: 1px solid #8b8b8b; + padding: 4px 0px 0px 16px; + font-size:8pt; + background: url($approot-relative + "images/green-square.gif?v=" + $version) no-repeat bottom left; + color:#83b217; + font-weight:bold; + text-decoration:none; +} +div.ui-accordion-content { + padding-top: 10px; + padding-left: 10px; +} +.ui-accordion-content a, ui-accordion-content a:visited { + color: $complement-color; + text-decoration:none; +} + +.ui-accordion-content a:hover { + color: $highlight-color; + text-decoration: none; +} + +.ui-accordion-content ul { + padding-left: 0; + margin-top: 0; +} + +.ui-accordion-content li { + color: $grey-color; + text-decoration: none; + margin: 0; + padding: 0px 0pt 0px 6px; + font-size: 9pt; + font-weight: normal; + border: 0; + + &::before { + font-family: "Font Awesome 5 Free"; + content: "\f0da"; + color: $highlight-color; + font-weight: 900; + margin-right: 6px; + font-size: 10px; + } +} + +td a.CollapsibleLabel, a.CollapsibleLabel{ + margin: 0; + padding: 0; + font-size:8pt; + text-decoration:none; + color:$grey-color; +} +td a.CollapsibleLabel::before, a.CollapsibleLabel::before{ + font-family: "Font Awesome 5 Free"; + color: $highlight-color; + content: "\f0da"; // caret-right + font-weight: 900; + margin: 0 5px; + font-size: 10px; +} + + /* Beware: IE6 does not support multiple selector with multiple classes, only the last class is used */ +td a.CollapsibleLabel.open, a.CollapsibleLabel.open{ + margin: 0; + padding: 0px 0pt 0px 16px; + font-size:8pt; + text-decoration:none; + color: $highlight-color; +} +td a.CollapsibleLabel.open::before, a.CollapsibleLabel.open::before { + font-family: "Font Awesome 5 Free"; + color: $highlight-color; + content: '\f0d7'; // caret-down + margin-right: 6px; + font-size: 10px; +} + +.page_header { + background-color:$frame-background-color; + padding:5px; +} +/* move up a header immediately following a display block (i.e. "actions" menu) */ +.display_block + .page_header { + margin-top: -8px; +} + +.notreeview li { background: url($approot-relative + "images/tv-item.gif?v=" + $version) 0 0 no-repeat; } +.notreeview .collapsable { background-image: url($approot-relative + "images/tv-collapsable.gif?v=" + $version); } +.notreeview .expandable { background-image: url($approot-relative + "images/tv-expandable.gif?v=" + $version); } +.notreeview .last { background-image: url($approot-relative + "images/tv-item-last.gif?v=" + $version); } +.notreeview .lastCollapsable { background-image: url($approot-relative + "images/tv-collapsable-last.gif?v=" + $version); } +.notreeview .lastExpandable { background-image: url($approot-relative + "images/tv-expandable-last.gif?v=" + $version); } + +#OrganizationSelection { + padding:5px 0px 16px 20px; +} + +/* popup menus */ +div.itop_popup { + margin: 0; + padding: 0; + float:right; +} +div.itop_popup > ul > li { + list-style: none; + cursor: pointer; + position: relative; +} + +div.actions_menu > ul { + height:19px; + line-height: 17px; + vertical-align: middle; + display:block; + nowidth:70px; /* Nasty work-around for IE... en attendant mieux */ + padding-left: 5px; + background: $highlight-color; + cursor: pointer; + margin: 0; +} + +div.actions_menu > ul > li { + float: left; + list-style: none; + font-size: 11px; + font-family: Tahoma,sans-serif; + height: 17px; + padding-right: 10px; + padding-left: 4px; + font-weight: bold; + color: $popup-menu-text-higlight-color; + vertical-align: middle; + margin: 0; +} + +div.actions_menu > ul > li > i { + margin-left: 5px; +} + +#logOffBtn > ul > li { + list-style: none; + vertical-align: middle; + margin: 0; + padding-left: 10px; + padding-right: 10px; + cursor: pointer; +} +#logOffBtn > ul { + list-style: none; + vertical-align: middle; + margin: 0; + padding: 0; + height: 25px; +} +.itop_popup > ul > li > ul, #logOffBtn > ul > li > ul { + box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.5); +} +.itop_popup li a, #logOffBtn li a { + display: block; + padding: 5px 12px; + text-decoration: none; + nowidth: 70px; + color: $popup-menu-text-color; + font-weight: bold; + white-space: nowrap; + background: $popup-menu-background-color; +} + +#logOffBtn li span { + display: block; + padding: 5px 12px; + text-decoration: none; + nowidth: 70px; + color: $popup-menu-text-color; + white-space: nowrap; + background: $popup-menu-background-color; +} +.itop_popup ul { + padding-left: 0; +} + +.menucontainer div.toolkit_menu { + background: $highlight-color; + margin-left: 10px; + > ul > li + { + float: left; + list-style: none; + font-size: 11px; + font-family: Tahoma, sans-serif; + height: 19px; + padding-right: 4px; + padding-left: 6px; + font-weight: bold; + color:#fff; + vertical-align: middle; + line-height: 17px; + margin: 0; + > i:nth-child(1) { + font-size: 13px; + padding-top: 3px; + } + > i:nth-child(2) { + margin-left: 5px; + } + } +} + + +.itop_popup li a:hover, #logOffBtn li a:hover { + background: #1A4473; +} + +.itop_popup ul > li > ul, #logOffBtn ul > li > ul +{ + border: 1px solid black; + background: #fff; +} + +.itop_popup li > ul, #logOffBtn li > ul +{ margin: 0; + padding: 0; + position: absolute; + display: none; + border-top: 1px solid white; + z-index: 1500; +} + +.itop_popup li ul li, #logOffBtn li ul li { + float: none; + display: inline; +} + +.itop_popup li ul li a, #logOffBtn li ul li a { + width: auto; + text-align: left; +} + +.itop_popup li ul li a:hover, #logOffBtn li ul li a:hover{ + background: $popup-menu-highlight-color; + color: $popup-menu-text-higlight-color; + font-weight: bold; +} +.itop_popup > ul { + margin: 0; +} +hr.menu-separator { + border: none 0; + border-top: 1px solid #ccc; + color: #ccc; + background-color: transparent; + height: 1px; + margin: 3px; + cursor: default; +} +/************************************/ +.wizHeader { + background: $complement-color; + padding: 15px; +} +.wizContainer { + border: 5px solid $complement-color; + background: $complement-light; + padding: 5px; +} + +.wizContainer table tr td { + background: transparent; +} +.alignRight { + text-align: right; + padding: 3px; +} + +.alignLeft { + text-align: left; + padding: 3px; +} + +.red { + background-color: #ff6000; + color: $text-color; +} + +th.red { + background: url($approot-relative + "images/red-header.gif?v=" + $version) bottom left repeat-x; + color: $text-color; +} + +.green { + background-color: #00cc00; + color: $text-color; +} +th.green { + background: url($approot-relative + "images/green-header.gif?v=" + $version) bottom left repeat-x; + color: $text-color; +} + +.orange { + background-color: #ffde00; + color: $text-color; +} + +th.orange { + background: url($approot-relative + "images/orange-header.gif?v=" + $version) bottom left repeat-x; + color: $text-color; +} + +/* For Date Picker: Creates a little calendar icon + * instead of a text link for "Choose date" + */ +td a.dp-choose-date, a.dp-choose-date, td a.dp-choose-date:hover, a.dp-choose-date:hover, td a.dp-choose-date:visited, a.dp-choose-date:visited { + float: left; + width: 16px; + height: 16px; + padding: 0; + margin: 5px 3px 0; + display: block; + text-indent: -2000px; + overflow: hidden; + background: url($approot-relative + "images/calendar.png?v=" + $version) no-repeat; +} +td a.dp-choose-date.dp-disabled, a.dp-choose-date.dp-disabled { + background-position: 0 -20px; + cursor: default; +} +/* For Date Picker: makes the input field shorter once the date picker code + * has run (to allow space for the calendar icon) + */ +input.dp-applied { + width: 140px; + float: left; +} + +/* For search forms */ +.mini_tabs a { + text-decoration: none; + font-weight:bold; + color: #ccc; + background-color:#333; + padding-left: 1em; + padding-right: 1em; + padding-bottom: 0.25em; +} +.mini_tabs a.selected { + color: #fff; + background-color: $complement-color; + padding-top: 0.25em; +} +.mini_tabs ul { + margin: -10px; +} +.mini_tabs ul li { + float: right; + list-style: none; + nopadding-left: 1em; + nopadding-right: 1em; + margin-top: 0; +} +/* Search forms v2 */ +.search_box{ + box-sizing: border-box; + position: relative; + z-index: 1100; /* To be over the table block/unblock UI. Not very sure about this. */ + text-align: center; /* Used when form is closed */ + + /* Sizing reset */ + *{ + box-sizing: border-box; + } +} +.search_form_handler{ + position: relative; + z-index: 10; + width: 100%; + margin-left: auto; + margin-right: auto; + font-size: 12px; + text-align: left; /* To compensate .search_box:text-align */ + border: 1px solid $search-form-container-bg-color; + //transition: width 0.3s ease-in-out; + + /* Sizing reset */ + *{ + box-sizing: border-box; + } + /* Hyperlink reset */ + a{ + color: inherit; + text-decoration: none; + } + /* Input reset */ + input[type="text"], + select{ + padding: 1px 2px; + } + + &:not(.closed){ + .sf_title{ + .sft_short{ + display: none; + } + + .sft_hint, + .sfobs_hint, + .sft_toggler{ + margin-top: 4px; + } + .sft_toggler{ + transform: rotateX(180deg); + transition: transform 0.5s linear; + } + } + } + &.closed{ + margin-bottom: 0.5em; + width: 150px; + overflow: hidden; + border-radius: 4px; + background-color: $complement-color; + + .sf_criterion_area{ + height: 0; + opacity: 0; + padding: 0; + } + .sf_title { + padding: 6px 8px; + text-align: center; + font-size: 12px; + + .sft_long{ + display: none; + } + .sft_hint, + .sfobs_hint{ + display: none; + } + } + } + + &:not(.no_auto_submit){ + .sft_hint{ + display: none; + } + .sfc_fg_apply{ + display: none; + } + } + &.no_auto_submit{ + .sfc_fg_search{ + display: none; + } + .sft_hint{ + display: inline-block; + } + } + + + &:not(.hide_obsolete_data){ + .sfobs_hint{ + display: none; + } + } + &.hide_obsolete_data{ + .sfobs_hint{ + display: inline-block; + } + } + &.hide_obsolete_data.no_auto_submit{ + .sfobs_hint{ + margin-left: 30px; + } + + } + + .sf_title{ + transition: opacity 0.3s, background-color 0.3s, color 0.3s linear; + padding: 8px 10px; + margin: 0; + color: $search-form-container-color; + background-color: $search-form-container-bg-color; + cursor: pointer; + .sft_hint, + .sfobs_hint{ + font-size: 8pt; + font-style: italic; + } + .sft_toggler{ + margin-left: 0.7em; + transition: color 0.2s ease-in-out, transform 0.4s ease-in-out; + + &:hover{ + color: $gray-extra-light; + } + } + } + .sf_message{ + display: none; + margin: 8px 8px 0px 8px; + border-radius: 0px; + } + .sf_criterion_area{ + /*display: none;*/ + padding: 8px 8px 3px 8px; /* padding-bottom must equals to padding-top - .search_form_criteria:margin-bottom */ + background-color: $white; + + .sf_criterion_row{ + position: relative;; + + &:not(:first-child){ + margin-top: 20px; + + &::before{ + content: ""; + position: absolute; + top: -12px; + left: 0px; + width: 100%; + border-top: 1px solid $search-criteria-box-border-color; + } + &::after{ + content: "or"; /* TODO: Make this into a dict entry */ + position: absolute; + top: -20px; + left: 8px; + padding-left: 5px; + padding-right: 5px; + color: $gray-light; + background-color: $white; /* Must match .sf_criterion_area:background-color */ + } + } + + .sf_criterion_group{ + display: inline; + } + } + + /* Common style between criterion and more criterion */ + .search_form_criteria, + .sf_more_criterion, + .sf_button{ + position: relative; + display: inline-block; + margin-right: 10px; + margin-bottom: 5px; + vertical-align: top; + + &.opened{ + margin-bottom: 0px; /* To compensate the .sfc/.sfm_header:padding-bottom: 13px */ + + .sfc_header, + .sfm_header{ + border-bottom: none !important; + box-shadow: none !important; + padding-bottom: 13px; /* Must be equal to .search_form_criteria:margin-bottom + this:padding-bottom */ + } + } + + > *{ + padding: 7px 8px; + vertical-align: top; + border: $search-criteria-box-border; + border-radius: $search-criteria-box-radius; + box-shadow: $box-shadow-regular; + } + .sfc_form_group, + .sfm_content{ + position: absolute; + z-index: -1; + min-width: 100%; + left: 0px; + margin-top: -1px; + + .sfc_fg_buttons{ + white-space: nowrap; + } + } + } + + /* Criteria tags */ + .search_form_criteria{ + /* Non editable criteria */ + &.locked{ + background-color: $gray-extra-light; + + .sfc_title{ + user-select: none; + cursor: initial; + } + } + /* Draft criteria (modifications not applied) */ + &.draft{ + .sfc_header, + .sfc_form_group{ + border-style: dashed; + } + + .sfc_title{ + font-style: italic; + } + } + /* Opened criteria (form group displayed) */ + &.opened{ + z-index: 1; /* To be over other criterion */ + + .sfc_toggle{ + transform: rotateX(-180deg); + } + .sfc_form_group{ + display: block; + } + } + &.opened_left{ + .sfc_form_group{ + left: auto; + right: 0px; + } + } + + /* Add "and" on criterion but the one and submit button */ + &:not(:last-of-type){ + margin-right: 30px; + + &::after{ + /* TODO: Find an elegant way to do this, without hardcoding the content (could be a