@syncfusion/ej2-pdfviewer
Version:
Essential JS 2 PDF viewer Component
397 lines (396 loc) • 18.6 kB
JavaScript
/**
* @hidden
* Builds filter predicates from a filter settings object.
*
* Returns two predicates:
* 1. commentPredicate: For comment panel filtering (includes reply logic)
* 2. documentPredicate: For document annotation filtering
*
* Both predicates are stateless, O(1) functions.
*
* @param {CommentFilterSettings} settings - Filter criteria
* @returns {FilterPredicates} Object containing commentPredicate and documentPredicate
*/
export function buildFilterPredicates(settings) {
/**
* Helper: Map measurement (calibrate) annotation user-facing names to internal indent identifiers.
* Bidirectional: handles both user-facing names and internal identifiers.
* @param {string} value - The measurement type value
* @returns {string} - The mapped internal identifier (indent)
*/
var mapMeasurementType = function (value) {
// Handle case when object is received for Radius
if (typeof value === 'object' && value !== null) {
if (value.PolygonRadius) {
return value.PolygonRadius;
}
}
// Handle string cases
if (typeof value === 'string') {
switch (value) {
case 'Distance': return 'LineDimension';
case 'Perimeter': return 'PolyLineDimension';
case 'Area': return 'PolygonDimension';
case 'Radius': return 'PolygonRadius';
case 'Volume': return 'PolygonVolume';
case 'Rectangle': return 'Square';
// Also handle reverse mapping (internal identifiers pass through)
case 'LineDimension':
case 'PolyLineDimension':
case 'PolygonDimension':
case 'PolygonRadius':
case 'PolygonVolume': return value;
default: return value;
}
}
return String(value);
};
/**
* Helper: Pad a string with zeros to reach desired length.
* Compatible with older ES versions.
* @param {string} str - The string to pad
* @param {number} length - The desired length
* @returns {string} - The padded string
*/
var padWithZeros = function (str, length) {
while (str.length < length) {
str = '0' + str;
}
return str;
};
/**
* Helper: Normalize color values to hexadecimal format.
* Converts rgba(), rgb(), and other formats to hex for comparison.
* @param {any} colorValue - The color value in any format (may be string, null, undefined, etc.)
* @returns {string} - The color value in hex format
*/
var normalizeColorToHex = function (colorValue) {
// Type check: ensure colorValue is a string
if (typeof colorValue !== 'string') {
return '';
}
if (!colorValue || colorValue === '') {
return '';
}
// Convert to string to be safe
var colorStr = String(colorValue).trim();
// If already in hex format, return as is
if (colorStr.indexOf('#') === 0) {
if (colorStr.length === 9) {
return colorStr.substring(0, 7).toLowerCase();
}
return colorStr.toLowerCase();
}
// Handle rgba format: rgba(255, 0, 0, 0.5) or rgba(255,0,0,0.5)
// eslint-disable-next-line security/detect-unsafe-regex
var rgbaMatch = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i);
if (rgbaMatch) {
var r = padWithZeros(parseInt(rgbaMatch[1], 10).toString(16), 2);
var g = padWithZeros(parseInt(rgbaMatch[2], 10).toString(16), 2);
var b = padWithZeros(parseInt(rgbaMatch[3], 10).toString(16), 2);
return ('#' + r + g + b).toLowerCase();
}
// If it's a named color or other format, try to return as-is lowercased
return colorStr.toLowerCase();
};
/**
* Helper: Get the annotation type using proper field lookup with fallback strategy.
* Tries specific type fields first, then falls back to shapeAnnotationType.
* Handles: Measurement, TextMarkup, FreeText, and Shape annotations.
* Distinguishes between lines and arrows by checking lineHeadStart and lineHeadEnd.
* @param {any} annotation - The annotation object
* @returns {string} - The annotation type
*/
var getAnnotationType = function (annotation) {
// PRIORITY ORDER: Check specific type fields, then fallback to shapeAnnotationType
// 1. Check for Measurement/Calibrate annotations via indent field
var indent = annotation.indent || annotation.Indent;
if (indent) {
// indent contains the internal identifier (e.g., "LineDimension")
return indent;
}
// 2. Check for TextMarkup annotations (Highlight, Underline, Strikethrough)
var textMarkupType = annotation.textMarkupAnnotationType || annotation.TextMarkupAnnotationType;
if (textMarkupType) {
return textMarkupType;
}
// 3. Check for FreeText annotations
var freeTextType = annotation.freeTextAnnotationType || annotation.FreeTextAnnotationType;
if (freeTextType) {
return freeTextType;
}
// FALLBACK: Use shapeAnnotationType (catches Shape, Sticky, Stamp, etc.)
var shapeType = annotation.shapeAnnotationType || annotation.ShapeAnnotationType || '';
// 4. Special handling for Arrow annotation: distinguish arrows from lines
// Arrows have shapeAnnotationType === 'line' but with lineHeadStart/lineHeadEnd !== "none"
if (shapeType === 'Line' && annotation.lineHeadStart !== 'None' && annotation.lineHeadEnd !== 'None') {
return 'Arrow';
}
return shapeType;
};
/**
* Helper: Check if annotation matches filter criteria (ignoring replies).
* Supports multiple annotation type systems with proper field lookup.
* Handles: Shape, Sticky, Stamp, TextMarkup, FreeText, and Measurement annotations.
* Used by both predicates.
* @param {any} annotation - The annotation object to evaluate
* @returns {boolean} - Returns true if annotation matches filter criteria
*/
var matchesAnnotation = function (annotation) {
// Type filter with proper annotation type retrieval
if (settings.type && Array.isArray(settings.type) && settings.type.length > 0) {
var annotType = getAnnotationType(annotation);
// Check if the annotation type matches any of the filter types
var typeMatches = false;
for (var _i = 0, _a = settings.type; _i < _a.length; _i++) {
var filterType = _a[_i];
// Map measurement type if needed (handles both user-facing and internal names)
var mappedFilterType = mapMeasurementType(filterType);
if (annotType === mappedFilterType || annotType === filterType) {
typeMatches = true;
break;
}
}
if (!typeMatches) {
return false;
}
}
// Color filter with proper color normalization and field lookup
if (settings.color && Array.isArray(settings.color) && settings.color.length > 0) {
// Try to get color from various fields (different annotation types use different fields)
var annotColorRaw = annotation.color || annotation.Color ||
annotation.strokeColor || annotation.StrokeColor ||
annotation.fillColor || annotation.FillColor ||
annotation.fontColor || annotation.FontColor || '';
// Normalize the annotation color to hex format
var annotColor = normalizeColorToHex(annotColorRaw);
// Normalize all filter colors to hex for comparison
var normalizedFilterColors = settings.color.map(function (c) { return normalizeColorToHex(c); });
if (!annotColor) {
return false;
}
// Check if the normalized annotation color matches any of the filter colors
if (annotColor && normalizedFilterColors.indexOf(annotColor) === -1) {
return false;
}
}
// Status filter (can be single value or array)
if (settings.status && Array.isArray(settings.status) && settings.status.length > 0) {
var annotStatus = annotation.state || annotation.State ||
annotation.stateModel || annotation.StateModel || annotation.review.state ||
'None';
// Handle both single status value and array of status values
var statusMatches = false;
if (Array.isArray(settings.status)) {
// If status is an array, check if annotation status is in the array
statusMatches = settings.status.indexOf(annotStatus) !== -1;
}
else {
// If status is a single value, compare directly
statusMatches = annotStatus === settings.status;
}
if (!statusMatches) {
return false;
}
}
// Author filter (parent only, not considering replies)
if (settings.author && Array.isArray(settings.author) && settings.author.length > 0) {
var annotAuthor = annotation.author || annotation.Author || '';
if (settings.author.indexOf(annotAuthor) === -1) {
return false;
}
}
// Modified date filter (collection-based, not range-based)
if (settings.modifiedDate && Array.isArray(settings.modifiedDate) && settings.modifiedDate.length > 0) {
var annotDate_1 = annotation.modifiedDate || annotation.ModifiedDate || '';
var annotDateOnly = new Date(annotDate_1).toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' });
var match = settings.modifiedDate.some(function (d) {
return new Date(d).toDateString() === new Date(annotDate_1).toDateString();
});
if (!match) {
return false;
}
}
return true;
};
/**
* Helper: Check if a reply (nested comment) matches thread-aware filter criteria.
* Supports both lowercase and capitalized field names.
* Checks author, status, and modifiedDate at reply level.
* Used only when thread-aware filtering (author, status, or modifiedDate) is active.
* @param {any} reply - The reply object to evaluate
* @returns {boolean} - Returns true if the reply matches filter criteri
*/
var replyMatchesFilter = function (reply) {
//AUTHOR filter
if (settings.author && Array.isArray(settings.author) && settings.author.length > 0) {
var replyAuthor = reply.author || reply.Author || '';
if (settings.author.indexOf(replyAuthor) === -1) {
return false;
}
}
//STATUS filter
if (settings.status && Array.isArray(settings.status) && settings.status.length > 0) {
var replyStatus = reply.state ||
reply.State ||
reply.stateModel ||
reply.StateModel ||
(reply.review && reply.review.state) ||
'None';
if (settings.status.indexOf(replyStatus) === -1) {
return false;
}
}
//DATE filter
if (settings.modifiedDate && Array.isArray(settings.modifiedDate) && settings.modifiedDate.length > 0) {
var replyDate_1 = reply.modifiedDate || reply.ModifiedDate || '';
var replyDateOnly = new Date(replyDate_1).toLocaleDateString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
});
var match = settings.modifiedDate.some(function (d) { return new Date(d).toDateString()
=== new Date(replyDate_1).toDateString(); });
if (!match) {
return false;
}
}
return true;
};
/**
* Helper: Check if a thread (parent + replies) should be displayed.
* Used only in comment panel (not document) when thread-aware filters are active.
* Thread-aware filters: author, status, modifiedDate.
*
* Implements includeReplies logic:
* - true: Show if parent matches OR any reply matches
* - false: Show only if parent matches
*
* Supports both lowercase and capitalized Comments/comments field names.
* @param {any} annotation - The parent annotation (thread root) to evaluate
* @returns {boolean} - Returns true if the thread should be displayed
*/
var threadMatchesFilter = function (annotation) {
//STRICT TYPE filter (annotation only)
if (settings.type && Array.isArray(settings.type) && settings.type.length > 0) {
var annotType = getAnnotationType(annotation);
var typeMatch = false;
for (var i = 0; i < settings.type.length; i++) {
// eslint-disable-next-line security/detect-object-injection
var mappedFilterType = mapMeasurementType(settings.type[i]);
// eslint-disable-next-line security/detect-object-injection
if (annotType === mappedFilterType || annotType === settings.type[i]) {
typeMatch = true;
break;
}
}
if (!typeMatch) {
return false;
}
}
//STRICT COLOR filter (annotation only)
if (settings.color && Array.isArray(settings.color) && settings.color.length > 0) {
var annotColorRaw = annotation.color || annotation.Color ||
annotation.strokeColor || annotation.StrokeColor ||
annotation.fillColor || annotation.FillColor ||
annotation.fontColor || annotation.FontColor || '';
var annotColor = normalizeColorToHex(annotColorRaw);
var normalizedFilterColors = settings.color.map(function (c) {
return normalizeColorToHex(c);
});
if (!annotColor || normalizedFilterColors.indexOf(annotColor) === -1) {
return false;
}
}
//STEP 3: check annotation-level filters (author/status/date)
if (matchesAnnotation(annotation)) {
return true;
}
// check replies (OR logic)
var replies = annotation.comments || annotation.Comments || [];
if (settings.includeReplies !== false && Array.isArray(replies)) {
for (var i = 0; i < replies.length; i++) {
// eslint-disable-next-line security/detect-object-injection
var reply = replies[i];
if (replyMatchesFilter(reply)) {
return true;
}
}
}
return false;
};
/**
* Helper: Check if any thread-aware filter is active.
* Thread-aware filters are: author, status, and modifiedDate.
* These filters can match against replies when includeReplies is enabled.
* @returns {boolean} Returns true if any thread-aware filter is active
*/
var isThreadAwareFilterActive = function () {
var hasAuthorFilter = settings.author && settings.author.length > 0;
var hasStatusFilter = settings.status && Array.isArray(settings.status) && settings.status.length > 0;
var hasModifiedDateFilter = settings.modifiedDate && settings.modifiedDate.length > 0;
return !!(hasAuthorFilter || hasStatusFilter || hasModifiedDateFilter);
};
/**
* Predicate for comment panel (includes reply thread logic)
*
* When thread-aware filtering is active (author, status, or modifiedDate),
* uses thread logic (parent + replies).
* Otherwise, uses simple annotation matching.
*
* Thread-aware filters include replies in results when includeReplies is enabled:
* - true: Show if parent matches OR any reply matches
* - false: Show only if parent matches
* @param {any} annotation - The annotation (thread root) to evaluate
* @returns {boolean} - Returns true if the annotation should be shown in the comment panel
*/
var commentPredicate = function (annotation) {
// If any thread-aware filter is active, use thread logic
if (isThreadAwareFilterActive()) {
return threadMatchesFilter(annotation);
}
// Otherwise, just match the annotation
return matchesAnnotation(annotation);
};
/**
* Predicate for document annotations.
*
* When includeReplies is true and thread-aware filters are active,
* use thread logic (parent + replies) to ensure consistency with comment panel.
* Otherwise, use basic annotation matching.
* @param {any} annotation - The annotation to evaluate
* @returns {boolean} Returns true if the annotation should be include
*/
var documentPredicate = function (annotation) {
// If includeReplies is enabled and thread-aware filters are active,
// use thread logic to ensure document visibility matches comment panel
if (settings.includeReplies !== false && isThreadAwareFilterActive()) {
return threadMatchesFilter(annotation);
}
// Otherwise, just match the annotation
return matchesAnnotation(annotation);
};
return {
commentPredicate: commentPredicate,
documentPredicate: documentPredicate
};
}
/**
* @hidden
* Helper function to check if a filter is empty (no criteria set).
* Used to determine if filtering is active.
*
* @param {CommentFilterSettings} settings - Filter settings to check
* @returns {boolean} true if filter has no criteria, false otherwise
*/
export function isFilterEmpty(settings) {
if (!settings) {
return true;
}
return ((!settings.type || settings.type.length === 0) &&
(!settings.color || settings.color.length === 0) &&
(!settings.status || (Array.isArray(settings.status) && settings.status.length === 0)) &&
(!settings.author || settings.author.length === 0) &&
(!settings.modifiedDate || settings.modifiedDate.length === 0));
}