utilikit-1.0.0/js/utilikit.classes.js

js/utilikit.classes.js
/**
 * @file
 * Utility class parsing and CSS application engine for UtiliKit.
 *
 * This file contains the core logic for parsing UtiliKit utility classes
 * and applying them as CSS styles to DOM elements. It includes sophisticated
 * parsing algorithms for complex CSS Grid syntax, responsive breakpoint
 * handling, and optimized CSS application workflows.
 *
 * Key Components:
 * - CSS Grid template parsing with advanced syntax support
 * - Utility class validation and parsing algorithms
 * - Responsive breakpoint priority management
 * - CSS property application with conflict resolution
 * - Development mode testing and validation utilities
 * - Performance-optimized element processing workflows
 *
 * Grid Template Features:
 * - Support for repeat(), minmax(), and fit-content() functions
 * - Auto-fit and autofill responsive grid patterns
 * - Fractional units (fr), percentages, and fixed dimensions
 * - Complex tokenization preserving compound keywords
 * - Responsive prefix support (sm, md, lg, xl, xxl)
 *
 * Performance Optimizations:
 * - Breakpoint priority sorting for cascade management
 * - Stale inline style cleanup to prevent style conflicts
 * - Early returns for invalid or non-applicable classes
 * - Efficient element processing with minimal DOM manipulation
 * - Static mode detection to skip unnecessary processing
 */
