@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
311 lines • 11.9 kB
JavaScript
/**
*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { color, rgbToHex, validHex } from '@uiw/react-color';
import { getWeight, lerpColor, parseDateString, parseLocalDateString, parseTimeString, } from '../../_generated/style-rules';
function getInterpolatedColor(colorRange, rowData, converters) {
const { onColumn, minValue, minColor, maxValue, maxColor, midValue, midColor, } = colorRange;
if (!validHex(minColor) || !validHex(maxColor)) {
return null;
}
const minRgb = color(minColor).rgb;
const maxRgb = color(maxColor).rgb;
const actualMinValue = Math.min(minValue, maxValue);
const actualMaxValue = Math.max(minValue, maxValue);
let midRgb = undefined;
if (midValue !== undefined && midColor !== undefined) {
const isMidInRange = actualMinValue <= midValue && midValue <= actualMaxValue;
if (!isMidInRange || !validHex(midColor)) {
return null;
}
midRgb = color(midColor).rgb;
}
const rawValue = rowData[onColumn];
let numericValue = null;
if (typeof rawValue === 'number') {
numericValue = Number.isFinite(rawValue) ? rawValue : null;
}
else if (typeof rawValue === 'bigint') {
const n = Number(rawValue);
numericValue = Number.isFinite(n) ? n : null;
}
else if (rawValue instanceof Date) {
const ts = rawValue.getTime();
numericValue = Number.isFinite(ts) ? ts : null;
}
else {
const converter = converters === null || converters === void 0 ? void 0 : converters[onColumn];
if (converter !== undefined) {
const converted = converter(rawValue);
if (typeof converted === 'number') {
numericValue = Number.isFinite(converted) ? converted : null;
}
}
}
if (numericValue === null) {
return null;
}
const clampedValue = Math.max(actualMinValue, Math.min(actualMaxValue, numericValue));
let rgb;
if (midRgb !== undefined && midValue !== undefined) {
const t = getWeight(minValue, midValue, clampedValue);
const isBetweenMinAndMid = t <= 1;
if (isBetweenMinAndMid) {
rgb = lerpColor(minRgb, midRgb, t);
}
else {
const t2 = getWeight(midValue, maxValue, clampedValue);
rgb = lerpColor(midRgb, maxRgb, t2);
}
}
else {
const t = getWeight(minValue, maxValue, clampedValue);
rgb = lerpColor(minRgb, maxRgb, t);
}
return rgbToHex(rgb);
}
function resolveValue(value, rowData, converters) {
if (typeof value === 'object' && value !== null && 'column' in value) {
const cellVal = rowData[value.column];
if (cellVal === undefined) {
return null;
}
if (typeof cellVal === 'string' ||
typeof cellVal === 'number' ||
typeof cellVal === 'boolean') {
return cellVal;
}
if (typeof cellVal === 'bigint') {
return Number(cellVal);
}
if (cellVal instanceof Date) {
const ts = cellVal.getTime();
return isNaN(ts) ? null : ts;
}
const converter = converters === null || converters === void 0 ? void 0 : converters[value.column];
if (converter !== undefined) {
return converter(cellVal);
}
return null;
}
if (typeof value === 'object' && value !== null && 'datetime' in value) {
return parseDateString(value.datetime);
}
if (typeof value === 'object' && value !== null && 'localdatetime' in value) {
return parseLocalDateString(value.localdatetime);
}
if (typeof value === 'object' && value !== null && 'time' in value) {
return parseTimeString(value.time);
}
return value;
}
function safeCompare(left, right, rowData, compareFn, converters) {
const leftVal = resolveValue(left, rowData, converters);
const rightVal = resolveValue(right, rowData, converters);
if (leftVal === null || rightVal === null) {
return null;
}
return compareFn(leftVal, rightVal);
}
function safeStringCompare(left, right, rowData, compareFn, converters) {
const leftVal = resolveValue(left, rowData, converters);
const rightVal = resolveValue(right, rowData, converters);
if (leftVal === null ||
rightVal === null ||
typeof leftVal !== 'string' ||
typeof rightVal !== 'string') {
return null;
}
return compareFn(leftVal, rightVal);
}
function evaluateWhere(rowData, where, converters) {
if (!where) {
return true;
}
if ('equal' in where) {
const [left, right] = where.equal;
const leftVal = resolveValue(left, rowData, converters);
const rightVal = resolveValue(right, rowData, converters);
if (leftVal === null || rightVal === null) {
return null;
}
return leftVal === rightVal;
}
if ('not' in where) {
const isMatch = evaluateWhere(rowData, where.not, converters);
return isMatch === null ? null : !isMatch;
}
if ('lessThan' in where) {
return safeCompare(where.lessThan[0], where.lessThan[1], rowData, (a, b) => a < b, converters);
}
if ('lessThanOrEqual' in where) {
return safeCompare(where.lessThanOrEqual[0], where.lessThanOrEqual[1], rowData, (a, b) => a <= b, converters);
}
if ('greaterThan' in where) {
return safeCompare(where.greaterThan[0], where.greaterThan[1], rowData, (a, b) => a > b, converters);
}
if ('greaterThanOrEqual' in where) {
return safeCompare(where.greaterThanOrEqual[0], where.greaterThanOrEqual[1], rowData, (a, b) => a >= b, converters);
}
if ('contains' in where) {
return safeStringCompare(where.contains[0], where.contains[1], rowData, (a, b) => a.includes(b), converters);
}
if ('startsWith' in where) {
return safeStringCompare(where.startsWith[0], where.startsWith[1], rowData, (a, b) => a.startsWith(b), converters);
}
if ('endsWith' in where) {
return safeStringCompare(where.endsWith[0], where.endsWith[1], rowData, (a, b) => a.endsWith(b), converters);
}
if ('isNull' in where) {
const value = resolveValue(where.isNull, rowData, converters);
return value === null;
}
if ('and' in where) {
let hasNull = false;
for (const clause of where.and) {
const isMatch = evaluateWhere(rowData, clause, converters);
if (isMatch === false) {
return false;
}
if (isMatch === null) {
hasNull = true;
}
}
return hasNull ? null : true;
}
if ('or' in where) {
let hasNull = false;
for (const clause of where.or) {
const isMatch = evaluateWhere(rowData, clause, converters);
if (isMatch === true) {
return true;
}
if (isMatch === null) {
hasNull = true;
}
}
return hasNull ? null : false;
}
return false;
}
// --- Style resolution ---
function resolveAppliedStyle(apply, rowData, converters) {
const result = {};
let hasStyle = false;
if (apply.color && !apply.color.isDisabled) {
result.backgroundColor = apply.color.value;
hasStyle = true;
}
if (apply.textColor && !apply.textColor.isDisabled) {
result.color = apply.textColor.value;
hasStyle = true;
}
if (apply.colorRange && !apply.colorRange.isDisabled) {
const interpolated = getInterpolatedColor(apply.colorRange, rowData, converters);
if (interpolated !== null) {
result.backgroundColor = interpolated;
hasStyle = true;
}
}
if (apply.textColorRange && !apply.textColorRange.isDisabled) {
const interpolated = getInterpolatedColor(apply.textColorRange, rowData, converters);
if (interpolated !== null) {
result.color = interpolated;
hasStyle = true;
}
}
return hasStyle ? result : undefined;
}
/**
* Computes styles for all cells in a row based on the given style rules.
* When a rule's `column` is `null`, its style is applied to every cell.
*
* Used internally by hooks, but exported for context-free usage.
*/
export function computeDataGridRowStyles(rules, rowData, converters) {
const enabledRules = rules.filter((r) => !r.isDisabled);
const totalRules = enabledRules.length;
const sortedRules = enabledRules
.map((rule, index) => {
var _a;
if (rule.priority !== undefined && rule.priority < 0) {
throw new Error(`DataGridStyleRule priority must be >= 0, got ${rule.priority}. Negative values are reserved for internal use.`);
}
return Object.assign(Object.assign({}, rule), { _order: index, _priority: (_a = rule.priority) !== null && _a !== void 0 ? _a : index - totalRules });
})
.sort((a, b) => {
if (a._priority !== b._priority) {
return a._priority - b._priority;
}
return a._order - b._order;
});
const cellPriorityMap = {};
for (const rule of sortedRules) {
const isMatching = evaluateWhere(rowData, rule.where, converters) === true;
if (!isMatching) {
continue;
}
const style = resolveAppliedStyle(rule.apply, rowData, converters);
if (!style) {
continue;
}
const effectivePriority = rule._priority;
const affectedColumns = rule.column === null ? Object.keys(rowData) : [rule.column];
for (const col of affectedColumns) {
if (!cellPriorityMap[col]) {
cellPriorityMap[col] = {};
}
for (const [prop, value] of Object.entries(style)) {
const existing = cellPriorityMap[col][prop];
if (!existing || effectivePriority >= existing.priority) {
cellPriorityMap[col][prop] = {
priority: effectivePriority,
value: value,
};
}
}
}
}
const cellStyles = {};
for (const [col, propMap] of Object.entries(cellPriorityMap)) {
const style = {};
let hasProp = false;
for (const [prop, entry] of Object.entries(propMap)) {
style[prop] = entry.value;
hasProp = true;
}
cellStyles[col] = hasProp ? style : undefined;
}
return { cellStyles };
}
/**
* Evaluate style rules for a single cell.
* Pure function -- no React context needed.
*
* @param rules - Array of style rules to evaluate
* @param rowData - The row's data as a record (column id -> value)
* @param columnId - Returns the computed style for this specific cell.
* @param converters - Optional per-column value converters for complex cell types.
*/
export function evaluateDataGridStyleRules(rules, rowData, columnId, converters) {
const { cellStyles } = computeDataGridRowStyles(rules, rowData, converters);
return cellStyles[columnId];
}
//# sourceMappingURL=evaluate.js.map