Skip to content

Commit d84dfd8

Browse files
committed
Feat: Add show/hide/toggle. Refactor naturalDisplay to handle nested css special cases. Add tests.
1 parent d4ac2b5 commit d84dfd8

File tree

6 files changed

+806
-409
lines changed

6 files changed

+806
-409
lines changed

packages/query/src/query.js

Lines changed: 179 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,10 +1512,11 @@ export class Query {
15121512
return height.length > 1 ? height : height[0];
15131513
}
15141514

1515-
naturalDisplay() {
1516-
const displays = this.map((el) => {
1517-
// Create cache key from current stylesheet state + inline styles
1518-
const stylesheetData = Array.from(document.styleSheets).map(sheet => {
1515+
naturalDisplay({ calculate = true } = {}) {
1516+
// store style hash across map
1517+
let styleHash;
1518+
if (calculate) {
1519+
styleHash = Array.from(document.styleSheets).map(sheet => {
15191520
try {
15201521
return { href: sheet.href, title: sheet.title, disabled: sheet.disabled };
15211522
}
@@ -1524,137 +1525,160 @@ export class Query {
15241525
return { crossOrigin: true, index: Array.from(document.styleSheets).indexOf(sheet) };
15251526
}
15261527
});
1528+
}
15271529

1528-
// Include inline display style in cache key since it overrides everything
1529-
const inlineDisplay = el.style.display;
1530-
const cacheKey = { stylesheetData, inlineDisplay };
1531-
const rulesHash = hashCode(cacheKey);
1532-
1533-
// Check cache for this element
1534-
const cached = Query.elementDisplayCache.get(el);
1535-
if (cached && cached.rulesHash === rulesHash) {
1536-
return cached.displayValue;
1537-
}
1538-
1539-
// If already visible, return current display
1540-
const current = getComputedStyle(el).display;
1541-
if (current !== 'none') {
1542-
// Cache visible elements too
1543-
Query.elementDisplayCache.set(el, { rulesHash, displayValue: current });
1544-
return current;
1545-
}
1546-
1547-
const matchingRules = [];
1548-
1549-
// Calculate CSS specificity (simplified)
1550-
const calculateSpecificity = (selector) => {
1551-
// Remove quoted strings to avoid counting selectors inside quotes
1552-
const cleaned = selector.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
1553-
const ids = (cleaned.match(/#[\w-]+/g) || []).length * 100;
1554-
const classes = (cleaned.match(/\.[\w-]+/g) || []).length * 10;
1555-
const attrs = (cleaned.match(/\[[^\]]+\]/g) || []).length * 10;
1556-
const pseudoClasses = (cleaned.match(/:[\w-]+/g) || []).length * 10;
1557-
const elements = (cleaned.match(/\b[a-z][\w-]*/gi) || []).length * 1;
1558-
return ids + classes + attrs + pseudoClasses + elements;
1559-
};
1530+
const displays = this.map((el, index) => {
1531+
// FAST PATH: if an inline style is applied return this
1532+
let inlineDisplay = el.style.display;
1533+
if (inlineDisplay && inlineDisplay !== 'none') {
1534+
return inlineDisplay;
1535+
}
15601536

1561-
// Helper to recursively parse CSS rules (handles nesting)
1562-
const parseRules = (rules) => {
1563-
for (const rule of rules) {
1564-
// Handle regular style rules
1565-
if (
1566-
rule.style?.display
1567-
&& rule.style.display !== 'none' // IGNORE ALL none rules
1568-
&& rule.selectorText
1569-
&& el.matches(rule.selectorText)
1570-
) {
1571-
matchingRules.push({
1572-
display: rule.style.display,
1573-
specificity: calculateSpecificity(rule.selectorText),
1574-
sourceOrder: matchingRules.length,
1575-
});
1537+
let cacheKey;
1538+
let rulesHash;
1539+
1540+
// CALCULATED PATH: check stylesheets for highest specificity display state
1541+
if (calculate) {
1542+
// Include inline display style in cache key since it overrides everything
1543+
cacheKey = { styleHash, inlineDisplay };
1544+
rulesHash = hashCode(cacheKey);
1545+
1546+
// FAST PATH: Skip stylesheet check, if rules are same
1547+
const cached = Query.elementDisplayCache.get(el);
1548+
if (cached && cached.rulesHash === rulesHash) {
1549+
return cached.displayValue;
1550+
}
1551+
1552+
// MEDIUM PATH: If already visible, return current display
1553+
const computedDisplay = getComputedStyle(el).display;
1554+
if (computedDisplay !== 'none') {
1555+
Query.elementDisplayCache.set(el, { rulesHash, displayValue: computedDisplay });
1556+
return computedDisplay;
1557+
}
1558+
1559+
// SLOW PATH: Start parsing rules to determine display type
1560+
const matchingRules = [];
1561+
1562+
// Calculate CSS specificity (simplified)
1563+
const calculateSpecificity = (selector) => {
1564+
// Remove quoted strings to avoid counting selectors inside quotes
1565+
const cleaned = selector.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
1566+
const ids = (cleaned.match(/#[\w-]+/g) || []).length * 100;
1567+
const classes = (cleaned.match(/\.[\w-]+/g) || []).length * 10;
1568+
const attrs = (cleaned.match(/\[[^\]]+\]/g) || []).length * 10;
1569+
const pseudoClasses = (cleaned.match(/:[\w-]+/g) || []).length * 10;
1570+
const elements = (cleaned.match(/\b[a-z][\w-]*/gi) || []).length * 1;
1571+
return ids + classes + attrs + pseudoClasses + elements;
1572+
};
1573+
1574+
// Helper to recursively parse CSS rules (handles nesting)
1575+
const parseRules = (rules, parentSelector = '') => {
1576+
for (const rule of rules) {
1577+
// Handle regular style rules
1578+
if (
1579+
rule.style?.display
1580+
&& rule.style.display !== 'none' // IGNORE ALL none rules
1581+
&& rule.selectorText
1582+
) {
1583+
// Resolve nested selector by replacing & with parent
1584+
let resolvedSelector = rule.selectorText;
1585+
if (parentSelector && resolvedSelector.includes('&')) {
1586+
resolvedSelector = resolvedSelector.replace(/&/g, parentSelector);
1587+
}
1588+
1589+
if (el.matches(resolvedSelector)) {
1590+
matchingRules.push({
1591+
display: rule.style.display,
1592+
specificity: calculateSpecificity(resolvedSelector),
1593+
sourceOrder: matchingRules.length,
1594+
});
1595+
}
1596+
}
1597+
// Recursively handle nested rules (CSS nesting)
1598+
if (rule.cssRules && rule.cssRules.length > 0) {
1599+
const nestedParent = parentSelector || rule.selectorText;
1600+
parseRules(rule.cssRules, nestedParent);
1601+
}
1602+
}
1603+
};
1604+
1605+
// Parse all stylesheets for matching rules
1606+
for (const sheet of document.styleSheets) {
1607+
try {
1608+
parseRules(sheet.cssRules);
15761609
}
1577-
// Recursively handle nested rules (CSS nesting)
1578-
if (rule.cssRules && rule.cssRules.length > 0) {
1579-
parseRules(rule.cssRules);
1610+
catch (e) {
1611+
// Cross-origin stylesheets - ignore
15801612
}
15811613
}
1582-
};
15831614

1584-
// Parse all stylesheets for matching rules
1585-
for (const sheet of document.styleSheets) {
1586-
try {
1587-
parseRules(sheet.cssRules);
1588-
}
1589-
catch (e) {
1590-
// Cross-origin stylesheets - ignore
1615+
// Sort by specificity, then source order
1616+
matchingRules.sort((a, b) => b.specificity - a.specificity || b.sourceOrder - a.sourceOrder);
1617+
1618+
// If we have matching rules, return the highest precedence value
1619+
if (matchingRules.length > 0) {
1620+
const displayValue = matchingRules[0].display;
1621+
Query.elementDisplayCache.set(el, { rulesHash, displayValue });
1622+
return displayValue;
15911623
}
15921624
}
15931625

1594-
// Sort by specificity, then source order
1595-
matchingRules.sort((a, b) => b.specificity - a.specificity || b.sourceOrder - a.sourceOrder);
1626+
// BACKUP Path: Use natural display type for browsers based off a lookup table
1627+
const naturalDisplay = {
1628+
inline: [
1629+
'a',
1630+
'abbr',
1631+
'b',
1632+
'bdi',
1633+
'bdo',
1634+
'br',
1635+
'cite',
1636+
'code',
1637+
'dfn',
1638+
'em',
1639+
'i',
1640+
'kbd',
1641+
'mark',
1642+
'q',
1643+
'ruby',
1644+
'samp',
1645+
'small',
1646+
'span',
1647+
'strong',
1648+
'sub',
1649+
'sup',
1650+
'time',
1651+
'u',
1652+
'var',
1653+
'wbr',
1654+
],
1655+
'inline-block': ['button', 'img', 'input', 'meter', 'object', 'progress', 'select', 'textarea'],
1656+
'table': ['table'],
1657+
'table-row': ['tr'],
1658+
'table-cell': ['td', 'th'],
1659+
'table-header-group': ['thead'],
1660+
'table-row-group': ['tbody'],
1661+
'table-footer-group': ['tfoot'],
1662+
'table-caption': ['caption'],
1663+
'table-column': ['col'],
1664+
'table-column-group': ['colgroup'],
1665+
'list-item': ['li'],
1666+
};
15961667

1668+
const tagName = el.tagName.toLowerCase();
15971669
let displayValue;
1598-
1599-
// If we have matching rules, return the highest precedence value
1600-
if (matchingRules.length > 0) {
1601-
displayValue = matchingRules[0].display;
1602-
}
1603-
else {
1604-
// No CSS rules found - use element's natural display value
1605-
const naturalDisplay = {
1606-
inline: [
1607-
'a',
1608-
'abbr',
1609-
'b',
1610-
'bdi',
1611-
'bdo',
1612-
'br',
1613-
'cite',
1614-
'code',
1615-
'dfn',
1616-
'em',
1617-
'i',
1618-
'kbd',
1619-
'mark',
1620-
'q',
1621-
'ruby',
1622-
'samp',
1623-
'small',
1624-
'span',
1625-
'strong',
1626-
'sub',
1627-
'sup',
1628-
'time',
1629-
'u',
1630-
'var',
1631-
'wbr',
1632-
],
1633-
'inline-block': ['button', 'img', 'input', 'meter', 'object', 'progress', 'select', 'textarea'],
1634-
'table': ['table'],
1635-
'table-row': ['tr'],
1636-
'table-cell': ['td', 'th'],
1637-
'table-header-group': ['thead'],
1638-
'table-row-group': ['tbody'],
1639-
'table-footer-group': ['tfoot'],
1640-
'table-caption': ['caption'],
1641-
'table-column': ['col'],
1642-
'table-column-group': ['colgroup'],
1643-
'list-item': ['li'],
1644-
};
1645-
1646-
const tagName = el.tagName.toLowerCase();
1647-
for (const [display, tags] of Object.entries(naturalDisplay)) {
1648-
if (tags.includes(tagName)) {
1649-
displayValue = display;
1650-
break;
1651-
}
1670+
for (const [display, tags] of Object.entries(naturalDisplay)) {
1671+
if (tags.includes(tagName)) {
1672+
displayValue = display;
1673+
break;
16521674
}
1653-
displayValue = displayValue || 'block'; // Default for most elements
16541675
}
1676+
displayValue = displayValue || 'block'; // Default for most elements
16551677

1656-
// Cache the result
1657-
Query.elementDisplayCache.set(el, { rulesHash, displayValue });
1678+
// Cache the result if we are calculating
1679+
if (calculate) {
1680+
Query.elementDisplayCache.set(el, { rulesHash, displayValue });
1681+
}
16581682

16591683
return displayValue;
16601684
});
@@ -1755,22 +1779,27 @@ export class Query {
17551779
return this.length > 0;
17561780
}
17571781

1758-
isVisible(options = {}) {
1782+
isVisible({ includeOpacity = false, includeVisibility = true } = {}) {
17591783
if (this.length === 0) {
17601784
return undefined;
17611785
}
1762-
1763-
const { includeOpacity = false } = options;
1764-
17651786
// Return true only if ALL elements are visible
17661787
return this.map(el => {
17671788
const rect = el.getBoundingClientRect();
17681789
const hasDimensions = rect.width > 0 && rect.height > 0;
17691790

17701791
if (!hasDimensions) { return false; }
17711792

1793+
const style = window.getComputedStyle(el);
1794+
1795+
// Check intentional hiding methods
1796+
if (includeVisibility) {
1797+
if (style.visibility === 'hidden') { return false; }
1798+
if (style.contentVisibility === 'hidden') { return false; }
1799+
}
1800+
1801+
// Check optional hiding mechanisms
17721802
if (includeOpacity) {
1773-
const style = window.getComputedStyle(el);
17741803
return parseFloat(style.opacity) > 0;
17751804
}
17761805

@@ -1876,6 +1905,30 @@ export class Query {
18761905
return this.chain(combinedElements);
18771906
}
18781907

1908+
show({ calculate = true } = {}) {
1909+
return this.each(function(el) {
1910+
const naturalDisplayValue = this.naturalDisplay({ calculate });
1911+
el.style.display = naturalDisplayValue || '';
1912+
});
1913+
}
1914+
1915+
hide() {
1916+
return this.css('display', 'none');
1917+
}
1918+
1919+
toggle({ calculate = true } = {}) {
1920+
return this.each(function(el) {
1921+
const isHidden = getComputedStyle(el).display === 'none';
1922+
if (isHidden) {
1923+
const naturalDisplayValue = this.naturalDisplay({ calculate });
1924+
el.style.display = naturalDisplayValue || '';
1925+
}
1926+
else {
1927+
el.style.display = 'none';
1928+
}
1929+
});
1930+
}
1931+
18791932
// special helper for SUI components
18801933
component() {
18811934
const components = this.map(el => el.component).filter(Boolean);

0 commit comments

Comments
 (0)