(function(Drupal, once, drupalSettings) {
  'use strict';

  // Ensure UtiliKit namespace exists for utility class processing
  Drupal.utilikit = Drupal.utilikit || {};

  /**
   * Parses CSS Grid template utility classes into valid CSS declarations.
   *
   * This function implements a sophisticated parser for CSS Grid template
   * syntax, supporting complex patterns including repeat() functions,
   * minmax() sizing, fit-content() functions, and responsive breakpoint
   * prefixes. The parser uses intelligent tokenization to handle compound
   * keywords and preserves CSS Grid specification compliance.
   *
   * Supported Syntax Patterns:
   * - Simple values: '1fr', '200px', 'auto', 'min-content'
   * - Repeat functions: 'repeat(3, 1fr)', 'repeat(auto-fit, minmax(250px, 1fr))'
   * - Minmax functions: 'minmax(100px, 1fr)', 'minmax(50%, 2fr)'
   * - Fit-content functions: 'fit-content(200px)', 'fit-content(50%)'
   * - Complex combinations: Multiple functions and values in sequence
   * - Responsive prefixes: 'uk-md-gc--', 'uk-lg-gr--'
   *
   * @param {string} className
   *   The complete utility class name to parse. Must follow UtiliKit
   *   naming convention: 'uk-[breakpoint-]gc/gr--value-pattern'.
   *   Examples: 'uk-gc--repeat-3-1fr', 'uk-md-gc--minmax-200px-1fr'
   *
   * @returns {string|null}
   *   Returns a complete CSS declaration string for grid-template-columns
   *   or grid-template-rows, or null if the className is not a valid
   *   grid template class or cannot be parsed.
   *
   * @example
   * // Simple fractional units
   * parseGridTemplateClass('uk-gc--1fr-2fr-1fr')
   * // Returns: 'grid-template-columns: 1fr 2fr 1fr;'
   *
   * @example
   * // Repeat function with auto-fit
   * parseGridTemplateClass('uk-gc--repeat-auto-fit-minmax-250px-1fr')
   * // Returns: 'grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));'
   *
   * @example
   * // Responsive breakpoint with complex grid
   * parseGridTemplateClass('uk-md-gc--repeat-3-minmax-200px-1fr')
   * // Returns: 'grid-template-columns: repeat(3, minmax(200px, 1fr));'
   *
   * @example
   * // Mixed functions and values
   * parseGridTemplateClass('uk-gc--fitc-200px-1fr-auto')
   * // Returns: 'grid-template-columns: fit-content(200px) 1fr auto;'
   */
  Drupal.utilikit.parseGridTemplateClass = function(className) {
    // Quick validation - must be UtiliKit class with double dash separator
    if (!className.startsWith('uk-') || !className.includes('--')) {
      return null;
    }

    // Determine if this is a grid columns (gc) or rows (gr) class
    const isColumns = className.includes('-gc--');
    const isRows = className.includes('-gr--');

    if (!isColumns && !isRows) {
      return null;
    }

    // Extract the value portion after the double dash separator
    const valueStart = className.indexOf('--') + 2;
    const normalized = className.substring(valueStart);

    /**
     * Smart tokenization function that preserves compound keywords.
     *
     * This internal function intelligently splits the class value while
     * preserving compound keywords like 'auto-fit' and 'auto-fill' that
     * should not be separated during parsing.
     *
     * @param {string} str The string to tokenize
     * @returns {Array<string>} Array of tokens preserving compound keywords
     */
    const smartSplit = function(str) {
      const tokens = [];
      let current = '';

      for (let i = 0; i < str.length; i++) {
        if (str[i] === '-') {
          // Check if this forms a compound keyword
          const nextPart = str.substring(i + 1).split('-')[0];
          if (current === 'auto' && (nextPart === 'fit' || nextPart === 'fill')) {
            // Preserve auto-fit and auto-fill as single tokens
            current += '-' + nextPart;
            i += nextPart.length;
          } else if (current) {
            tokens.push(current);
            current = '';
          }
        } else {
          current += str[i];
        }
      }
      if (current) tokens.push(current);
      return tokens;
    };

    const tokens = smartSplit(normalized);

    // Keywords mapping for CSS Grid values and legacy aliases
    const keywords = {
      'minc': 'min-content',
      'maxc': 'max-content',
      'auto': 'auto',
      'auto-fit': 'auto-fit',      // Modern syntax
      'auto-fill': 'auto-fill',    // Modern syntax
      'autofit': 'auto-fit',       // Legacy support for backwards compatibility
      'autofill': 'auto-fill',     // Legacy support for backwards compatibility
    };

    // Parse tokens into CSS Grid syntax
    const parsed = [];
    let i = 0;

    while (i < tokens.length) {
      const token = tokens[i];

      // Handle repeat() function parsing
      if (token === 'repeat') {
        if (i + 1 >= tokens.length) {
          parsed.push('repeat(/* missing args */)');
          i++;
          continue;
        }

        // Get repeat count (number or keyword like auto-fit)
        const count = keywords[tokens[i + 1]] || tokens[i + 1];
        i += 2;

        // Check if the next token starts a minmax() function
        if (i < tokens.length && tokens[i] === 'minmax') {
          if (i + 2 < tokens.length) {
            let min = tokens[i + 1];
            let max = tokens[i + 2];

            // Convert 'd' to '.' for decimal values (e.g., '0d5fr' -> '0.5fr')
            min = min.replace(/d/g, '.');
            max = max.replace(/d/g, '.');

            // Convert 'pr' suffix to '%' for percentage values
            min = min.replace('pr', '%');
            max = max.replace('pr', '%');

            parsed.push(`repeat(${count}, minmax(${min}, ${max}))`);
            i += 3;
          } else {
            parsed.push(`repeat(${count}, minmax(/* missing args */))`);
            i += 1;
          }
        } else if (i < tokens.length) {
          // Simple repeat with single value
          let value = keywords[tokens[i]] || tokens[i];

          // Apply value transformations
          value = value.replace(/d/g, '.');
          value = value.replace('pr', '%');

          parsed.push(`repeat(${count}, ${value})`);
          i++;
        } else {
          parsed.push(`repeat(${count}, /* missing value */)`);
        }
      }
      // Handle standalone minmax() function
      else if (token === 'minmax') {
        if (i + 2 < tokens.length) {
          let min = tokens[i + 1];
          let max = tokens[i + 2];

          // Apply value transformations
          min = min.replace('pr', '%');
          max = max.replace('pr', '%');

          parsed.push(`minmax(${min}, ${max})`);
          i += 3;
        } else {
          parsed.push('minmax(/* missing args */)');
          i++;
        }
      }
      // Handle fit-content() function (abbreviated as 'fitc')
      else if (token === 'fitc') {
        if (i + 1 < tokens.length) {
          let value = tokens[i + 1];
          value = value.replace('pr', '%');

          parsed.push(`fit-content(${value})`);
          i += 2;
        } else {
          parsed.push('fit-content(/* missing args */)');
          i++;
        }
      }
      // Handle regular values (dimensions, fractions, keywords)
      else {
        let value = keywords[token] || token;

        // Preserve decimal fr values (e.g., '0.5fr') without modification
        if (value.includes('.') && value.endsWith('fr')) {
          // Already in correct format
        } else {
          // Apply standard unit conversions
          value = value.replace('pr', '%');
        }

        parsed.push(value);
        i++;
      }
    }

    // Construct final CSS declaration
    const cssValue = parsed.join(' ');
    const property = isColumns ? 'grid-template-columns' : 'grid-template-rows';

    return `${property}: ${cssValue};`;
  };

  /**
   * Development utility for testing and validating the grid parser.
   *
   * This function runs a comprehensive test suite against the grid template
   * parser to ensure correct parsing of various CSS Grid syntax patterns.
   * It's designed for development and debugging purposes to validate parser
   * behavior and catch regressions.
   *
   * Test Coverage:
   * - Simple fractional and fixed unit combinations
   * - Repeat functions with various count types
   * - Minmax functions with different unit types
   * - Fit-content functions and mixed patterns
   * - Auto-fit and auto-fill responsive patterns
   * - Responsive breakpoint prefixes
   * - Edge cases and error conditions
   *
   * @example
   * // Run the test suite (development mode only)
   * if (drupalSettings.utilikit.devMode) {
   *   Drupal.utilikit.testGridParser();
   * }
   */
  Drupal.utilikit.testGridParser = function() {
    const tests = [
      // Basic fractional and fixed unit patterns
      {
        input: 'uk-gc--repeat-auto-fit-minmax-280px-1fr',
        expected: 'grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));'
      },
      {
        input: 'uk-gc--repeat-auto-fill-minmax-250px-1fr',
        expected: 'grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));'
      },
      {
        input: 'uk-gc--1fr-2fr-0.5fr',
        expected: 'grid-template-columns: 1fr 2fr 0.5fr;'
      },
      {
        input: 'uk-gc--repeat-3-1fr',
        expected: 'grid-template-columns: repeat(3, 1fr);'
      },
      {
        input: 'uk-gc--minmax-50pr-2fr',
        expected: 'grid-template-columns: minmax(50%, 2fr);'
      },
      {
        input: 'uk-gr--auto-1fr-auto',
        expected: 'grid-template-rows: auto 1fr auto;'
      },
      // Fit-content function patterns
      {
        input: 'uk-gc--fitc-200px',
        expected: 'grid-template-columns: fit-content(200px);'
      },
      {
        input: 'uk-gc--fitc-300px-1fr',
        expected: 'grid-template-columns: fit-content(300px) 1fr;'
      },
      {
        input: 'uk-gc--1fr-fitc-250px',
        expected: 'grid-template-columns: 1fr fit-content(250px);'
      },
      {
        input: 'uk-gc--fitc-50pr',
        expected: 'grid-template-columns: fit-content(50%);'
      },
      {
        input: 'uk-gc--200px-1fr',
        expected: 'grid-template-columns: 200px 1fr;'
      },
      {
        input: 'uk-gc--auto-auto',
        expected: 'grid-template-columns: auto auto;'
      },
      {
        input: 'uk-gc--1fr-auto-2fr',
        expected: 'grid-template-columns: 1fr auto 2fr;'
      },
      {
        input: 'uk-gc--100px-1fr-100px',
        expected: 'grid-template-columns: 100px 1fr 100px;'
      },
      {
        input: 'uk-gc--repeat-2-200px',
        expected: 'grid-template-columns: repeat(2, 200px);'
      },
      {
        input: 'uk-gc--repeat-4-1fr',
        expected: 'grid-template-columns: repeat(4, 1fr);'
      },
      {
        input: 'uk-gc--minmax-100px-1fr',
        expected: 'grid-template-columns: minmax(100px, 1fr);'
      },
      {
        input: 'uk-gc--minmax-200px-1fr',
        expected: 'grid-template-columns: minmax(200px, 1fr);'
      },
      {
        input: 'uk-gc--minmax-10rem-1fr',
        expected: 'grid-template-columns: minmax(10rem, 1fr);'
      },
      {
        input: 'uk-gc--repeat-3-minmax-200px-1fr',
        expected: 'grid-template-columns: repeat(3, minmax(200px, 1fr));'
      },
      {
        input: 'uk-gc--repeat-2-minmax-100px-1fr',
        expected: 'grid-template-columns: repeat(2, minmax(100px, 1fr));'
      },
      {
        input: 'uk-gc--repeat-auto-fit-minmax-200px-1fr',
        expected: 'grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));'
      },
      {
        input: 'uk-gc--repeat-auto-fit-minmax-300px-1fr',
        expected: 'grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));'
      },
      {
        input: 'uk-gc--repeat-auto-fill-minmax-200px-1fr',
        expected: 'grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));'
      },
      {
        input: 'uk-gc--repeat-auto-fill-minmax-150px-1fr',
        expected: 'grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));'
      },
      {
        input: 'uk-gc--repeat-auto-fill-minmax-250px-1fr',
        expected: 'grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));'
      },
      {
        input: 'uk-gc--repeat-auto-fit-250px',
        expected: 'grid-template-columns: repeat(auto-fit, 250px);'
      },
      {
        input: 'uk-gc--repeat-auto-fill-200px',
        expected: 'grid-template-columns: repeat(auto-fill, 200px);'
      },
      {
        input: 'uk-md-gc--repeat-2-1fr',
        expected: 'grid-template-columns: repeat(2, 1fr);'
      },
      {
        input: 'uk-lg-gc--repeat-auto-fit-minmax-250px-1fr',
        expected: 'grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));'
      }
    ];

    console.log('Testing Grid Parser:');
    tests.forEach(test => {
      const result = Drupal.utilikit.parseGridTemplateClass(test.input);
      const passed = result === test.expected;
      console.log(
        `${passed ? '✅' : '❌'} ${test.input}`,
        `\n   Expected: ${test.expected}`,
        `\n   Got:      ${result}`
      );
    });
  };

  /**
   * Applies UtiliKit utility classes to DOM elements with intelligent processing.
   *
   * This is the core function responsible for parsing utility classes and
   * applying corresponding CSS styles to elements. It handles responsive
   * breakpoint logic, property conflict resolution, and maintains performance
   * through optimized processing workflows.
   *
   * Processing Workflow:
   * 1. Initialize environment and validate dependencies
   * 2. Collect and categorize utility classes from each element
   * 3. Filter classes based on active breakpoints and screen size
   * 4. Sort classes by breakpoint priority for proper cascade
   * 5. Apply CSS properties while tracking applied properties
   * 6. Clean up stale inline styles from previous processing
   * 7. Update tracking attributes for subsequent runs
   *
   * Performance Features:
   * - Breakpoint applicability checks to skip unnecessary processing
   * - Property tracking to enable efficient style cleanup
   * - Sorted application order for predictable CSS cascade
   * - Static mode detection to skip style application
   * - Development mode logging for debugging and optimization
   *
   * @param {NodeList|Array} elements
   *   Collection of DOM elements to process for utility class application.
   *   Each element should have the 'utilikit' class and one or more
   *   utility classes following the pattern 'uk-[breakpoint-]prefix--value'.
   *
   * @example
   * // Apply classes to newly added elements
   * const newElements = document.querySelectorAll('.utilikit:not([data-utilikit-processed])');
   * Drupal.utilikit.applyClasses(newElements);
   *
   * @example
   * // Reprocess all elements after breakpoint change
   * const allElements = document.querySelectorAll('.utilikit');
   * Drupal.utilikit.applyClasses(allElements);
   *
   * @example
   * // Process elements in specific container
   * const container = document.querySelector('.dynamic-content');
   * const elements = container.querySelectorAll('.utilikit');
   * Drupal.utilikit.applyClasses(elements);
   */
  Drupal.utilikit.applyClasses = function(elements) {
    Drupal.utilikit.utilikitLog('Starting to process ' + elements.length + ' elements', { mode: drupalSettings.utilikit?.renderingMode || 'inline' }, 'log');

    // Initialize environment if not already done
    if (!Drupal.utilikit.state) {
      Drupal.utilikit.initEnvironment(document);

      // Verify initialization succeeded before proceeding
      if (!Drupal.utilikit.state || !Drupal.utilikit.rules) {
        Drupal.utilikit.utilikitLog('Failed to initialize - missing state or rules', null, 'warn');
        return;
      }
    }

    // Get runtime configuration for mode detection and debugging
    const renderingMode = drupalSettings.utilikit?.renderingMode || 'inline';
    const isDevMode = drupalSettings.utilikit?.devMode || false;
    const isStaticMode = renderingMode === 'static';

    elements.forEach((el) => {
      Drupal.utilikit.utilikitLog('Processing element with ' + el.classList.length + ' classes', { mode: renderingMode }, 'log');
      const appliedProps = new Set();

      // Collect and categorize all applicable utility classes
      const classesToApply = [];
      el.classList.forEach((className) => {
        // Skip non-UtiliKit classes
        if (!className.startsWith('uk-')) return;

        // Parse class name structure
        const classBody = className.slice(3);
        const parts = classBody.split('--');
        if (parts.length !== 2) return;

        const prefixParts = parts[0].split('-');
        const suffix = parts[1];
        let bp = null;
        let prefix = null;

        // Determine breakpoint and utility prefix
        if (prefixParts.length === 1) {
          prefix = prefixParts[0];
        } else if (prefixParts.length === 2) {
          bp = prefixParts[0];
          prefix = prefixParts[1];
        } else {
          return;
        }

        // Development mode breakpoint debugging
        if (bp) {
          Drupal.utilikit.utilikitLog('Processing breakpoint class: ' + className, { bp: bp, active: Drupal.utilikit.activeBreakpoints.includes(bp) }, 'log');
        }

        // Skip classes for breakpoints that don't apply to current screen size
        if (bp && !Drupal.utilikit.breakpointApplies(bp)) return;

        // Skip classes for disabled breakpoints in configuration
        if (bp && Drupal.utilikit.activeBreakpoints && !Drupal.utilikit.activeBreakpoints.includes(bp)) {
          return;
        }

        // Skip classes with unknown utility prefixes
        if (!(prefix in Drupal.utilikit.rules)) return;

        // Determine breakpoint priority for sorting (mobile-first cascade)
        const breakpointOrder = { '': 0, 'sm': 1, 'md': 2, 'lg': 3, 'xl': 4, 'xxl': 5 };
        const priority = breakpointOrder[bp || ''] || 0;

        classesToApply.push({
          className,
          prefix,
          suffix,
          bp,
          priority,
          rule: Drupal.utilikit.rules[prefix]
        });
      });

      // Sort by breakpoint priority to ensure proper CSS cascade
      // (smaller breakpoints first, larger breakpoints override)
      classesToApply.sort((a, b) => a.priority - b.priority);

      // Apply classes in priority order
      classesToApply.forEach(({ className, prefix, suffix, rule }) => {
        if (isStaticMode) {
          // Static mode: track classes for debugging but don't apply styles
          Drupal.utilikit.utilikitLog('Static mode - class detected: ' + className, null, 'log');
          appliedProps.add(rule.css); // Track for data attribute
        } else {
          // Inline mode: apply styles dynamically
          Drupal.utilikit.applyRule(el, rule, prefix, suffix, appliedProps, className);
        }
      });

      // Clean up stale inline styles from previous processing runs
      const previousProps = el.dataset.utilikitProps ? el.dataset.utilikitProps.split(',') : [];
      previousProps.forEach((prop) => {
        if (!appliedProps.has(prop)) el.style[prop] = '';
      });

      // Update tracking data for next processing run
      el.dataset.utilikitProps = Array.from(appliedProps).join(',');
      Drupal.utilikit.utilikitLog('Element completed: ' + appliedProps.size + ' properties applied', Array.from(appliedProps), 'log');
    });
  };

})(Drupal, once, drupalSettings);

