utilikit-1.0.0/js/utilikit.rules.apply.js
js/utilikit.rules.apply.js
/**
* @file
* UtiliKit CSS Rule Application Engine.
*
* This file contains the core rule application logic for the UtiliKit
* utility-first CSS system. It provides specialized functions for applying
* different types of CSS rules including spacing, transforms, colors, grids,
* and responsive utilities to DOM elements dynamically.
*
* The rule application system supports:
* - Directional spacing (padding, margin, border-radius, border-width)
* - CSS shorthand notation for box model properties
* - Keyword-based rules (display, position, etc.)
* - Transform functions (rotate, scale, translate)
* - Color values with alpha transparency
* - CSS Grid track lists and fractional units
* - Numeric values with flexible units
* - Range-based grid positioning
*
* @see utilikit.rules.js for rule definitions
* @see utilikit.behavior.js for element processing workflow
*/
(function(Drupal, once, drupalSettings) {
'use strict';
Drupal.utilikit = Drupal.utilikit || {};
/**
* Applies directional side-based CSS rules to elements.
*
* Handles utility classes that target specific sides of the box model,
* such as padding-top, margin-left, border-radius-top-right, etc.
* Supports all standard CSS units and the special 'auto' value where
* applicable.
*
* @param {HTMLElement} el
* The DOM element to apply styles to.
* @param {object} rule
* The rule definition object containing CSS property and metadata.
* @param {string} prefix
* The UtiliKit prefix identifier (e.g., "pd", "mg", "br", "bw").
* @param {string} suffix
* The value suffix from the class (e.g., "t-24", "r-auto").
* @param {Set} utiliKitProperties
* Set tracking modified CSS property names for cleanup.
* @param {string} className
* The complete utility class name for error reporting.
*
* @returns {boolean}
* TRUE if the rule was successfully applied, FALSE otherwise.
*/
Drupal.utilikit.applySidesRule = function(el, rule, prefix, suffix, utiliKitProperties, className) {
if (!rule.sides) return false;
// First convert 'd' to '.' in the suffix
const normalizedSuffix = suffix.replace(/d/g, '.');
const match = normalizedSuffix.match(/^(t|r|b|l)-(auto|\d+(?:\.\d+)?(?:px|pr|em|rem|vh|vw)?)$/);
if (!match) return false;
const side = match[1];
let value = match[2];
if (value !== 'auto') {
// Parse the value with units
const unitMatch = value.match(/^(\d+(?:\.\d+)?)(px|pr|em|rem|vh|vw)?$/);
if (unitMatch) {
const num = unitMatch[1];
let unit = unitMatch[2] || 'px';
if (unit === 'pr') unit = '%';
value = num + unit;
} else {
value = value + 'px'; // Fallback
}
}
if (prefix === 'br') {
if (value === 'auto') return false; // border-radius does not support auto
const radiusMap = {
t: ['borderTopLeftRadius', 'borderTopRightRadius'],
r: ['borderTopRightRadius', 'borderBottomRightRadius'],
b: ['borderBottomLeftRadius', 'borderBottomRightRadius'],
l: ['borderTopLeftRadius', 'borderBottomLeftRadius'],
};
radiusMap[side].forEach((prop) => {
el.style[prop] = value;
utiliKitProperties.add(prop);
});
} else if (prefix === 'bw') {
// For border-width, apply to specific side
const prop = `border${side.toUpperCase()}Width`.replace(/^borderTWidth$/, 'borderTopWidth')
.replace(/^borderRWidth$/, 'borderRightWidth')
.replace(/^borderBWidth$/, 'borderBottomWidth')
.replace(/^borderLWidth$/, 'borderLeftWidth');
el.style[prop] = value;
utiliKitProperties.add(prop);
} else {
const prop = `${rule.css}${{ t: 'Top', r: 'Right', b: 'Bottom', l: 'Left' }[side]}`;
el.style[prop] = value;
utiliKitProperties.add(prop);
}
return true;
};
/**
* Applies CSS shorthand notation for box model properties.
*
* Processes UtiliKit classes that use CSS shorthand syntax for padding,
* margin, border-width, and border-radius. Supports 1-4 value notation
* following standard CSS shorthand rules.
*
* Examples:
* - uk-pd--12 → padding: 12px (all sides)
* - uk-mg--12-24 → margin: 12px 24px (vertical horizontal)
* - uk-pd--8-12-16-20 → padding: 8px 12px 16px 20px (TRBL)
*
* @param {HTMLElement} el
* The DOM element to apply styles to.
* @param {object} rule
* The rule definition object from UtiliKit rules.
* @param {string} prefix
* The UtiliKit prefix ("pd", "mg", "bw", "br").
* @param {string} suffix
* The hyphen-separated values (e.g., "12-24", "8-16-24-32").
* @param {Set} utiliKitProperties
* Set tracking modified CSS properties for cleanup.
* @param {string} className
* The complete utility class name for error reporting.
*
* @returns {boolean}
* TRUE if the shorthand rule was applied, FALSE otherwise.
*/
Drupal.utilikit.applySpacingShorthandRule = function(el, rule, prefix, suffix, utiliKitProperties, className) {
if (!['pd', 'mg', 'bw', 'br'].includes(prefix)) return false;
if (suffix.match(/^(t|r|b|l)-/)) return false;
// First convert 'd' to '.' in the entire suffix
const normalizedSuffix = suffix.replace(/d/g, '.');
// Split by dashes to handle each value individually
const parts = normalizedSuffix.split('-');
const values = [];
// Parse each part
for (let part of parts) {
if (part === 'auto') {
if (rule.allowAuto && ['mg'].includes(prefix)) {
values.push('auto');
} else {
return false;
}
} else {
const numMatch = part.match(/^(\d+(?:\.\d+)?)(px|pr|em|rem)?$/);
if (!numMatch) return false;
let unit = 'px';
if (numMatch[2] === 'pr') unit = '%';
else if (numMatch[2]) unit = numMatch[2];
values.push(numMatch[1] + unit);
}
}
// Validate we got 1-4 values
if (values.length === 0 || values.length > 4) return false;
// Continue with the rest of the function...
// Apply CSS shorthand logic
let top, right, bottom, left;
switch (values.length) {
case 1:
// uk-pd--20 → padding: 20px (all sides)
top = right = bottom = left = values[0];
break;
case 2:
// uk-pd--12-24 → padding: 12px 24px (vertical horizontal)
top = bottom = values[0];
right = left = values[1];
break;
case 3:
// uk-pd--12-24-36 → padding: 12px 24px 36px (top horizontal bottom)
top = values[0];
right = left = values[1];
bottom = values[2];
break;
case 4:
// uk-pd--12-24-36-48 → padding: 12px 24px 36px 48px (TRBL)
top = values[0];
right = values[1];
bottom = values[2];
left = values[3];
break;
default:
return false;
}
// Apply to element based on prefix
if (prefix === 'pd') {
el.style.paddingTop = top;
el.style.paddingRight = right;
el.style.paddingBottom = bottom;
el.style.paddingLeft = left;
utiliKitProperties.add('paddingTop');
utiliKitProperties.add('paddingRight');
utiliKitProperties.add('paddingBottom');
utiliKitProperties.add('paddingLeft');
} else if (prefix === 'mg') {
el.style.marginTop = top;
el.style.marginRight = right;
el.style.marginBottom = bottom;
el.style.marginLeft = left;
utiliKitProperties.add('marginTop');
utiliKitProperties.add('marginRight');
utiliKitProperties.add('marginBottom');
utiliKitProperties.add('marginLeft');
} else if (prefix === 'bw') {
el.style.borderTopWidth = top;
el.style.borderRightWidth = right;
el.style.borderBottomWidth = bottom;
el.style.borderLeftWidth = left;
utiliKitProperties.add('borderTopWidth');
utiliKitProperties.add('borderRightWidth');
utiliKitProperties.add('borderBottomWidth');
utiliKitProperties.add('borderLeftWidth');
} else if (prefix === 'br') {
// For border-radius, apply to all corners
el.style.borderTopLeftRadius = top;
el.style.borderTopRightRadius = right;
el.style.borderBottomRightRadius = bottom;
el.style.borderBottomLeftRadius = left;
utiliKitProperties.add('borderTopLeftRadius');
utiliKitProperties.add('borderTopRightRadius');
utiliKitProperties.add('borderBottomRightRadius');
utiliKitProperties.add('borderBottomLeftRadius');
}
return true;
};
/**
* Applies keyword-based CSS property values.
*
* Handles UtiliKit classes that set CSS properties to predefined keyword
* values such as display: flex, position: absolute, text-align: center, etc.
* Validates against a whitelist of allowed keyword values.
*
* @param {HTMLElement} el
* The DOM element to apply the style to.
* @param {object} rule
* The rule definition containing the CSS property name.
* @param {string} suffix
* The keyword value to apply (e.g., "flex", "absolute", "center").
* @param {Set} utiliKitProperties
* Set tracking modified CSS properties for cleanup.
* @param {string} className
* The complete utility class name for error reporting.
*
* @returns {boolean}
* TRUE if the keyword rule was applied, FALSE otherwise.
*/
Drupal.utilikit.applyKeywordRule = function(el, rule, suffix, utiliKitProperties, className) {
if (!rule.isKeyword) return false;
const match = suffix.match(/^([\w-]+)$/);
if (!match) return false;
el.style[rule.css] = match[1];
utiliKitProperties.add(rule.css);
return true;
};
/**
* Applies CSS transform functions to elements.
*
* Processes UtiliKit transform utilities such as rotate, scale, translate.
* Supports decimal notation using 'd' character (e.g., 45d5 = 45.5).
* Integrates with the existing transform parsing system to handle multiple
* transforms on the same element.
*
* @param {HTMLElement} el
* The DOM element to apply the transform to.
* @param {object} rule
* The rule definition containing transform metadata.
* @param {string} suffix
* The numeric value for the transform (e.g., "90", "45d5").
* @param {Set} utiliKitProperties
* Set tracking modified CSS properties for cleanup.
* @param {string} className
* The complete utility class name for error reporting.
*
* @returns {boolean}
* TRUE if the transform was applied, FALSE otherwise.
*/
Drupal.utilikit.applyTransformRule = function(el, rule, suffix, utiliKitProperties, className) {
if (!rule.isTransform) return false;
// Updated regex to support decimals with 'd' notation
// Matches: 90, 45d5, 150, 120d75, etc.
const match = suffix.match(/^(\d+(?:d\d+)?)$/);
if (!match) return false;
// Convert 'd' notation to decimal point
const valueStr = match[1].replace('d', '.');
const value = parseFloat(valueStr);
// Validate the numeric value
if (isNaN(value)) return false;
// Apply the transform using the existing helper
Drupal.utilikit.parseTransform(el, rule.isTransform, value);
utiliKitProperties.add('transform');
return true;
};
/**
* Applies CSS Grid gap property with flexible syntax.
*
* Processes UtiliKit gap utilities that set row-gap and column-gap
* properties. Supports single values (applied to both axes) and dual
* values (row-gap column-gap). Handles multiple CSS units including
* viewport units and percentages.
*
* Examples:
* - uk-gp--12 → gap: 12px 12px
* - uk-gp--12-24 → gap: 12px 24px
* - uk-gp--1rem-2rem → gap: 1rem 2rem
*
* @param {HTMLElement} el
* The DOM element to apply the gap rule to.
* @param {object} rule
* The rule definition object for gap property.
* @param {string} prefix
* The UtiliKit prefix (should be 'gp' for gap).
* @param {string} suffix
* The gap values from the class name.
* @param {Set} utiliKitProperties
* Set tracking modified CSS properties for cleanup.
* @param {string} className
* The complete utility class name for error reporting.
*
* @returns {boolean}
* TRUE if the gap was applied successfully, FALSE otherwise.
*/
Drupal.utilikit.applyGapRule = function(el, rule, prefix, suffix, utiliKitProperties, className) {
if (prefix !== 'gp') return false;
// First convert 'd' to '.' in the suffix
const normalizedSuffix = suffix.replace(/d/g, '.');
const match = normalizedSuffix.match(/^(\d+(?:\.\d+)?(?:px|pr|em|rem|vh|vw)?)(?:-(\d+(?:\.\d+)?(?:px|pr|em|rem|vh|vw)?))?$/);
if (!match) return false;
// Process first value
let rowGap = match[1];
const rowMatch = rowGap.match(/^(\d+(?:\.\d+)?)(px|pr|em|rem|vh|vw)?$/);
if (rowMatch) {
const num = rowMatch[1];
let unit = rowMatch[2] || 'px';
if (unit === 'pr') unit = '%';
rowGap = num + unit;
}
// Process second value if exists
let colGap = rowGap; // Default to same as row
if (match[2]) {
const colMatch = match[2].match(/^(\d+(?:\.\d+)?)(px|pr|em|rem|vh|vw)?$/);
if (colMatch) {
const num = colMatch[1];
let unit = colMatch[2] || 'px';
if (unit === 'pr') unit = '%';
colGap = num + unit;
}
}
el.style.gap = `${rowGap} ${colGap}`;
utiliKitProperties.add('gap');
return true;
};
/**
* Applies numeric-based utility rules with flexible unit support.
*
* Handles various numeric rule types including opacity conversion,
* decimal values, integers, and flexible numeric values with units.
* Supports special handling for line-height (unitless multipliers)
* and viewport unit restrictions for appropriate properties.
*
* Rule types supported:
* - Opacity: 0-100 integer → 0-1 decimal
* - Decimal fixed: precise decimal values
* - Integer: whole number values
* - Numeric flexible: values with optional units
*
* @param {HTMLElement} el
* The DOM element to apply the style to.
* @param {object} rule
* The rule definition containing property and type metadata.
* @param {string} prefix
* The UtiliKit prefix for the current property.
* @param {string} suffix
* The value portion of the utility class.
* @param {Set} utiliKitProperties
* Set tracking modified CSS properties for cleanup.
* @param {string} className
* The complete utility class name for error reporting.
*
* @returns {boolean}
* TRUE if the style was applied, FALSE if invalid format.
*/
Drupal.utilikit.applyNumericRule = function(el, rule, prefix, suffix, utiliKitProperties, className) {
// Handle opacity: integer 0–100 → decimal 0–1
if (rule.isOpacity) {
const match = suffix.match(/^(\d{1,3})$/);
if (!match) return false;
const value = parseInt(match[1], 10);
if (value < 0 || value > 100) return false;
el.style[rule.css] = (value / 100).toString();
utiliKitProperties.add(rule.css);
return true;
}
// Handle fixed decimal: integer or exactly two decimal places
if (rule.isDecimalFixed) {
// Updated regex to handle 'd' as decimal point
const match = suffix.match(/^(?:\d+|\d+d\d{1,2}|0d\d{1,2})$/);
if (!match) return false;
// Convert 'd' to '.' for CSS value
const value = match[0].replace('d', '.');
el.style[rule.css] = value;
utiliKitProperties.add(rule.css);
return true;
}
if (rule.isInteger) {
const match = suffix.match(/^(-?\d+)$/);
if (!match) return false;
el.style[rule.css] = match[1];
utiliKitProperties.add(rule.css);
return true;
}
// Handle numeric + unit: px, %, em, rem – and allow "auto" if flagged
if (rule.isNumericFlexible) {
if (rule.allowAuto && suffix === 'auto') {
el.style[rule.css] = 'auto';
utiliKitProperties.add(rule.css);
return true;
}
// Accept integers or decimals written with 'd' (e.g., 12d5 → 12.5)
const match = suffix.match(/^(\d+(?:d\d+)?)(px|pr|em|rem|vh|vw)?$/);
if (!match) return false;
// Keep the viewport-unit whitelist as in your original logic.
const viewportAllowedRules = ['wd', 'ht', 'xw', 'xh', 'nw', 'nh', 'fs'];
const unitRaw = match[2] || '';
if ((unitRaw === 'vh' || unitRaw === 'vw') && !viewportAllowedRules.includes(prefix)) {
return false;
}
// Convert 'd' to '.' and validate numeric.
const valueStr = match[1].replace('d', '.');
const numeric = Number(valueStr);
if (Number.isNaN(numeric)) return false;
// Map units: default px; 'pr' → '%'; otherwise passthrough.
let unit = 'px';
if (unitRaw === 'pr') unit = '%';
else if (unitRaw) unit = unitRaw;
// Special handling for unitless line-height (allowed 0–10).
if (prefix === 'lh' && !unitRaw && numeric >= 0 && numeric <= 10) {
el.style[rule.css] = valueStr; // unitless multiplier
utiliKitProperties.add(rule.css);
return true;
}
el.style[rule.css] = `${valueStr}${unit}`;
utiliKitProperties.add(rule.css);
return true;
}
return false;
};
/**
* Applies color values with optional alpha transparency.
*
* Processes UtiliKit color utilities that support both solid colors
* and colors with alpha transparency. Delegates to the color parsing
* system for validation and format conversion.
*
* Supported formats:
* - Named colors: red, blue, primary, secondary
* - Hex colors: ff0000, 00ff00-50 (with alpha)
* - RGB/HSL colors with alpha notation
*
* @param {HTMLElement} el
* The DOM element to apply the color to.
* @param {object} rule
* The rule definition containing the CSS property name.
* @param {string} suffix
* The color value portion of the utility class.
* @param {Set} utiliKitProperties
* Set tracking modified CSS properties for cleanup.
* @param {string} className
* The complete utility class name for error reporting.
*
* @returns {boolean}
* TRUE if the color was applied, FALSE if invalid format.
*/
Drupal.utilikit.applyColorRule = function(el, rule, suffix, utiliKitProperties, className) {
if (!rule.isColor) return false;
const color = Drupal.utilikit.parseColorWithAlpha(suffix);
if (color === null) {
return false;
}
el.style[rule.css] = color;
utiliKitProperties.add(rule.css);
return true;
};
/**
* Applies CSS Grid fractional unit values.
*
* Processes UtiliKit classes that use CSS Grid's fractional unit (fr)
* for flexible grid sizing. Validates the numeric portion and formats
* the final CSS value with the 'fr' unit.
*
* @param {HTMLElement} el
* The DOM element to apply the fractional rule to.
* @param {object} rule
* The rule definition containing the CSS property name.
* @param {string} suffix
* The numeric value with 'fr' suffix.
* @param {Set} utiliKitProperties
* Set tracking modified CSS properties for cleanup.
* @param {string} className
* The complete utility class name for error reporting.
*
* @returns {boolean}
* TRUE if the fractional value was applied, FALSE otherwise.
*/
Drupal.utilikit.applyFractionalRule = function(el, rule, suffix, utiliKitProperties, className) {
if (!rule.isFractional) return false;
const match = suffix.match(/^(\d+)fr$/);
if (!match) return false;
el.style[rule.css] = `${match[1]}fr`;
utiliKitProperties.add(rule.css);
return true;
};
/**
* Applies CSS Grid range positioning values.
*
* Processes UtiliKit classes for CSS Grid line-based positioning
* using the format "start-end" which becomes "start / end" in CSS.
* Used for grid-column and grid-row span definitions.
*
* @param {HTMLElement} el
* The DOM element to apply grid positioning to.
* @param {object} rule
* The rule definition containing the CSS property name.
* @param {string} suffix
* The range value in "start-end" format.
* @param {Set} utiliKitProperties
* Set tracking modified CSS properties for cleanup.
* @param {string} className
* The complete utility class name for error reporting.
*
* @returns {boolean}
* TRUE if the range was applied, FALSE otherwise.
*/
Drupal.utilikit.applyRangeRule = function(el, rule, suffix, utiliKitProperties, className) {
if (!rule.isRange) return false;
const match = suffix.match(/^(\d+)-(\d+)$/);
if (!match) return false;
el.style[rule.css] = `${match[1]} / ${match[2]}`;
utiliKitProperties.add(rule.css);
return true;
};
/**
* Applies CSS Grid track list definitions.
*
* Processes complex grid template utilities using functions like repeat(),
* minmax(), and fr units. Delegates to the grid template parsing system
* to handle the complex syntax and generate appropriate CSS values.
*
* Supported formats:
* - uk-gc--repeat-2-1fr → grid-template-columns: repeat(2, 1fr)
* - uk-gr--minmax-100px-1fr → grid-template-rows: minmax(100px, 1fr)
*
* @param {HTMLElement} el
* The DOM element to apply grid templates to.
* @param {object} rule
* The rule definition for grid track lists.
* @param {string} className
* The complete utility class name for parsing.
* @param {Set} utiliKitProperties
* Set tracking modified CSS properties for cleanup.
*
* @returns {boolean}
* TRUE if the grid template was applied, FALSE otherwise.
*/
Drupal.utilikit.applyGridTrackListRule = function(el, rule, className, utiliKitProperties) {
if (!rule.isGridTrackList) return false;
const css = Drupal.utilikit.parseGridTemplateClass(className);
if (!css) return false;
const match = css.match(/^(grid-template-(columns|rows)):\s*(.+);$/);
if (!match) return false;
const prop = match[1];
const value = match[3];
// Apply the style
if (prop === 'grid-template-columns') {
el.style.gridTemplateColumns = value;
utiliKitProperties.add('gridTemplateColumns');
} else if (prop === 'grid-template-rows') {
el.style.gridTemplateRows = value;
utiliKitProperties.add('gridTemplateRows');
}
return true;
};
/**
* Main rule application dispatcher function.
*
* Coordinates the application of UtiliKit rules by attempting all
* applicable rule handlers in sequence. Ensures the environment is
* initialized before processing and provides centralized error logging
* for unsupported rule formats.
*
* This is the primary entry point for converting UtiliKit utility
* classes into applied CSS styles on DOM elements.
*
* @param {HTMLElement} el
* The DOM element to apply the rule to.
* @param {object} rule
* The rule definition from the UtiliKit rules registry.
* @param {string} prefix
* The UtiliKit prefix identifying the property type.
* @param {string} suffix
* The value portion of the utility class to be processed.
* @param {Set} utiliKitProperties
* Set tracking applied CSS properties for element cleanup.
* @param {string} className
* The complete utility class name for debugging and logging.
*
* @returns {boolean}
* TRUE if any rule handler successfully applied the style, FALSE if
* no handler could process the given suffix format.
*/
Drupal.utilikit.applyRule = function(el, rule, prefix, suffix, utiliKitProperties, className) {
if (!Drupal.utilikit.state) {
Drupal.utilikit.initEnvironment(document);
}
// Special handling for grid templates - they need the full className
if (rule.isGridTrackList) {
return Drupal.utilikit.applyGridTrackListRule(el, rule, className, utiliKitProperties);
}
const success =
Drupal.utilikit.applySidesRule(el, rule, prefix, suffix, utiliKitProperties, className) ||
Drupal.utilikit.applySpacingShorthandRule(el, rule, prefix, suffix, utiliKitProperties, className) ||
Drupal.utilikit.applyKeywordRule(el, rule, suffix, utiliKitProperties, className) ||
Drupal.utilikit.applyTransformRule(el, rule, suffix, utiliKitProperties, className) ||
Drupal.utilikit.applyGapRule(el, rule, prefix, suffix, utiliKitProperties, className) ||
Drupal.utilikit.applyNumericRule(el, rule, prefix, suffix, utiliKitProperties, className) ||
Drupal.utilikit.applyColorRule(el, rule, suffix, utiliKitProperties, className) ||
Drupal.utilikit.applyFractionalRule(el, rule, suffix, utiliKitProperties, className) ||
Drupal.utilikit.applyRangeRule(el, rule, suffix, utiliKitProperties, className);
if (!success) {
Drupal.utilikit.logInvalid('Invalid value', suffix, rule, className);
}
return success;
};
//console.log('Current breakpoint:', Drupal.utilikit.getBreakpoint());
})(Drupal, once, drupalSettings);
