svarog-ui-core
Version:
Core components of Svarog UI without themes
325 lines (277 loc) • 8.51 kB
JavaScript
// src/components/Grid/Grid.js
import {
createComponent,
createElement,
appendChildren,
} from '../../utils/componentFactory.js';
import { withThemeAwareness } from '../../utils/composition.js';
import { createStyleInjector } from '../../utils/styleInjection.js';
import { gridStyles } from './Grid.styles.js';
// Create style injector for Grid component
const injectGridStyles = createStyleInjector('Grid');
/**
* Creates a Column component for use within a Grid
* @param {Object} props - Column properties
* @returns {Object} Column component
*/
const createColumn = (props) => {
const {
width = 12,
mobileWidth,
tabletWidth,
desktopWidth,
desktopOffset,
offset,
} = props;
// Inject styles on component creation
injectGridStyles(gridStyles);
// State
const columnState = { ...props };
// Validate column width
const validateWidth = (prop, value) => {
if (!Number.isInteger(value) || value < 1 || value > 12) {
throw new Error(`${prop} must be an integer between 1 and 12`);
}
};
// Validate all widths
validateWidth('width', width);
if (mobileWidth !== undefined) validateWidth('mobileWidth', mobileWidth);
if (tabletWidth !== undefined) validateWidth('tabletWidth', tabletWidth);
if (desktopWidth !== undefined) validateWidth('desktopWidth', desktopWidth);
if (desktopOffset !== undefined)
validateWidth('desktopOffset', desktopOffset);
if (offset !== undefined) validateWidth('offset', offset);
// Create the column element
const buildColumnElement = () => {
const classes = ['column'];
// Regular styles only (no CSS custom properties)
const style = {};
// Handle offsets as regular styles
if (columnState.offset !== undefined) {
style.gridColumnStart = columnState.offset + 1;
}
const column = createElement('div', {
classes,
style,
});
// Apply CSS custom properties after element creation
column.style.setProperty('--col-width', columnState.width);
if (columnState.mobileWidth !== undefined) {
column.style.setProperty('--col-width-mobile', columnState.mobileWidth);
}
if (columnState.tabletWidth !== undefined) {
column.style.setProperty('--col-width-tablet', columnState.tabletWidth);
}
if (columnState.desktopWidth !== undefined) {
column.style.setProperty('--col-width-desktop', columnState.desktopWidth);
}
if (columnState.desktopOffset !== undefined) {
column.style.setProperty(
'--col-offset-desktop',
columnState.desktopOffset + 1
);
}
// Append children - properly handle both array and single child
if (Array.isArray(columnState.children)) {
appendChildren(column, columnState.children);
} else if (columnState.children instanceof HTMLElement) {
column.appendChild(columnState.children);
} else if (columnState.children) {
column.textContent = String(columnState.children);
}
return column;
};
// Initial column element
let columnElement = buildColumnElement();
// Public API
return {
/**
* Get the column element
* @returns {HTMLElement} Column element
*/
getElement() {
return columnElement;
},
/**
* Update column properties
* @param {Object} newProps - New properties
* @returns {Object} Column component (for chaining)
*/
update(newProps) {
// Update state
Object.assign(columnState, newProps);
// Rebuild element
const oldElement = columnElement;
columnElement = buildColumnElement();
// Replace in DOM if the old element was inserted
if (oldElement.parentNode) {
oldElement.parentNode.replaceChild(columnElement, oldElement);
}
return this;
},
/**
* Clean up resources
*/
destroy() {
columnElement = null;
},
/**
* Handle theme changes
* @param {string} newTheme - New theme name
* @param {string} previousTheme - Previous theme name
*/
onThemeChange(newTheme, previousTheme) {
// Handle theme changes if needed
console.debug(
`Column: Theme changed from ${previousTheme} to ${newTheme}`
);
},
};
};
/**
* Creates a Grid component
* @param {Object} props - Grid properties
* @returns {Object} Grid component
*/
const createGrid = (props = {}) => {
const { rowGap, columnGap, gap, alignItems, justifyItems } = props;
// Inject styles on component creation
injectGridStyles(gridStyles);
// State
const gridState = { ...props };
// Validation
const isValidCSSValue = (value) => {
return (
typeof value === 'string' &&
(value.endsWith('px') ||
value.endsWith('rem') ||
value.endsWith('em') ||
value.endsWith('%') ||
value === 'auto')
);
};
if (gap && !isValidCSSValue(gap)) {
throw new Error('Invalid gap value');
}
if (rowGap && !isValidCSSValue(rowGap)) {
throw new Error('Invalid rowGap value');
}
if (columnGap && !isValidCSSValue(columnGap)) {
throw new Error('Invalid columnGap value');
}
const validAlignments = ['start', 'end', 'center', 'stretch'];
if (alignItems && !validAlignments.includes(alignItems)) {
throw new Error(`alignItems must be one of: ${validAlignments.join(', ')}`);
}
if (justifyItems && !validAlignments.includes(justifyItems)) {
throw new Error(
`justifyItems must be one of: ${validAlignments.join(', ')}`
);
}
// Create the grid element
const buildGridElement = () => {
const classNames = ['grid'];
if (gridState.reverse) {
classNames.push('grid--reverse');
}
if (gridState.mobileReverse) {
classNames.push('grid--mobile-reverse');
}
const style = {
display: 'grid',
gridTemplateColumns: 'repeat(12, 1fr)',
};
// Gap handling
if (gridState.gap) {
style.gap = gridState.gap;
} else {
if (gridState.rowGap) {
style.rowGap = gridState.rowGap;
}
if (gridState.columnGap) {
style.columnGap = gridState.columnGap;
}
}
// Alignment
if (gridState.alignItems) {
style.alignItems = gridState.alignItems;
}
if (gridState.justifyItems) {
style.justifyItems = gridState.justifyItems;
}
return createElement('div', {
classes: classNames,
style,
});
};
// Initial grid element
let gridElement = buildGridElement();
// Public API
return {
/**
* Get the grid element
* @returns {HTMLElement} Grid element
*/
getElement() {
return gridElement;
},
/**
* Append a child to the grid
* @param {HTMLElement} child - Child element
* @returns {Object} Grid component (for chaining)
*/
appendChild(child) {
if (!(child instanceof HTMLElement)) {
throw new Error('Child must be an HTMLElement');
}
gridElement.appendChild(child);
return this;
},
/**
* Update grid properties
* @param {Object} newProps - New properties
* @returns {Object} Grid component (for chaining)
*/
update(newProps) {
// Update state
Object.assign(gridState, newProps);
// Rebuild element
const oldElement = gridElement;
gridElement = buildGridElement();
// Move all children to the new element
while (oldElement.firstChild) {
gridElement.appendChild(oldElement.firstChild);
}
// Replace in DOM if the old element was inserted
if (oldElement.parentNode) {
oldElement.parentNode.replaceChild(gridElement, oldElement);
}
return this;
},
/**
* Clean up resources
*/
destroy() {
gridElement = null;
},
/**
* Handle theme changes
* @param {string} newTheme - New theme name
* @param {string} previousTheme - Previous theme name
*/
onThemeChange(newTheme, previousTheme) {
// Handle theme changes if needed
console.debug(`Grid: Theme changed from ${previousTheme} to ${newTheme}`);
},
};
};
// Define required props for validation
createColumn.requiredProps = ['children'];
createGrid.requiredProps = [];
// Create theme-aware components
const Column = withThemeAwareness(createComponent('Column', createColumn));
const Grid = withThemeAwareness(createComponent('Grid', createGrid));
// Add Column as a static property of Grid
Grid.Column = Column;
// Export
export default Grid;