/**
 * Integration Notes for UtiliKit Class Processing System:
 *
 * CSS Grid Template Parsing Architecture:
 * - Supports the complete CSS Grid specification for template definitions
 * - Handles complex syntax including nested functions and responsive variants
 * - Implements intelligent tokenization to preserve compound keywords
 * - Provides comprehensive error handling for malformed syntax
 * - Maintains CSS specification compliance for all generated output
 *
 * Responsive Breakpoint System:
 * - Mobile-first approach with progressive enhancement
 * - Breakpoint priority sorting ensures proper CSS cascade behavior
 * - Screen size applicability checks prevent unnecessary processing
 * - Configuration-based breakpoint activation for performance optimization
 * - Development mode debugging for breakpoint application logic
 *
 * Performance Optimization Strategies:
 * - Early returns for invalid or non-applicable classes
 * - Efficient DOM querying with minimal style recalculation
 * - Property tracking enables targeted cleanup of stale styles
 * - Static mode detection skips unnecessary processing overhead
 * - Batch processing of elements minimizes layout thrashing
 *
 * Development and Debugging Features:
 * - Comprehensive test suite for grid parser validation
 * - Detailed logging for class processing workflow
 * - Breakpoint application debugging with context information
 * - Property application tracking for optimization analysis
 * - Error reporting with specific context for troubleshooting
 *
 * Integration with UtiliKit Core Services:
 * - Environment initialization with state management integration
 * - Rules registry integration for utility prefix validation
 * - Active breakpoints configuration from drupalSettings
 * - Rendering mode detection for processing workflow adaptation
 * - Logging service integration for consistent debugging output
 *
 * Browser Compatibility and Standards:
 * - CSS Grid specification compliance for all generated output
 * - Modern JavaScript features with graceful degradation
 * - Cross-browser testing for grid template syntax support
 * - Standards-compliant CSS property names and values
 * - Performance optimization for various device capabilities
 *
 * Error Handling and Robustness:
 * - Graceful handling of malformed utility class syntax
 * - Fallback processing for missing or corrupted configuration
 * - Non-blocking error recovery that preserves other functionality
 * - Comprehensive validation of parser inputs and outputs
 * - Development mode warnings for debugging and optimization
 */

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc