diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index 1607896e9..3516a90f3 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -263,7 +263,6 @@ enum class PropertyId : Uint32 { ListStyleImage = String::hash( "list-style-image" ), Float = String::hash( "float" ), Clear = String::hash( "clear" ), - DataLanguage = String::hash( "data-language" ), // Minor hack Action = String::hash( "action" ), Method = String::hash( "method" ), Enctype = String::hash( "enctype" ), diff --git a/include/eepp/ui/uihtmlwidget.hpp b/include/eepp/ui/uihtmlwidget.hpp index 8da4d9200..5b0a9cf31 100644 --- a/include/eepp/ui/uihtmlwidget.hpp +++ b/include/eepp/ui/uihtmlwidget.hpp @@ -56,6 +56,8 @@ class EE_API UIHTMLWidget : public UILayout { virtual std::vector getPropertiesImplemented() const; + using UIWidget::getPropertyString; + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, const Uint32& state = 0 ) const; @@ -85,6 +87,23 @@ class EE_API UIHTMLWidget : public UILayout { bool isOutOfFlow() const; + bool hasDataProperty( const std::string& name ) const; + + const StyleSheetProperty* getDataProperty( const std::string& name ) const; + + std::string getDataPropertyString( const std::string& name, + const std::string& defaultValue = "" ) const; + + void setDataProperty( const StyleSheetProperty& property ); + + void setDataProperty( const std::string& name, const std::string& value ); + + void removeDataProperty( const std::string& name ); + + const UnorderedMap& getDataProperties() const { + return mDataProperties; + } + protected: CSSDisplay mDisplay{ CSSDisplay::Block }; CSSPosition mPosition{ CSSPosition::Static }; diff --git a/src/eepp/ui/css/stylesheetproperty.cpp b/src/eepp/ui/css/stylesheetproperty.cpp index f2fb0bb9d..4302cb83d 100644 --- a/src/eepp/ui/css/stylesheetproperty.cpp +++ b/src/eepp/ui/css/stylesheetproperty.cpp @@ -13,6 +13,10 @@ namespace EE { namespace UI { namespace CSS { +static bool isDataPropertyName( std::string_view name ) { + return String::istartsWith( String::trim( name ), "data-" ); +} + StyleSheetProperty::StyleSheetProperty() : mSpecificity( 0 ), mVolatile( false ), mImportant( false ) {} @@ -38,7 +42,7 @@ StyleSheetProperty::StyleSheetProperty( const PropertyDefinition* definition, checkVars(); if ( NULL == mShorthandDefinition && NULL == mPropertyDefinition && - !String::startsWith( mName, "-" ) ) { + !String::startsWith( mName, "-" ) && !isDataPropertyName( mName ) ) { Log::warning( "Property \"%s\" is not defined!", mName ); } } @@ -62,7 +66,7 @@ StyleSheetProperty::StyleSheetProperty( bool isVolatile, const PropertyDefinitio checkVars(); if ( NULL == mShorthandDefinition && NULL == mPropertyDefinition && - !String::startsWith( mName, "-" ) ) { + !String::startsWith( mName, "-" ) && !isDataPropertyName( mName ) ) { Log::warning( "Property \"%s\" is not defined!", mName ); } } @@ -89,7 +93,7 @@ StyleSheetProperty::StyleSheetProperty( const std::string& name, const std::stri checkVars(); if ( NULL == mShorthandDefinition && NULL == mPropertyDefinition && - !String::startsWith( mName, "-" ) ) { + !String::startsWith( mName, "-" ) && !isDataPropertyName( mName ) ) { Log::warning( "Property \"%s\" is not defined!", mName ); } } @@ -115,7 +119,8 @@ StyleSheetProperty::StyleSheetProperty( const std::string& name, const std::stri createIndexed(); checkVars(); - if ( NULL == mShorthandDefinition && NULL == mPropertyDefinition ) { + if ( NULL == mShorthandDefinition && NULL == mPropertyDefinition && + !isDataPropertyName( mName ) ) { Log::warning( "Property \"%s\" is not defined!", mName ); } } diff --git a/src/eepp/ui/css/stylesheetselector.cpp b/src/eepp/ui/css/stylesheetselector.cpp index 8319563c5..5caf6503b 100644 --- a/src/eepp/ui/css/stylesheetselector.cpp +++ b/src/eepp/ui/css/stylesheetselector.cpp @@ -55,10 +55,34 @@ void StyleSheetSelector::parseSelector( std::string selector ) { std::string buffer; StyleSheetSelectorRule::PatternMatch curPatternMatch = StyleSheetSelectorRule::ANY; + bool inAttribute = false; + char quote = 0; for ( auto charIt = selector.rbegin(); charIt != selector.rend(); ++charIt ) { char curChar = *charIt; + if ( quote != 0 ) { + buffer = curChar + buffer; + if ( curChar == quote ) + quote = 0; + continue; + } + + if ( inAttribute ) { + buffer = curChar + buffer; + if ( curChar == '"' || curChar == '\'' ) + quote = curChar; + else if ( curChar == '[' ) + inAttribute = false; + continue; + } + + if ( curChar == ']' ) { + inAttribute = true; + buffer = curChar + buffer; + continue; + } + switch ( curChar ) { case StyleSheetSelectorRule::DESCENDANT: addSelectorRule( buffer, curPatternMatch, StyleSheetSelectorRule::DESCENDANT ); diff --git a/src/eepp/ui/css/stylesheetselectorrule.cpp b/src/eepp/ui/css/stylesheetselectorrule.cpp index 3cab395de..12f6aee27 100644 --- a/src/eepp/ui/css/stylesheetselectorrule.cpp +++ b/src/eepp/ui/css/stylesheetselectorrule.cpp @@ -1,5 +1,6 @@ #include #include +#include #include namespace EE { namespace UI { namespace CSS { @@ -41,6 +42,10 @@ static bool isStructuralPseudoClass( const std::string& pseudoClass ) { return false; } +static bool isDataAttributeName( std::string_view name ) { + return String::istartsWith( String::trim( name ), "data-" ); +} + StyleSheetSelectorRule::PseudoClasses StyleSheetSelectorRule::toPseudoClass( std::string_view cls ) { if ( "focus" == cls ) @@ -135,6 +140,8 @@ void StyleSheetSelectorRule::parseFragment( const std::string& selectorFragment mAttributeSelectors.push_back( attr ); mSpecificity += SpecificityClass; + buffer.clear(); + return; } if ( curSelectorType == TAG || curSelectorType == CLASS || curSelectorType == ID ) { @@ -217,6 +224,9 @@ void StyleSheetSelectorRule::parseFragment( const std::string& selectorFragment if ( !mClasses.empty() ) mRequirementFlags |= Class; + if ( !mAttributeSelectors.empty() ) + mRequirementFlags |= Attribute; + if ( mPseudoClasses ) { mRequirementFlags |= PseudoClass; mSpecificity += SpecificityPseudoClass * numberOfSetBits( mPseudoClasses ); @@ -310,28 +320,44 @@ bool StyleSheetSelectorRule::matches( UIWidget* element, const bool& applyPseudo if ( !mAttributeSelectors.empty() ) { for ( const auto& attr : mAttributeSelectors ) { - if ( attr.op != AttributeOperator::None ) { - std::string elVal = element->getPropertyString( attr.name ); + bool attrExists = false; + std::string elValStorage; + const std::string* elVal = &elValStorage; + if ( element->isType( UI_TYPE_HTML_WIDGET ) && isDataAttributeName( attr.name ) ) { + auto* htmlElement = element->asType(); + const auto* property = htmlElement->getDataProperty( attr.name ); + attrExists = property != nullptr; + if ( attrExists ) + elVal = &property->value(); + } else { + elValStorage = element->getPropertyString( attr.name ); + attrExists = !elValStorage.empty(); + } + + if ( !attrExists ) + return false; + + if ( attr.op != AttributeOperator::None ) { switch ( attr.op ) { case AttributeOperator::Exact: // = - if ( elVal != attr.value ) + if ( *elVal != attr.value ) return false; break; case AttributeOperator::StartsWith: // ^= - if ( !String::startsWith( elVal, attr.value ) ) + if ( !String::startsWith( *elVal, attr.value ) ) return false; break; case AttributeOperator::EndsWith: // $= - if ( !String::endsWith( elVal, attr.value ) ) + if ( !String::endsWith( *elVal, attr.value ) ) return false; break; case AttributeOperator::Contains: // *= - if ( elVal.find( attr.value ) == std::string::npos ) + if ( elVal->find( attr.value ) == std::string::npos ) return false; break; case AttributeOperator::ContainsWord: { // ~= (Space-separated word check) - auto words = String::split( elVal, ' ', true ); + auto words = String::split( *elVal, ' ', true ); if ( std::find( words.begin(), words.end(), attr.value ) == words.end() ) { return false; } @@ -339,8 +365,8 @@ bool StyleSheetSelectorRule::matches( UIWidget* element, const bool& applyPseudo } case AttributeOperator::StartsWithDash: // |= (Exact match or starts with value // + "-") - if ( elVal != attr.value && - !String::startsWith( elVal, attr.value + "-" ) ) { + if ( *elVal != attr.value && + !String::startsWith( *elVal, attr.value + "-" ) ) { return false; } break; diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index d38cae58a..dcd00ed00 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -489,8 +489,6 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "display-options", "" ).setType( PropertyType::String ); registerProperty( "menu-width-mode", "" ).setType( PropertyType::String ); - registerProperty( "data-language", "" ).setType( PropertyType::String ); - registerProperty( "action", "" ).setType( PropertyType::String ); registerProperty( "method", "GET" ).setType( PropertyType::String ); registerProperty( "enctype", "application/x-www-form-urlencoded" ) diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index ee1ec9580..85f8fa34e 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -3062,10 +3062,6 @@ bool UICodeEditor::applyProperty( const StyleSheetProperty& attribute ) { case PropertyId::Text: mDoc->textInput( attribute.asString() ); break; - case PropertyId::DataLanguage: - setSyntaxDefinition( - SyntaxDefinitionManager::instance()->findFromString( attribute.asString() ) ); - break; case PropertyId::BackgroundColor: { setBackgroundColor( attribute.asColor() ); updateDynamicTheme(); diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index 7f3ac94ca..f4ae90722 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -7,6 +7,29 @@ namespace EE { namespace UI { +static bool isDataPropertyName( std::string_view name ) { + return String::istartsWith( String::trim( name ), "data-" ); +} + +static bool isNormalizedDataPropertyName( std::string_view name ) { + if ( !String::startsWith( name, "data-" ) || String::trim( name ).size() != name.size() ) + return false; + + for ( auto c : name ) { + if ( c >= 'A' && c <= 'Z' ) + return false; + } + + return true; +} + +static std::string normalizeDataPropertyName( std::string_view name ) { + auto trimmedName = String::trim( name ); + std::string normalizedName( trimmedName ); + String::toLowerInPlace( normalizedName ); + return normalizedName; +} + UIHTMLWidget* UIHTMLWidget::New() { return eeNew( UIHTMLWidget, () ); } @@ -467,4 +490,48 @@ bool UIHTMLWidget::isOutOfFlow() const { return mPosition == CSSPosition::Absolute || mPosition == CSSPosition::Fixed; } +bool UIHTMLWidget::hasDataProperty( const std::string& name ) const { + if ( isNormalizedDataPropertyName( name ) ) + return mDataProperties.find( name ) != mDataProperties.end(); + return mDataProperties.find( normalizeDataPropertyName( name ) ) != mDataProperties.end(); +} + +const StyleSheetProperty* UIHTMLWidget::getDataProperty( const std::string& name ) const { + auto it = isNormalizedDataPropertyName( name ) + ? mDataProperties.find( name ) + : mDataProperties.find( normalizeDataPropertyName( name ) ); + return it != mDataProperties.end() ? &it->second : nullptr; +} + +std::string UIHTMLWidget::getDataPropertyString( const std::string& name, + const std::string& defaultValue ) const { + const StyleSheetProperty* property = getDataProperty( name ); + return property ? property->value() : defaultValue; +} + +void UIHTMLWidget::setDataProperty( const StyleSheetProperty& property ) { + const auto& name = property.getName(); + if ( isDataPropertyName( name ) ) + mDataProperties[isNormalizedDataPropertyName( name ) ? name + : normalizeDataPropertyName( name )] = + property; +} + +void UIHTMLWidget::setDataProperty( const std::string& name, const std::string& value ) { + if ( !isDataPropertyName( name ) ) + return; + + std::string normalizedName = + isNormalizedDataPropertyName( name ) ? name : normalizeDataPropertyName( name ); + mDataProperties[normalizedName] = StyleSheetProperty( normalizedName, value, false ); +} + +void UIHTMLWidget::removeDataProperty( const std::string& name ) { + if ( isNormalizedDataPropertyName( name ) ) { + mDataProperties.erase( name ); + return; + } + mDataProperties.erase( normalizeDataPropertyName( name ) ); +} + }} // namespace EE::UI diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 18e7307c1..eb8dc4455 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -377,13 +377,6 @@ bool UIRichText::applyProperty( const StyleSheetProperty& attribute ) { setTextAlign( TEXT_ALIGN_RIGHT ); break; } - case PropertyId::DataLanguage: { - if ( mTag == "pre" && mChild && mChild->isType( UI_TYPE_CODEEDITOR ) ) { - mChild->asType()->applyProperty( attribute ); - } else - mDataProperties["data-language"] = attribute; - break; - } case PropertyId::LineHeight: setLineHeightEq( attribute.value() ); break; diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index 25947c369..c9fedbc3d 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -26,6 +26,10 @@ using namespace EE::Window; namespace EE { namespace UI { +static bool isDataAttributeName( std::string_view name ) { + return String::istartsWith( String::trim( name ), "data-" ); +} + UIWidget* UIWidget::New() { return eeNew( UIWidget, () ); } @@ -1674,6 +1678,8 @@ std::vector UIWidget::getPropertiesImplemented() const { } std::string UIWidget::getPropertyString( const std::string& property ) const { + if ( isType( UI_TYPE_HTML_WIDGET ) && isDataAttributeName( property ) ) + return asConstType()->getDataPropertyString( property ); return getPropertyString( StyleSheetSpecification::instance()->getProperty( property ) ); } @@ -2412,6 +2418,11 @@ void UIWidget::loadFromXmlNode( const pugi::xml_node& node ) { for ( pugi::xml_attribute_iterator ait = node.attributes_begin(); ait != node.attributes_end(); ++ait ) { + if ( isType( UI_TYPE_HTML_WIDGET ) && isDataAttributeName( ait->name() ) ) { + asType()->setDataProperty( ait->name(), ait->value() ); + continue; + } + if ( String::iequals( ait->name(), "style" ) ) { StyleSheetPropertiesParser propertiesParser; propertiesParser.parse( std::string_view{ ait->value() } ); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 133ac7a72..871636caa 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -533,6 +533,53 @@ UTEST( UIHTMLInput, sizeAttribute ) { Engine::destroySingleton(); } +UTEST( UIHTML, DataProperties ) { + init_ui_test(); + auto* sceneNode = SceneManager::instance()->getUISceneNode(); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html( + + +
+
+
+ + + )html" ) ); + + auto* target = sceneNode->getRoot()->find( "target" )->asType(); + ASSERT_TRUE( target != nullptr ); + + EXPECT_TRUE( target->hasDataProperty( "data-role" ) ); + EXPECT_TRUE( target->hasDataProperty( "DATA-ROLE" ) ); + EXPECT_TRUE( target->hasDataProperty( "data-empty" ) ); + EXPECT_FALSE( target->hasDataProperty( "data-missing" ) ); + EXPECT_TRUE( target->getDataPropertyString( "data-role" ) == "hero" ); + EXPECT_TRUE( target->getDataPropertyString( "data-empty", "fallback" ) == "" ); + EXPECT_TRUE( target->getDataPropertyString( "data-missing", "fallback" ) == "fallback" ); + EXPECT_TRUE( target->getPropertyString( "data-role" ) == "hero" ); + + EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-role]" ).size(), (size_t)1 ); + EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-role=\"hero\"]" ).size(), (size_t)1 ); + EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-tags~=\"featured\"]" ).size(), + (size_t)1 ); + EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-lang|=\"en\"]" ).size(), (size_t)1 ); + EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id^=\"user-\"]" ).size(), (size_t)1 ); + EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id$=\"-42\"]" ).size(), (size_t)1 ); + EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id*=\"ser\"]" ).size(), (size_t)1 ); + EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-empty]" ).size(), (size_t)1 ); + EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-missing]" ).size(), (size_t)0 ); + + EXPECT_TRUE( target->getDataPropertyString( "data-language" ) == "cpp" ); + + Engine::destroySingleton(); +} + UTEST( UIHTMLTextArea, rowsColsAttribute ) { init_ui_test(); auto* scene = SceneManager::instance()->getUISceneNode();