From 33ae464fa7636235b26626a42d061a8a1e7610b8 Mon Sep 17 00:00:00 2001 From: Balaji-SF4331 Date: Mon, 8 Sep 2025 11:48:29 +0530 Subject: [PATCH 1/5] React Components release packages version 31.1.17 committed --- README.md | 71 +- components/base/src/animate.tsx | 767 +++ components/base/src/animation.tsx | 299 -- components/base/src/base.tsx | 186 - components/base/src/browser.tsx | 504 -- components/base/src/component.tsx | 91 - components/base/src/dom.tsx | 486 -- components/base/src/drag-util.tsx | 211 - components/base/src/dragdrop.tsx | 131 - components/base/src/draggable.tsx | 1212 ----- components/base/src/droppable.tsx | 268 -- components/base/src/enums.tsx | 162 + components/base/src/event-handler.tsx | 202 - components/base/src/fetch.tsx | 202 - components/base/src/hijri-parser.tsx | 226 - components/base/src/index.ts | 25 - components/base/src/internationalization.tsx | 312 -- components/base/src/intl/date-formatter.tsx | 334 -- components/base/src/intl/date-parser.tsx | 499 -- components/base/src/intl/index.ts | 8 - components/base/src/intl/intl-base.tsx | 1320 ------ components/base/src/intl/number-formatter.tsx | 456 -- components/base/src/intl/number-parser.tsx | 162 - components/base/src/intl/parser-base.tsx | 330 -- components/base/src/l10n.tsx | 92 - components/base/src/observer.tsx | 187 - components/base/src/provider.tsx | 59 - components/base/src/ripple.tsx | 315 -- components/base/src/sanitize-helper.tsx | 299 -- components/base/src/svg-icon.tsx | 67 - components/base/src/touch.tsx | 535 --- components/base/src/util.tsx | 418 -- components/base/src/validate-lic.tsx | 494 -- components/base/styles/_all.scss | 2 - .../styles/_material3-dark-definition.scss | 15 - .../base/styles/_material3-definition.scss | 16 - .../base/styles/border-radius/_radius.scss | 25 + components/base/styles/common/_all.scss | 2 - components/base/styles/common/_core.scss | 112 - .../styles/definition/_material3-dark.scss | 660 --- components/base/styles/functions/_all.scss | 2 + components/base/styles/functions/_calc.scss | 43 + components/base/styles/functions/_core.scss | 6 + components/base/styles/material3-dark.scss | 2 - components/base/styles/material3.scss | 2 - components/base/styles/mixins/_all.scss | 3 + .../{common/_mixin.scss => mixins/_core.scss} | 0 components/base/styles/mixins/_decorator.scss | 44 + components/base/styles/mixins/_radius.scss | 64 + components/base/styles/styles/_all.scss | 5 + .../_all.scss => styles/_animation.scss} | 39 - components/base/styles/styles/_base.scss | 77 + components/base/styles/styles/_layout.scss | 179 + components/base/styles/styles/_resize.scss | 100 + components/base/styles/styles/_theme.scss | 32 + .../{definition => themes}/_material3.scss | 370 +- .../base/styles/typography/_variables.scss | 84 + components/buttons/CHANGELOG.md | 108 - components/buttons/README.md | 143 - components/buttons/src/button/button.tsx | 313 -- components/buttons/src/button/index.ts | 4 - .../buttons/src/check-box/check-box.tsx | 350 -- components/buttons/src/check-box/index.ts | 4 - .../src/{chipList => chip-list}/chip-list.tsx | 126 +- .../src/{chipList => chip-list}/index.ts | 0 components/buttons/src/chip/chip.tsx | 338 -- components/buttons/src/chip/index.ts | 4 - .../floating-action-button.tsx | 223 - .../src/floating-action-button/index.ts | 4 - components/buttons/src/index.ts | 9 - components/buttons/src/radio-button/index.ts | 4 - .../buttons/src/radio-button/radio-button.tsx | 170 - components/buttons/styles/button/_layout.scss | 596 --- .../button/_material3-dark-definition.scss | 1 - .../styles/button/_material3-definition.scss | 375 -- components/buttons/styles/button/_mixin.scss | 480 -- components/buttons/styles/button/_theme.scss | 1687 ------- .../buttons/styles/button/material3-dark.scss | 2 - .../buttons/styles/check-box/_layout.scss | 816 ---- .../check-box/_material3-dark-definition.scss | 1 - .../check-box/_material3-definition.scss | 83 - .../styles/check-box/material3-dark.scss | 2 - components/buttons/styles/chips/_layout.scss | 192 - .../chips/_material3-dark-definition.scss | 1 - .../styles/chips/_material3-definition.scss | 564 --- components/buttons/styles/chips/_theme.scss | 451 -- .../buttons/styles/chips/material3-dark.scss | 2 - .../buttons/styles/chips/material3.scss | 2 - .../floating-action-button/_layout.scss | 140 - .../_material3-dark-definition.scss | 1 - .../_material3-definition.scss | 27 - .../styles/floating-action-button/_mixin.scss | 6 + .../styles/floating-action-button/_theme.scss | 24 - .../material3-dark.scss | 2 - components/buttons/styles/material3-dark.scss | 10 - components/buttons/styles/material3.scss | 10 - .../buttons/styles/radio-button/_layout.scss | 838 ---- .../_material3-dark-definition.scss | 1 - .../radio-button/_material3-definition.scss | 75 - .../styles/radio-button/material3-dark.scss | 2 - .../styles/radio-button/material3.scss | 2 - components/{base => calendars}/CHANGELOG.md | 0 components/calendars/README.md | 74 + components/{base => calendars}/gulpfile.js | 0 components/{buttons => calendars}/license | 0 .../{navigations => calendars}/package.json | 27 +- .../calendars/src/calendar/calendar-cell.tsx | 147 + .../calendars/src/calendar/calendar.tsx | 1588 +++++++ components/calendars/src/calendar/index.ts | 9 + .../calendars/src/datepicker/datepicker.tsx | 971 ++++ components/calendars/src/datepicker/index.ts | 4 + components/calendars/src/index.ts | 9 + .../calendars/src/utils/calendar-util.ts | 32 + components/calendars/src/utils/index.ts | 1 + .../styles/calendar}/_all.scss | 0 .../calendars/styles/calendar/_layout.scss | 401 ++ .../calendar/_material3-definition.scss | 153 + .../calendars/styles/calendar/_theme.scss | 295 ++ .../calendars/styles/calendar/material3.scss | 4 + .../styles/datepicker}/_all.scss | 1 + .../calendars/styles/datepicker/_layout.scss | 398 ++ .../datepicker/_material3-definition.scss | 88 + .../calendars/styles/datepicker/_theme.scss | 30 + .../styles/datepicker/material3.scss | 5 + components/calendars/styles/material3.scss | 10 + .../styles/timepicker}/_all.scss | 0 .../calendars/styles/timepicker/_layout.scss | 187 + .../timepicker/_material3-definition.scss | 83 + .../calendars/styles/timepicker/_theme.scss | 69 + .../styles/timepicker/material3.scss | 5 + .../{buttons => calendars}/tsconfig.json | 0 components/charts/CHANGELOG.md | 31 + components/charts/README.md | 63 + components/{buttons => charts}/gulpfile.js | 0 components/{inputs => charts}/license | 0 .../{notifications => charts}/package.json | 13 +- components/charts/src/chart/Chart.tsx | 116 + .../charts/src/chart/base/Legend-base.tsx | 351 ++ .../src/chart/base/default-properties.tsx | 717 +++ components/charts/src/chart/base/enum.tsx | 433 ++ .../charts/src/chart/base/interfaces.tsx | 3072 ++++++++++++ .../charts/src/chart/chart-area/ChartArea.tsx | 38 + .../src/chart/chart-area/ChartStackLabels.tsx | 42 + .../src/chart/chart-area/ChartSubTitle.tsx | 37 + .../src/chart/chart-area/ChartTitle.tsx | 37 + .../src/chart/chart-area/chart-interfaces.tsx | 3532 ++++++++++++++ .../charts/src/chart/chart-axis/ChartAxes.tsx | 129 + .../charts/src/chart/chart-axis/Columns.tsx | 45 + .../src/chart/chart-axis/LabelStyle.tsx | 13 + .../src/chart/chart-axis/MajorGridLines.tsx | 13 + .../src/chart/chart-axis/MajorTickLines.tsx | 14 + .../src/chart/chart-axis/MinorGridLines.tsx | 14 + .../src/chart/chart-axis/MinorTickLines.tsx | 13 + .../src/chart/chart-axis/PrimaryXAxis.tsx | 227 + .../src/chart/chart-axis/PrimaryYAxis.tsx | 101 + .../charts/src/chart/chart-axis/Rows.tsx | 52 + .../src/chart/chart-axis/StripLines.tsx | 26 + .../src/chart/chart-axis/TitleStyle.tsx | 14 + .../charts/src/chart/chart-axis/base.tsx | 291 ++ .../src/chart/chart-legend/ChartLegend.tsx | 36 + .../src/chart/chart-tooltip/ChartTooltip.tsx | 51 + components/charts/src/chart/common/base.tsx | 117 + components/charts/src/chart/common/data.tsx | 45 + .../charts/src/chart/hooks/useClipRect.tsx | 394 ++ .../charts/src/chart/hooks/useDeepCompare.tsx | 140 + components/charts/src/chart/index.ts | 25 + .../charts/src/chart/layout/ChartProvider.tsx | 155 + .../charts/src/chart/layout/LayoutContext.tsx | 1021 ++++ .../AxesRenderer/AxisOutsideRenderer.tsx | 84 + .../renderer/AxesRenderer/AxisRender.tsx | 571 +++ .../AxisTypeRenderer/AxisUtils.tsx | 294 ++ .../AxisTypeRenderer/CategoryAxisRenderer.tsx | 132 + .../AxisTypeRenderer/DateTimeAxisRenderer.tsx | 661 +++ .../AxisTypeRenderer/DoubleAxisRenderer.tsx | 642 +++ .../LogarithmicAxisRenderer.tsx | 199 + .../AxesRenderer/CartesianLayoutRender.tsx | 1891 ++++++++ .../AxesRenderer/ChartColumnsRender.tsx | 62 + .../renderer/AxesRenderer/ChartRowsRender.tsx | 62 + .../AxesRenderer/ChartStripLinesRender.tsx | 885 ++++ .../src/chart/renderer/ChartAreaRender.tsx | 96 + .../src/chart/renderer/ChartRenderer.tsx | 286 ++ .../renderer/ChartStackLabelsRenderer.tsx | 439 ++ .../chart/renderer/ChartSubtitleRender.tsx | 269 ++ .../src/chart/renderer/ChartTitleRenderer.tsx | 261 ++ .../LegendRenderer/ChartLegendRenderer.tsx | 795 ++++ .../renderer/LegendRenderer/CommonLegend.tsx | 1157 +++++ .../SeriesRenderer/AreaSeriesRenderer.tsx | 494 ++ .../SeriesRenderer/BarSeriesRenderer.tsx | 215 + .../SeriesRenderer/BubbleSeriesRenderer.tsx | 231 + .../renderer/SeriesRenderer/ColumnBase.tsx | 533 +++ .../SeriesRenderer/ColumnSeriesRenderer.tsx | 287 ++ .../SeriesRenderer/DataLabelRender.tsx | 1198 +++++ .../renderer/SeriesRenderer/LineBase.tsx | 228 + .../renderer/SeriesRenderer/MarkerBase.tsx | 43 + .../SeriesRenderer/MarkerRenderer.tsx | 779 +++ .../renderer/SeriesRenderer/ProcessData.tsx | 741 +++ .../SeriesRenderer/ScatterSeriesRenderer.tsx | 280 ++ .../SeriesRenderer/SeriesAnimation.tsx | 1069 +++++ .../SeriesRenderer/SeriesRenderer.tsx | 1091 +++++ .../SplineAreaSeriesRenderer.tsx | 833 ++++ .../SeriesRenderer/SplineSeriesRenderer.tsx | 618 +++ .../StackingBarSeriesRenderer.tsx | 201 + .../StackingColumnSeriesRenderer.tsx | 227 + .../SeriesRenderer/StepLineSeriesRenderer.tsx | 221 + .../SeriesRenderer/lineSeriesRenderer.tsx | 203 + .../renderer/SeriesRenderer/updatePoint.tsx | 239 + .../src/chart/renderer/TooltipRenderer.tsx | 933 ++++ .../src/chart/renderer/TrackballRenderer.tsx | 962 ++++ .../chart/renderer/Zooming/zoom-toolbar.tsx | 1098 +++++ .../src/chart/renderer/Zooming/zooming.tsx | 1457 ++++++ .../charts/src/chart/series/DataLabel.tsx | 14 + components/charts/src/chart/series/Marker.tsx | 20 + components/charts/src/chart/series/Series.tsx | 371 ++ .../charts/src/chart/utils/constants.ts | 20 + components/charts/src/chart/utils/getData.tsx | 201 + components/charts/src/chart/utils/helper.tsx | 1740 +++++++ components/charts/src/chart/utils/theme.tsx | 219 + .../charts/src/chart/zooming/ChartZooming.tsx | 34 + components/charts/src/index.ts | 1 + components/{inputs => charts}/tsconfig.json | 0 components/data/CHANGELOG.md | 3 + components/{base => data}/README.md | 20 +- components/{icons => data}/gulpfile.js | 0 components/{base => data}/license | 0 components/{base => data}/package.json | 15 +- components/data/src/adaptors.ts | 3037 ++++++++++++ components/data/src/index.ts | 7 + components/data/src/manager.ts | 1004 ++++ components/data/src/query.ts | 865 ++++ components/data/src/schema.ts | 27 + components/data/src/util.ts | 2582 ++++++++++ components/{popups => data}/tsconfig.json | 0 components/dropdowns/CHANGELOG.md | 3 + components/dropdowns/README.md | 63 + components/{inputs => dropdowns}/gulpfile.js | 0 components/{navigations => dropdowns}/license | 0 .../{splitbuttons => dropdowns}/package.json | 18 +- .../dropdowns/src/common/drop-down-base.tsx | 948 ++++ components/dropdowns/src/common/index.ts | 4 + .../src/drop-down-list/drop-down-list.tsx | 1265 +++++ .../dropdowns/src/drop-down-list/index.ts | 1 + .../src/dropdowns/drop-down-base}/_all.scss | 0 .../src/dropdowns/drop-down-base/_layout.scss | 124 + .../drop-down-base/_material3-definition.scss | 73 + .../src/dropdowns/drop-down-base/_theme.scss | 183 + .../src/dropdowns/drop-down-list}/_all.scss | 0 .../src/dropdowns/drop-down-list/_layout.scss | 280 ++ .../drop-down-list/_material3-definition.scss | 118 + .../src/dropdowns/drop-down-list/_theme.scss | 14 + components/dropdowns/src/index.ts | 2 + .../styles/drop-down-base}/_all.scss | 0 .../styles/drop-down-base/_layout.scss | 124 + .../drop-down-base/_material3-definition.scss | 73 + .../styles/drop-down-base/_theme.scss | 183 + .../styles/drop-down-base}/material3.scss | 1 + .../styles/drop-down-list}/_all.scss | 0 .../styles/drop-down-list/_layout.scss | 280 ++ .../drop-down-list/_material3-definition.scss | 118 + .../styles/drop-down-list/_theme.scss | 14 + .../styles/drop-down-list}/material3.scss | 1 + components/dropdowns/styles/material3.scss | 5 + .../{splitbuttons => dropdowns}/tsconfig.json | 2 +- components/grids/CHANGELOG.md | 29 + components/grids/README.md | 91 + components/{navigations => grids}/gulpfile.js | 0 components/{notifications => grids}/license | 0 components/{popups => grids}/package.json | 33 +- .../grids/src/grid/components/Column.tsx | 399 ++ components/grids/src/grid/components/Grid.tsx | 313 ++ components/grids/src/grid/components/Row.tsx | 377 ++ components/grids/src/grid/components/index.ts | 3 + .../grids/src/grid/contexts/GridProviders.tsx | 71 + components/grids/src/grid/contexts/index.ts | 1 + .../src/grid/grid}/_all.scss | 0 components/grids/src/grid/grid/_layout.scss | 727 +++ .../src/grid/grid/_material3-definition.scss | 119 + components/grids/src/grid/grid/_theme.scss | 92 + components/grids/src/grid/hooks/index.ts | 12 + components/grids/src/grid/hooks/useColumn.ts | 316 ++ components/grids/src/grid/hooks/useEdit.ts | 1505 ++++++ .../grids/src/grid/hooks/useEditDialog.ts | 84 + components/grids/src/grid/hooks/useFilter.ts | 746 +++ .../grids/src/grid/hooks/useFocusStrategy.ts | 1788 +++++++ components/grids/src/grid/hooks/useGrid.tsx | 1510 ++++++ components/grids/src/grid/hooks/useRender.tsx | 727 +++ components/grids/src/grid/hooks/useScroll.tsx | 314 ++ components/grids/src/grid/hooks/useSearch.ts | 94 + .../grids/src/grid/hooks/useSelection.ts | 584 +++ components/grids/src/grid/hooks/useSort.ts | 234 + components/grids/src/grid/hooks/useToolbar.ts | 288 ++ components/grids/src/grid/index.ts | 8 + components/grids/src/grid/models/index.ts | 1 + components/grids/src/grid/models/useData.ts | 380 ++ components/grids/src/grid/services/index.ts | 2 + .../src/grid/services/service-locator.ts | 42 + .../src/grid/services/value-formatter.ts | 71 + .../src/grid/types/aggregate.interfaces.ts | 268 ++ .../grids/src/grid/types/column.interfaces.ts | 1009 ++++ .../grids/src/grid/types/edit.interfaces.ts | 1005 ++++ components/grids/src/grid/types/enum.ts | 524 +++ .../grids/src/grid/types/filter.interfaces.ts | 740 +++ .../grids/src/grid/types/focus.interfaces.ts | 796 ++++ .../grids/src/grid/types/grid.interfaces.ts | 2173 +++++++++ components/grids/src/grid/types/index.ts | 13 + components/grids/src/grid/types/interfaces.ts | 1460 ++++++ .../grids/src/grid/types/page.interfaces.ts | 142 + .../grids/src/grid/types/search.interfaces.ts | 138 + .../src/grid/types/selection.interfaces.ts | 291 ++ .../grids/src/grid/types/sort.interfaces.ts | 247 + .../src/grid/types/toolbar.interfaces.ts | 372 ++ components/grids/src/grid/utils/index.ts | 1 + components/grids/src/grid/utils/utils.ts | 435 ++ components/grids/src/grid/views/Aggregate.tsx | 44 + .../grids/src/grid/views/ContentPanel.tsx | 122 + .../grids/src/grid/views/ContentRows.tsx | 387 ++ .../grids/src/grid/views/ContentTable.tsx | 106 + components/grids/src/grid/views/FilterBar.tsx | 208 + .../grids/src/grid/views/FooterPanel.tsx | 112 + .../grids/src/grid/views/FooterRows.tsx | 239 + .../grids/src/grid/views/FooterTable.tsx | 102 + .../grids/src/grid/views/HeaderPanel.tsx | 112 + .../grids/src/grid/views/HeaderRows.tsx | 173 + .../grids/src/grid/views/HeaderTable.tsx | 100 + .../grids/src/grid/views/PagerPanel.tsx | 85 + components/grids/src/grid/views/Render.tsx | 241 + .../src/grid/views/editing/ConfirmDialog.tsx | 95 + .../grids/src/grid/views/editing/EditCell.tsx | 365 ++ .../src/grid/views/editing/InlineEditForm.tsx | 769 +++ .../grids/src/grid/views/editing/ToolBar.tsx | 381 ++ .../grid/views/editing/ValidationTooltips.tsx | 104 + components/grids/src/grid/views/index.ts | 15 + components/grids/src/index.ts | 1 + .../tooltip => grids/styles/grid}/_all.scss | 0 components/grids/styles/grid/_layout.scss | 727 +++ .../styles/grid/_material3-definition.scss | 119 + components/grids/styles/grid/_theme.scss | 92 + components/grids/styles/grid/material3.scss | 15 + components/grids/styles/material3.scss | 15 + .../{navigations => grids}/tsconfig.json | 2 +- components/icons/CHANGELOG.MD | 13 - components/icons/CHANGELOG.md | 3 + components/icons/src/icon.tsx | 47 - components/icons/src/icons/above-average.tsx | 5 - components/icons/src/icons/activities.tsx | 4 - .../icons/src/icons/add-chart-element.tsx | 4 - components/icons/src/icons/add-notes.tsx | 4 - components/icons/src/icons/adjustment.tsx | 4 - .../icons/src/icons/agenda-date-range.tsx | 4 - components/icons/src/icons/ai-chat.tsx | 4 - components/icons/src/icons/align-bottom.tsx | 4 - components/icons/src/icons/align-center.tsx | 4 - components/icons/src/icons/align-left.tsx | 4 - components/icons/src/icons/align-middle.tsx | 4 - components/icons/src/icons/align-right.tsx | 4 - components/icons/src/icons/align-top.tsx | 4 - components/icons/src/icons/all.tsx | 4 - components/icons/src/icons/animals.tsx | 4 - .../icons/src/icons/annotation-edit.tsx | 4 - components/icons/src/icons/area.tsx | 4 - components/icons/src/icons/arrow-down.tsx | 4 - .../icons/src/icons/arrow-head-fill.tsx | 4 - components/icons/src/icons/arrow-head.tsx | 4 - components/icons/src/icons/arrow-left.tsx | 4 - components/icons/src/icons/arrow-right-up.tsx | 4 - components/icons/src/icons/arrow-right.tsx | 4 - .../icons/src/icons/arrow-tail-fill.tsx | 4 - components/icons/src/icons/arrow-tail.tsx | 4 - components/icons/src/icons/arrow-up.tsx | 4 - components/icons/src/icons/audio.tsx | 4 - .../icons/src/icons/auto-fit-all-column.tsx | 4 - .../icons/src/icons/auto-fit-column.tsx | 4 - .../icons/src/icons/auto-fit-content.tsx | 4 - .../icons/src/icons/auto-fit-window.tsx | 4 - components/icons/src/icons/bar-head.tsx | 4 - components/icons/src/icons/bar-tail.tsx | 4 - components/icons/src/icons/below-average.tsx | 4 - components/icons/src/icons/between.tsx | 4 - components/icons/src/icons/blockquote.tsx | 4 - components/icons/src/icons/bold.tsx | 4 - components/icons/src/icons/bookmark.tsx | 4 - components/icons/src/icons/border-all.tsx | 4 - components/icons/src/icons/border-bottom.tsx | 4 - components/icons/src/icons/border-box.tsx | 4 - components/icons/src/icons/border-center.tsx | 4 - components/icons/src/icons/border-custom.tsx | 4 - ...al-1.tsx => border-diagonal-backslash.tsx} | 2 +- .../icons/src/icons/border-diagonal-down.tsx | 4 - ...agonal-2.tsx => border-diagonal-slash.tsx} | 2 +- .../icons/src/icons/border-diagonal-up.tsx | 4 - components/icons/src/icons/border-frame.tsx | 4 - ...r-shadow-2.tsx => border-inner-shadow.tsx} | 2 +- components/icons/src/icons/border-inner.tsx | 4 - components/icons/src/icons/border-left.tsx | 4 - components/icons/src/icons/border-middle.tsx | 4 - ...der-none-1.tsx => border-none-content.tsx} | 2 +- components/icons/src/icons/border-none.tsx | 4 - ...r-shadow-1.tsx => border-outer-shadow.tsx} | 2 +- components/icons/src/icons/border-outer.tsx | 4 - components/icons/src/icons/border-right.tsx | 4 - components/icons/src/icons/border-top.tsx | 4 - .../icons/src/icons/bottom-10-items.tsx | 4 - components/icons/src/icons/bottom-10.tsx | 4 - components/icons/src/icons/box.tsx | 4 - components/icons/src/icons/break-page.tsx | 4 - components/icons/src/icons/break-section.tsx | 4 - components/icons/src/icons/break.tsx | 4 - components/icons/src/icons/brightness.tsx | 4 - components/icons/src/icons/bring-forward.tsx | 4 - .../icons/src/icons/bring-to-center.tsx | 4 - components/icons/src/icons/bring-to-front.tsx | 4 - components/icons/src/icons/bring-to-view.tsx | 4 - components/icons/src/icons/building-block.tsx | 4 - .../icons/{bullet-6.tsx => bullet-arrow.tsx} | 2 +- .../icons/{bullet-5.tsx => bullet-check.tsx} | 2 +- ...bullet-1.tsx => bullet-circle-outline.tsx} | 2 +- .../icons/{bullet-2.tsx => bullet-circle.tsx} | 2 +- .../{bullet-4.tsx => bullet-diamond.tsx} | 2 +- .../icons/{bullet-7.tsx => bullet-point.tsx} | 2 +- .../icons/{bullet-3.tsx => bullet-square.tsx} | 2 +- components/icons/src/icons/button-field.tsx | 4 - .../icons/src/icons/calculate-sheet.tsx | 4 - .../icons/src/icons/calculated-member.tsx | 4 - components/icons/src/icons/calculation.tsx | 4 - components/icons/src/icons/caption.tsx | 4 - components/icons/src/icons/cell.tsx | 4 - components/icons/src/icons/change-case.tsx | 4 - .../icons/src/icons/change-scale-ratio.tsx | 4 - components/icons/src/icons/changes-accept.tsx | 4 - components/icons/src/icons/changes-reject.tsx | 4 - components/icons/src/icons/changes-track.tsx | 4 - .../icons/src/icons/character-style.tsx | 4 - .../chart-2d-100-percent-stacked-area.tsx | 4 - .../chart-2d-100-percent-stacked-bar.tsx | 4 - .../chart-2d-100-percent-stacked-column.tsx | 4 - ...art-2d-100-percent-stacked-line-marked.tsx | 4 - .../chart-2d-100-percent-stacked-line.tsx | 4 - components/icons/src/icons/chart-2d-area.tsx | 4 - .../src/icons/chart-2d-clustered-bar.tsx | 4 - .../src/icons/chart-2d-clustered-column.tsx | 4 - .../icons/src/icons/chart-2d-line-marked.tsx | 4 - components/icons/src/icons/chart-2d-line.tsx | 4 - components/icons/src/icons/chart-2d-pie-2.tsx | 4 - .../icons/src/icons/chart-2d-stacked-area.tsx | 4 - .../icons/src/icons/chart-2d-stacked-bar.tsx | 4 - .../src/icons/chart-2d-stacked-column.tsx | 4 - .../icons/chart-2d-stacked-line-marked.tsx | 4 - .../icons/src/icons/chart-2d-stacked-line.tsx | 4 - .../icons/chart-axes-primary-horizontal.tsx | 4 - .../src/icons/chart-axes-primary-vertical.tsx | 4 - components/icons/src/icons/chart-axes.tsx | 4 - .../chart-axis-titles-primary-horizontal.tsx | 4 - .../chart-axis-titles-primary-vertical.tsx | 4 - .../icons/src/icons/chart-axis-titles.tsx | 4 - .../src/icons/chart-data-labels-center.tsx | 4 - .../icons/chart-data-labels-inside-base.tsx | 4 - .../icons/chart-data-labels-inside-end.tsx | 4 - .../src/icons/chart-data-labels-none.tsx | 4 - .../icons/chart-data-labels-outside-end.tsx | 4 - .../icons/src/icons/chart-data-labels.tsx | 4 - components/icons/src/icons/chart-donut.tsx | 4 - .../icons/src/icons/chart-gridlines.tsx | 4 - .../icons/src/icons/chart-insert-bar.tsx | 4 - .../icons/src/icons/chart-insert-column.tsx | 4 - .../icons/src/icons/chart-insert-line.tsx | 4 - .../icons/src/icons/chart-insert-pie.tsx | 4 - .../src/icons/chart-insert-x-y-scatter.tsx | 4 - .../icons/src/icons/chart-legend-bottom.tsx | 4 - .../icons/src/icons/chart-legend-left.tsx | 4 - .../icons/src/icons/chart-legend-none.tsx | 4 - .../icons/src/icons/chart-legend-right.tsx | 4 - .../icons/src/icons/chart-legend-top.tsx | 4 - components/icons/src/icons/chart-legend.tsx | 4 - components/icons/src/icons/chart-lines.tsx | 4 - .../icons/chart-primary-major-horizontal.tsx | 4 - .../icons/chart-primary-major-vertical.tsx | 4 - .../icons/chart-primary-minor-horizontal.tsx | 4 - .../icons/chart-primary-minor-vertical.tsx | 4 - .../src/icons/chart-switch-row-column.tsx | 4 - .../icons/chart-title-centered-overlay.tsx | 4 - .../icons/src/icons/chart-title-none.tsx | 4 - components/icons/src/icons/chart-title.tsx | 4 - components/icons/src/icons/chart.tsx | 4 - components/icons/src/icons/check-box.tsx | 4 - components/icons/src/icons/check-large.tsx | 4 - components/icons/src/icons/check-small.tsx | 4 - components/icons/src/icons/check-tick.tsx | 4 - components/icons/src/icons/check.tsx | 4 - ...te-state.tsx => checkbox-intermediate.tsx} | 2 +- .../icons/src/icons/chevron-down-double.tsx | 4 - .../icons/src/icons/chevron-down-fill.tsx | 4 - .../icons/src/icons/chevron-down-small.tsx | 4 - components/icons/src/icons/chevron-down.tsx | 4 - .../icons/src/icons/chevron-left-double.tsx | 4 - .../icons/src/icons/chevron-left-fill.tsx | 4 - .../icons/src/icons/chevron-left-small.tsx | 4 - components/icons/src/icons/chevron-left.tsx | 4 - .../icons/src/icons/chevron-right-double.tsx | 4 - .../icons/src/icons/chevron-right-fill.tsx | 4 - .../icons/src/icons/chevron-right-small.tsx | 4 - components/icons/src/icons/chevron-right.tsx | 4 - .../icons/src/icons/chevron-up-double.tsx | 4 - .../icons/src/icons/chevron-up-fill.tsx | 4 - .../icons/src/icons/chevron-up-small.tsx | 4 - components/icons/src/icons/chevron-up.tsx | 4 - components/icons/src/icons/circle-add.tsx | 4 - components/icons/src/icons/circle-check.tsx | 4 - components/icons/src/icons/circle-close.tsx | 4 - .../icons/src/icons/circle-head-fill.tsx | 4 - components/icons/src/icons/circle-head.tsx | 4 - components/icons/src/icons/circle-info.tsx | 4 - components/icons/src/icons/circle-remove.tsx | 4 - .../icons/src/icons/circle-tail-fill.tsx | 4 - components/icons/src/icons/circle-tail.tsx | 4 - components/icons/src/icons/circle.tsx | 4 - components/icons/src/icons/clear-form.tsx | 4 - components/icons/src/icons/clear-format.tsx | 4 - components/icons/src/icons/clear-rules.tsx | 4 - components/icons/src/icons/clock.tsx | 4 - components/icons/src/icons/close.tsx | 4 - components/icons/src/icons/code-view.tsx | 4 - .../icons/{collapse-2.tsx => collapse.tsx} | 2 +- components/icons/src/icons/color-scales.tsx | 4 - components/icons/src/icons/columns.tsx | 4 - components/icons/src/icons/combo-box.tsx | 4 - components/icons/src/icons/comment-add.tsx | 4 - components/icons/src/icons/comment-reopen.tsx | 4 - .../icons/src/icons/comment-resolve.tsx | 4 - components/icons/src/icons/comment-show.tsx | 4 - .../icons/conditional-formatting-large.tsx | 4 - .../src/icons/conditional-formatting.tsx | 4 - .../icons/src/icons/content-control.tsx | 4 - .../icons/src/icons/continue-numbering.tsx | 4 - .../icons/src/icons/continuous-page-break.tsx | 4 - components/icons/src/icons/contrast.tsx | 4 - components/icons/src/icons/copy.tsx | 4 - components/icons/src/icons/critical-path.tsx | 4 - components/icons/src/icons/crop.tsx | 4 - components/icons/src/icons/cut.tsx | 4 - components/icons/src/icons/data-bars.tsx | 4 - .../icons/src/icons/data-validation.tsx | 4 - components/icons/src/icons/date-occurring.tsx | 4 - components/icons/src/icons/day.tsx | 4 - .../icons/src/icons/decrease-indent-rtl.tsx | 4 - .../icons/src/icons/decrease-indent.tsx | 4 - components/icons/src/icons/delete-column.tsx | 4 - components/icons/src/icons/delete-notes.tsx | 4 - components/icons/src/icons/delete-row.tsx | 4 - components/icons/src/icons/description.tsx | 4 - components/icons/src/icons/dimension.tsx | 4 - components/icons/src/icons/display.tsx | 4 - components/icons/src/icons/double-check.tsx | 4 - components/icons/src/icons/download.tsx | 4 - .../src/icons/drag-and-drop-indicator.tsx | 4 - components/icons/src/icons/drag-and-drop.tsx | 4 - components/icons/src/icons/drag-fill.tsx | 4 - components/icons/src/icons/drop-down.tsx | 4 - components/icons/src/icons/dropdown-list.tsx | 4 - components/icons/src/icons/duplicate-cell.tsx | 4 - components/icons/src/icons/duplicate.tsx | 4 - components/icons/src/icons/edit-notes.tsx | 4 - components/icons/src/icons/edit.tsx | 4 - components/icons/src/icons/elaborate.tsx | 4 - components/icons/src/icons/emoji.tsx | 4 - components/icons/src/icons/end-footnote.tsx | 4 - components/icons/src/icons/equalto.tsx | 4 - components/icons/src/icons/erase.tsx | 4 - components/icons/src/icons/error-treeview.tsx | 4 - .../icons/src/icons/even-page-break.tsx | 4 - .../icons/src/icons/exit-full-screen.tsx | 4 - components/icons/src/icons/expand.tsx | 4 - components/icons/src/icons/export-csv.tsx | 4 - components/icons/src/icons/export-excel.tsx | 4 - ...{export-pdf-1.tsx => export-pdf-arrow.tsx} | 2 +- components/icons/src/icons/export-pdf.tsx | 4 - components/icons/src/icons/export-png.tsx | 4 - components/icons/src/icons/export-svg.tsx | 4 - .../{export-word-1.tsx => export-word.tsx} | 2 +- components/icons/src/icons/export-xls.tsx | 4 - components/icons/src/icons/export.tsx | 4 - components/icons/src/icons/eye-slash.tsx | 4 - components/icons/src/icons/eye.tsx | 4 - components/icons/src/icons/fade.tsx | 4 - components/icons/src/icons/field-settings.tsx | 4 - components/icons/src/icons/file-document.tsx | 4 - components/icons/src/icons/file-new.tsx | 4 - components/icons/src/icons/filter-active.tsx | 4 - components/icons/src/icons/filter-clear.tsx | 4 - components/icons/src/icons/filter-main.tsx | 4 - components/icons/src/icons/filter.tsx | 4 - .../src/icons/filtered-sort-ascending.tsx | 4 - .../src/icons/filtered-sort-descending.tsx | 4 - components/icons/src/icons/filtered.tsx | 4 - components/icons/src/icons/filters.tsx | 4 - components/icons/src/icons/first-page.tsx | 4 - .../icons/src/icons/fixed-column-width.tsx | 4 - components/icons/src/icons/flags.tsx | 4 - .../icons/src/icons/flip-horizontal.tsx | 4 - components/icons/src/icons/flip-vertical.tsx | 4 - components/icons/src/icons/folder-open.tsx | 4 - components/icons/src/icons/folder.tsx | 4 - components/icons/src/icons/font-color.tsx | 4 - components/icons/src/icons/font-name.tsx | 4 - components/icons/src/icons/font-size.tsx | 4 - .../icons/src/icons/food-and-drinks.tsx | 4 - components/icons/src/icons/footer.tsx | 4 - components/icons/src/icons/form-field.tsx | 4 - components/icons/src/icons/format-painter.tsx | 4 - components/icons/src/icons/frame-bevel.tsx | 4 - components/icons/src/icons/frame-custom.tsx | 4 - .../{frame-11.tsx => frame-extra-tall.tsx} | 2 +- .../{frame-5.tsx => frame-full-width.tsx} | 2 +- components/icons/src/icons/frame-hook.tsx | 4 - ...rame-2.tsx => frame-horizontal-narrow.tsx} | 2 +- components/icons/src/icons/frame-inset.tsx | 4 - .../icons/{frame-4.tsx => frame-large.tsx} | 2 +- components/icons/src/icons/frame-line.tsx | 4 - components/icons/src/icons/frame-mat.tsx | 4 - .../icons/{frame-3.tsx => frame-medium.tsx} | 2 +- .../icons/{frame-6.tsx => frame-narrow.tsx} | 2 +- components/icons/src/icons/frame-none.tsx | 4 - .../icons/{frame-1.tsx => frame-square.tsx} | 2 +- .../icons/{frame-10.tsx => frame-tall.tsx} | 2 +- ...{frame-7.tsx => frame-vertical-narrow.tsx} | 2 +- .../{frame-9.tsx => frame-vertical-wide.tsx} | 2 +- .../icons/{frame-8.tsx => frame-vertical.tsx} | 2 +- components/icons/src/icons/freeze-column.tsx | 4 - components/icons/src/icons/freeze-pane.tsx | 4 - components/icons/src/icons/freeze-row.tsx | 4 - components/icons/src/icons/full-screen.tsx | 4 - components/icons/src/icons/gantt-gripper.tsx | 4 - components/icons/src/icons/grain.tsx | 4 - components/icons/src/icons/grammar-check.tsx | 4 - components/icons/src/icons/grand-total.tsx | 4 - components/icons/src/icons/greater-than.tsx | 4 - components/icons/src/icons/grid-view.tsx | 4 - components/icons/src/icons/grip-vertical.tsx | 4 - .../icons/{group-2.tsx => group-combine.tsx} | 2 +- components/icons/src/icons/group-icon.tsx | 4 - .../icons/{group-1.tsx => group-items.tsx} | 2 +- components/icons/src/icons/hand-gestures.tsx | 4 - components/icons/src/icons/header.tsx | 4 - .../icons/src/icons/hide-formula-bar.tsx | 4 - components/icons/src/icons/hide-gridlines.tsx | 4 - components/icons/src/icons/hide-headings.tsx | 4 - .../icons/src/icons/highlight-color.tsx | 4 - components/icons/src/icons/highlight.tsx | 4 - components/icons/src/icons/home.tsx | 4 - components/icons/src/icons/hyperlink-copy.tsx | 4 - components/icons/src/icons/hyperlink-edit.tsx | 4 - components/icons/src/icons/hyperlink-open.tsx | 4 - .../icons/src/icons/hyperlink-remove.tsx | 4 - components/icons/src/icons/iconsets.tsx | 4 - .../{caption-1.tsx => image-caption.tsx} | 2 +- components/icons/src/icons/image.tsx | 4 - .../icons/{import-1.tsx => import-turbo.tsx} | 2 +- components/icons/src/icons/import-word.tsx | 4 - components/icons/src/icons/import.tsx | 4 - .../icons/src/icons/increase-indent-rtl.tsx | 4 - .../icons/src/icons/increase-indent.tsx | 4 - components/icons/src/icons/index.ts | 544 --- components/icons/src/icons/insert-above.tsx | 4 - components/icons/src/icons/insert-below.tsx | 4 - components/icons/src/icons/insert-code.tsx | 4 - components/icons/src/icons/insert-left.tsx | 4 - components/icons/src/icons/insert-right.tsx | 4 - components/icons/src/icons/insert-sheet.tsx | 4 - ...diate-state-2.tsx => intermediate-bar.tsx} | 2 +- components/icons/src/icons/italic.tsx | 4 - components/icons/src/icons/justify.tsx | 4 - components/icons/src/icons/kpi.tsx | 4 - components/icons/src/icons/last-page.tsx | 4 - components/icons/src/icons/launcher.tsx | 4 - components/icons/src/icons/layers.tsx | 4 - components/icons/src/icons/length.tsx | 4 - components/icons/src/icons/less-than.tsx | 4 - components/icons/src/icons/level-1.tsx | 4 - components/icons/src/icons/level-2.tsx | 4 - components/icons/src/icons/level-3.tsx | 4 - components/icons/src/icons/level-4.tsx | 4 - components/icons/src/icons/level-5.tsx | 4 - components/icons/src/icons/line-normal.tsx | 4 - components/icons/src/icons/line-small.tsx | 4 - components/icons/src/icons/line-spacing.tsx | 4 - .../icons/src/icons/line-very-small.tsx | 4 - components/icons/src/icons/line.tsx | 4 - components/icons/src/icons/link-remove.tsx | 4 - components/icons/src/icons/link.tsx | 4 - components/icons/src/icons/linked-style.tsx | 4 - .../icons/src/icons/list-ordered-rtl.tsx | 4 - components/icons/src/icons/list-ordered.tsx | 4 - .../icons/src/icons/list-unordered-rtl.tsx | 4 - components/icons/src/icons/list-unordered.tsx | 4 - components/icons/src/icons/location.tsx | 4 - components/icons/src/icons/lock.tsx | 4 - components/icons/src/icons/lower-case.tsx | 4 - components/icons/src/icons/mdx.tsx | 4 - components/icons/src/icons/menu.tsx | 4 - components/icons/src/icons/merge-cells.tsx | 4 - components/icons/src/icons/microphone.tsx | 4 - components/icons/src/icons/month-agenda.tsx | 4 - components/icons/src/icons/month.tsx | 4 - components/icons/src/icons/more-chevron.tsx | 4 - ...ontal-1.tsx => more-horizontal-filled.tsx} | 2 +- .../icons/src/icons/more-scatter-charts.tsx | 4 - ...ertical-1.tsx => more-vertical-filled.tsx} | 2 +- ...tical-2.tsx => more-vertical-outlined.tsx} | 2 +- components/icons/src/icons/mouse-pointer.tsx | 4 - .../src/icons/multiple-comment-resolve.tsx | 4 - .../icons/src/icons/multiple-comment.tsx | 4 - components/icons/src/icons/named-set.tsx | 4 - components/icons/src/icons/nature.tsx | 4 - components/icons/src/icons/none.tsx | 4 - components/icons/src/icons/notes.tsx | 4 - .../icons/src/icons/number-formatting.tsx | 4 - components/icons/src/icons/objects.tsx | 4 - components/icons/src/icons/odd-page-break.tsx | 4 - components/icons/src/icons/opacity.tsx | 4 - components/icons/src/icons/open-link.tsx | 4 - components/icons/src/icons/order.tsx | 4 - components/icons/src/icons/organize-pdf.tsx | 4 - .../icons/src/icons/page-column-left.tsx | 4 - .../icons/src/icons/page-column-one.tsx | 4 - .../icons/src/icons/page-column-right.tsx | 4 - .../icons/src/icons/page-column-three.tsx | 4 - .../icons/src/icons/page-column-two.tsx | 4 - components/icons/src/icons/page-column.tsx | 4 - components/icons/src/icons/page-columns.tsx | 4 - components/icons/src/icons/page-numbering.tsx | 4 - components/icons/src/icons/page-setup.tsx | 4 - components/icons/src/icons/page-size.tsx | 4 - components/icons/src/icons/page-text-wrap.tsx | 4 - components/icons/src/icons/paint-bucket.tsx | 4 - components/icons/src/icons/pan.tsx | 4 - components/icons/src/icons/paragraph.tsx | 4 - components/icons/src/icons/password.tsx | 4 - .../src/icons/paste-match-destination.tsx | 4 - components/icons/src/icons/paste-style.tsx | 4 - .../icons/src/icons/paste-text-only.tsx | 4 - components/icons/src/icons/paste.tsx | 4 - components/icons/src/icons/pause.tsx | 4 - components/icons/src/icons/pentagon.tsx | 4 - components/icons/src/icons/people.tsx | 4 - components/icons/src/icons/perimeter.tsx | 4 - components/icons/src/icons/play.tsx | 4 - components/icons/src/icons/plus.tsx | 4 - components/icons/src/icons/preformat-code.tsx | 4 - components/icons/src/icons/print-layout.tsx | 4 - components/icons/src/icons/print.tsx | 4 - ...{properties-2.tsx => properties-panel.tsx} | 2 +- ...{properties-1.tsx => properties-tools.tsx} | 2 +- components/icons/src/icons/protect-sheet.tsx | 4 - .../icons/src/icons/protect-workbook.tsx | 4 - components/icons/src/icons/radio-button.tsx | 4 - components/icons/src/icons/radius.tsx | 4 - components/icons/src/icons/reapply.tsx | 4 - components/icons/src/icons/rectangle.tsx | 4 - .../icons/src/icons/recurrence-edit.tsx | 4 - components/icons/src/icons/redact.tsx | 4 - components/icons/src/icons/redaction.tsx | 4 - components/icons/src/icons/redo.tsx | 4 - components/icons/src/icons/refresh.tsx | 4 - components/icons/src/icons/rename.tsx | 4 - components/icons/src/icons/repeat.tsx | 4 - .../icons/src/icons/repeating-section.tsx | 4 - components/icons/src/icons/rephrase.tsx | 4 - components/icons/src/icons/replace.tsx | 4 - components/icons/src/icons/reset.tsx | 4 - components/icons/src/icons/resize.tsx | 4 - .../icons/src/icons/resizer-horizontal.tsx | 4 - components/icons/src/icons/resizer-right.tsx | 4 - .../icons/src/icons/resizer-vertical.tsx | 4 - components/icons/src/icons/resizer.tsx | 4 - .../{restart-at-1.tsx => restart-at.tsx} | 2 +- components/icons/src/icons/saturation.tsx | 4 - components/icons/src/icons/save-as.tsx | 4 - components/icons/src/icons/save.tsx | 4 - components/icons/src/icons/search.tsx | 4 - components/icons/src/icons/select-all.tsx | 4 - components/icons/src/icons/selection.tsx | 4 - components/icons/src/icons/send-backward.tsx | 4 - components/icons/src/icons/send-to-back.tsx | 4 - components/icons/src/icons/send.tsx | 4 - components/icons/src/icons/settings.tsx | 4 - components/icons/src/icons/shapes.tsx | 4 - components/icons/src/icons/sharpness.tsx | 4 - components/icons/src/icons/shorten.tsx | 4 - .../icons/src/icons/show-hide-panel.tsx | 4 - components/icons/src/icons/signature.tsx | 4 - components/icons/src/icons/smart-paste.tsx | 4 - ...cending-2.tsx => sort-ascending-boxed.tsx} | 2 +- ...{sorting-2.tsx => sort-ascending-list.tsx} | 2 +- components/icons/src/icons/sort-ascending.tsx | 4 - .../{sorting-1.tsx => sort-bidirectional.tsx} | 2 +- ...ending-2.tsx => sort-descending-boxed.tsx} | 2 +- ...sorting-3.tsx => sort-descending-list.tsx} | 2 +- .../icons/src/icons/sort-descending.tsx | 4 - components/icons/src/icons/spacing-after.tsx | 4 - components/icons/src/icons/spacing-before.tsx | 4 - components/icons/src/icons/spell-check.tsx | 4 - .../icons/src/icons/split-horizontal.tsx | 4 - components/icons/src/icons/split-vertical.tsx | 4 - .../icons/src/icons/square-head-fill.tsx | 4 - components/icons/src/icons/square-head.tsx | 4 - .../icons/src/icons/square-tail-fill.tsx | 4 - components/icons/src/icons/square-tail.tsx | 4 - components/icons/src/icons/squiggly.tsx | 4 - components/icons/src/icons/stamp.tsx | 4 - components/icons/src/icons/star-filled.tsx | 4 - components/icons/src/icons/stop-rectangle.tsx | 4 - components/icons/src/icons/strikethrough.tsx | 4 - components/icons/src/icons/stroke-width.tsx | 4 - components/icons/src/icons/style.tsx | 4 - components/icons/src/icons/sub-total.tsx | 4 - components/icons/src/icons/subscript.tsx | 4 - components/icons/src/icons/sum.tsx | 4 - components/icons/src/icons/superscript.tsx | 4 - components/icons/src/icons/symbols.tsx | 4 - .../icons/src/icons/table-align-center.tsx | 4 - .../icons/src/icons/table-align-left.tsx | 4 - .../icons/src/icons/table-align-right.tsx | 4 - .../icons/src/icons/table-border-custom.tsx | 4 - .../icons/src/icons/table-cell-none.tsx | 4 - components/icons/src/icons/table-cell.tsx | 4 - components/icons/src/icons/table-delete.tsx | 4 - .../src/icons/{table-2.tsx => table-grid.tsx} | 2 +- components/icons/src/icons/table-header.tsx | 4 - .../icons/src/icons/table-insert-column.tsx | 4 - .../icons/src/icons/table-insert-row.tsx | 4 - components/icons/src/icons/table-merge.tsx | 4 - components/icons/src/icons/table-nested.tsx | 4 - .../icons/src/icons/table-of-content.tsx | 4 - .../icons/src/icons/table-overwrite-cells.tsx | 4 - components/icons/src/icons/table-update.tsx | 4 - components/icons/src/icons/table.tsx | 4 - .../icons/src/icons/text-alternative.tsx | 4 - .../icons/src/icons/text-annotation.tsx | 4 - components/icons/src/icons/text-form.tsx | 4 - components/icons/src/icons/text-header.tsx | 4 - components/icons/src/icons/text-outline.tsx | 4 - .../icons/src/icons/text-that-contains.tsx | 4 - components/icons/src/icons/text-wrap.tsx | 4 - components/icons/src/icons/thumbnail.tsx | 4 - .../icons/src/icons/thumbs-down-fill.tsx | 4 - components/icons/src/icons/thumbs-down.tsx | 4 - components/icons/src/icons/thumbs-up-fill.tsx | 4 - components/icons/src/icons/thumbs-up.tsx | 4 - components/icons/src/icons/time-zone.tsx | 4 - components/icons/src/icons/timeline-day.tsx | 4 - components/icons/src/icons/timeline-month.tsx | 4 - components/icons/src/icons/timeline-today.tsx | 4 - components/icons/src/icons/timeline-week.tsx | 4 - .../icons/src/icons/timeline-work-week.tsx | 4 - components/icons/src/icons/tint.tsx | 4 - components/icons/src/icons/top-10.tsx | 4 - .../icons/src/icons/top-bottom-rules.tsx | 4 - components/icons/src/icons/transform-left.tsx | 4 - .../icons/src/icons/transform-right.tsx | 4 - components/icons/src/icons/transform.tsx | 4 - components/icons/src/icons/translate.tsx | 4 - components/icons/src/icons/trash.tsx | 4 - .../icons/src/icons/travel-and-places.tsx | 4 - components/icons/src/icons/triangle.tsx | 4 - components/icons/src/icons/two-column.tsx | 4 - components/icons/src/icons/two-row.tsx | 4 - components/icons/src/icons/underline.tsx | 4 - components/icons/src/icons/undo.tsx | 4 - components/icons/src/icons/unfiltered.tsx | 4 - .../{ungroup-2.tsx => ungroup-divide.tsx} | 2 +- .../{ungroup-1.tsx => ungroup-items.tsx} | 2 +- components/icons/src/icons/unlock.tsx | 4 - .../src/icons/{upload-1.tsx => upload.tsx} | 2 +- components/icons/src/icons/upper-case.tsx | 4 - components/icons/src/icons/user-defined.tsx | 4 - components/icons/src/icons/user.tsx | 4 - .../icons/src/icons/vertical-align-bottom.tsx | 4 - .../icons/src/icons/vertical-align-center.tsx | 4 - .../icons/src/icons/vertical-align-top.tsx | 4 - components/icons/src/icons/video.tsx | 4 - components/icons/src/icons/view-side.tsx | 4 - components/icons/src/icons/volume.tsx | 4 - components/icons/src/icons/warning.tsx | 4 - components/icons/src/icons/web-layout.tsx | 4 - components/icons/src/icons/week.tsx | 4 - components/icons/src/icons/xml-mapping.tsx | 4 - components/icons/src/icons/zoom-in.tsx | 4 - components/icons/src/icons/zoom-out.tsx | 4 - components/icons/src/icons/zoom-to-fit.tsx | 4 - components/icons/src/index.ts | 3 - components/icons/src/svg-icon.tsx | 78 - components/icons/tsconfig.json | 51 - components/inputs/CHANGELOG.md | 63 - components/inputs/README.md | 99 - components/inputs/package.json | 72 - components/inputs/src/common/index.ts | 4 - components/inputs/src/common/inputbase.tsx | 204 - .../src/form-validator/form-validator.tsx | 1092 +++++ components/inputs/src/form-validator/index.ts | 4 + components/inputs/src/index.ts | 7 - .../src/inputs/input}/_all.scss | 0 .../{styles => src/inputs}/input/_layout.scss | 4172 +++++++---------- .../inputs}/input/_material3-definition.scss | 1439 +++--- .../src/inputs/numerictextbox}/_all.scss | 0 .../src/inputs/numerictextbox/_layout.scss | 38 + .../numerictextbox/_material3-definition.scss | 3 + .../inputs/textarea}/_all.scss | 0 .../inputs}/textarea/_layout.scss | 13 +- .../textarea}/_material3-definition.scss | 0 .../textarea => src/inputs/textbox}/_all.scss | 0 .../inputs}/textbox/_layout.scss | 8 +- .../inputs/textbox/_material3-definition.scss | 4 + .../inputs/src/numeric-textbox/index.ts | 4 + .../numeric-textbox.tsx} | 211 +- components/inputs/src/numerictextbox/index.ts | 4 - components/inputs/src/textarea/index.ts | 4 - components/inputs/src/textarea/textarea.tsx | 330 -- components/inputs/src/textbox/index.ts | 4 - components/inputs/src/textbox/textbox.tsx | 323 -- components/inputs/styles/input/_all.scss | 2 - .../input/_material3-dark-definition.scss | 1 - .../inputs/styles/input/_responsive.scss | 1 - .../inputs/styles/input/material3-dark.scss | 2 - components/inputs/styles/input/material3.scss | 2 - components/inputs/styles/material3-dark.scss | 8 - components/inputs/styles/material3.scss | 8 - .../inputs/styles/numerictextbox/_layout.scss | 37 - .../_material3-dark-definition.scss | 1 - .../numerictextbox/_material3-definition.scss | 8 - .../styles/numerictextbox/material3-dark.scss | 2 - .../styles/numerictextbox/material3.scss | 2 - .../textarea/_material3-dark-definition.scss | 1 - .../textarea/_material3-definition.scss | 1 - .../styles/textarea/material3-dark.scss | 2 - .../inputs/styles/textarea/material3.scss | 2 - .../textbox/_material3-dark-definition.scss | 1 - .../styles/textbox/_material3-definition.scss | 4 - .../inputs/styles/textbox/material3-dark.scss | 2 - .../inputs/styles/textbox/material3.scss | 2 - components/lists/CHANGELOG.md | 3 + components/lists/README.md | 62 + .../{notifications => lists}/gulpfile.js | 0 components/{popups => lists}/license | 0 components/{buttons => lists}/package.json | 50 +- components/lists/src/common/index.ts | 4 + components/lists/src/common/list-base.tsx | 890 ++++ components/lists/src/index.ts | 5 + components/lists/src/list-view/index.ts | 4 + components/lists/src/list-view/list-view.tsx | 1011 ++++ .../src/lists/list-base}/_all.scss | 0 .../lists/src/lists/list-base/_layout.scss | 112 + .../list-base/_material3-definition.scss | 45 + .../lists/src/lists/list-base/_theme.scss | 64 + .../lists/src/lists/list-view/_all.scss | 2 + .../lists/src/lists/list-view/_layout.scss | 272 ++ .../list-view/_material3-definition.scss | 64 + .../lists/src/lists/list-view/_theme.scss | 45 + components/lists/styles/list-base/_all.scss | 2 + .../lists/styles/list-base/_layout.scss | 112 + .../list-base/_material3-definition.scss | 45 + components/lists/styles/list-base/_theme.scss | 64 + components/lists/styles/list-view/_all.scss | 2 + .../lists/styles/list-view/_layout.scss | 272 ++ .../list-view/_material3-definition.scss | 64 + components/lists/styles/list-view/_theme.scss | 45 + .../{notifications => lists}/tsconfig.json | 2 +- components/navigations/CHANGELOG.md | 36 - components/navigations/README.md | 67 - .../navigations/src/common/h-scroll.tsx | 437 -- components/navigations/src/common/index.ts | 5 - .../navigations/src/common/v-scroll.tsx | 419 -- .../context-menu.tsx | 47 +- .../{contextMenu => context-menu}/index.ts | 0 components/navigations/src/index.ts | 3 - .../src/navigations/context-menu/_all.scss | 2 + .../src/navigations/context-menu/_layout.scss | 69 + .../context-menu/_material3-definition.scss | 25 + .../src/navigations/context-menu/_theme.scss | 47 + .../src/navigations/h-scroll/_all.scss | 2 + .../src/navigations/h-scroll/_layout.scss | 53 + .../h-scroll/_material3-definition.scss | 13 + .../src/navigations/h-scroll/_theme.scss | 40 + .../src/navigations/toolbar/_all.scss | 2 + .../src/navigations/toolbar/_layout.scss | 249 + .../toolbar/_material3-definition.scss | 53 + .../src/navigations/toolbar/_theme.scss | 63 + .../src/navigations/v-scroll/_all.scss | 2 + .../src/navigations/v-scroll/_layout.scss | 50 + .../v-scroll/_material3-definition.scss | 13 + .../src/navigations/v-scroll/_theme.scss | 26 + components/navigations/src/toolbar/index.ts | 7 - .../navigations/src/toolbar/toolbar-item.tsx | 66 - .../src/toolbar/toolbar-multi-row.tsx | 49 - .../navigations/src/toolbar/toolbar-popup.tsx | 621 --- .../src/toolbar/toolbar-scrollable.tsx | 216 - .../src/toolbar/toolbar-separator.tsx | 64 - .../src/toolbar/toolbar-spacer.tsx | 66 - .../navigations/src/toolbar/toolbar.tsx | 506 -- .../styles/context-menu/_layout.scss | 122 - .../_material3-dark-definition.scss | 1 - .../context-menu/_material3-definition.scss | 40 - .../styles/context-menu/_theme-mixin.scss | 17 - .../styles/context-menu/_theme.scss | 82 - .../styles/context-menu/material3-dark.scss | 2 - .../styles/context-menu/material3.scss | 2 - .../navigations/styles/h-scroll/_layout.scss | 105 - .../h-scroll/_material3-dark-definition.scss | 1 - .../h-scroll/_material3-definition.scss | 25 - .../navigations/styles/h-scroll/_theme.scss | 101 - .../styles/h-scroll/material3-dark.scss | 2 - .../styles/h-scroll/material3.scss | 2 - .../navigations/styles/material3-dark.scss | 8 - components/navigations/styles/material3.scss | 8 - .../navigations/styles/toolbar/_layout.scss | 357 -- .../toolbar/_material3-dark-definition.scss | 1 - .../styles/toolbar/_material3-definition.scss | 75 - .../navigations/styles/toolbar/_theme.scss | 164 - .../styles/toolbar/material3-dark.scss | 2 - .../navigations/styles/toolbar/material3.scss | 2 - .../navigations/styles/v-scroll/_layout.scss | 76 - .../v-scroll/_material3-dark-definition.scss | 1 - .../v-scroll/_material3-definition.scss | 21 - .../navigations/styles/v-scroll/_theme.scss | 70 - .../styles/v-scroll/material3-dark.scss | 2 - .../styles/v-scroll/material3.scss | 2 - components/notifications/CHANGELOG.md | 53 - components/notifications/README.md | 89 - components/notifications/src/index.ts | 7 - components/notifications/src/message/index.ts | 4 - .../notifications/src/message/message.tsx | 258 - .../notifications}/message/_all.scss | 0 .../src/notifications/message/_layout.scss | 236 + .../message/_material3-definition.scss | 72 + .../notifications}/skeleton/_all.scss | 0 .../src/notifications/skeleton/_layout.scss | 68 + .../skeleton/_material3-definition.scss | 11 + .../src/notifications/toast/_all.scss | 2 + .../src/notifications/toast/_layout.scss | 129 + .../toast/_material3-definition.scss | 32 + .../src/notifications/toast/_theme.scss | 34 + .../notifications/src/skeleton/index.ts | 4 - .../notifications/src/skeleton/skeleton.tsx | 199 - components/notifications/src/toast/index.ts | 4 - components/notifications/src/toast/toast.tsx | 665 --- .../notifications/styles/material3-dark.scss | 6 - .../notifications/styles/material3.scss | 6 - .../notifications/styles/message/_layout.scss | 171 - .../message/_material3-dark-definition.scss | 1 - .../styles/message/_material3-definition.scss | 117 - .../styles/message/material3-dark.scss | 2 - .../styles/message/material3.scss | 2 - .../styles/skeleton/_layout.scss | 118 - .../skeleton/_material3-dark-definition.scss | 1 - .../skeleton/_material3-definition.scss | 12 - .../styles/skeleton/material3-dark.scss | 2 - .../styles/skeleton/material3.scss | 2 - .../notifications/styles/toast/_layout.scss | 456 -- .../toast/_material3-dark-definition.scss | 1 - .../styles/toast/_material3-definition.scss | 119 - .../notifications/styles/toast/_theme.scss | 34 + .../styles/toast/material3-dark.scss | 2 - .../notifications/styles/toast/material3.scss | 2 - components/pager/CHANGELOG.md | 3 + components/{icons => pager}/README.md | 23 +- components/{popups => pager}/gulpfile.js | 0 components/{splitbuttons => pager}/license | 0 components/pager/package.json | 45 + components/pager/src/index.ts | 1 + components/pager/src/numericContainer.tsx | 210 + components/pager/src/page.tsx | 542 +++ components/pager/src/pager/pager/_all.scss | 2 + components/pager/src/pager/pager/_layout.scss | 189 + .../pager/pager/_material3-definition.scss | 33 + components/pager/src/pager/pager/_theme.scss | 58 + components/pager/src/usePager.tsx | 141 + components/pager/src/usePagerFocus.ts | 333 ++ components/pager/styles/material3.scss | 3 + components/pager/styles/pager/_all.scss | 2 + components/pager/styles/pager/_layout.scss | 189 + .../styles/pager/_material3-definition.scss | 33 + components/pager/styles/pager/_theme.scss | 58 + .../styles/pager}/material3.scss | 1 + components/{base => pager}/tsconfig.json | 21 +- components/popups/CHANGELOG.md | 25 - components/popups/src/common/collision.tsx | 181 - components/popups/src/common/index.ts | 5 - components/popups/src/common/position.tsx | 65 - components/popups/src/common/resize.tsx | 540 +++ components/popups/src/dialog/dialog.tsx | 844 ++++ components/popups/src/dialog/index.ts | 4 + components/popups/src/index.ts | 8 - components/popups/src/popup/index.ts | 5 - components/popups/src/popup/popup.tsx | 792 ---- .../spinner => src/popups/dialog}/_all.scss | 1 + .../popups/src/popups/dialog/_layout.scss | 197 + .../popups/dialog/_material3-definition.scss | 38 + .../popups/src/popups/dialog/_theme.scss | 34 + .../{styles => src/popups}/popup/_all.scss | 0 .../{styles => src/popups}/popup/_layout.scss | 16 +- .../popups/popup/_material3-definition.scss} | 0 .../src/popups/spinner}/_all.scss | 0 .../popups}/spinner/_layout.scss | 0 .../spinner/_material3-definition.scss | 0 .../popups/src/popups/tooltip/_all.scss | 2 + .../popups/src/popups/tooltip/_layout.scss | 67 + .../popups/tooltip/_material3-definition.scss | 40 + .../popups/src/popups/tooltip/_theme.scss | 62 + components/popups/src/spinner/index.ts | 4 - components/popups/src/spinner/spinner.tsx | 414 -- components/popups/src/tooltip/index.ts | 4 - components/popups/src/tooltip/tooltip.tsx | 1590 ------- .../styles/dialog}/_all.scss | 1 + components/popups/styles/dialog/_layout.scss | 197 + .../styles/dialog/_material3-definition.scss | 38 + components/popups/styles/dialog/_theme.scss | 34 + .../popups/styles/dialog/material3.scss | 4 + components/popups/styles/material3-dark.scss | 6 - components/popups/styles/material3.scss | 6 - .../popups/styles/popup/material3-dark.scss | 2 - components/popups/styles/popup/material3.scss | 2 - .../spinner/_material3-dark-definition.scss | 1 - .../popups/styles/spinner/material3-dark.scss | 2 - .../popups/styles/spinner/material3.scss | 2 - components/popups/styles/tooltip/_layout.scss | 77 - .../tooltip/_material3-dark-definition.scss | 1 - .../styles/tooltip/_material3-definition.scss | 51 - components/popups/styles/tooltip/_theme.scss | 106 - .../popups/styles/tooltip/material3-dark.scss | 2 - .../popups/styles/tooltip/material3.scss | 2 - components/splitbuttons/CHANGELOG.md | 41 - components/splitbuttons/README.md | 77 - .../src/dropdown-button/dropdown-button.tsx | 549 --- .../splitbuttons/src/dropdown-button/index.ts | 4 - components/splitbuttons/src/index.ts | 5 - .../splitbuttons/src/split-button/index.ts | 4 - .../src/split-button/split-button.tsx | 295 -- .../styles/drop-down-button/_layout.scss | 306 -- .../_material3-dark-definition.scss | 1 - .../_material3-definition.scss | 49 - .../styles/drop-down-button/_mixin.scss | 7 + .../styles/drop-down-button/_theme.scss | 68 - .../drop-down-button/material3-dark.scss | 2 - .../styles/drop-down-button/material3.scss | 2 - .../splitbuttons/styles/material3-dark.scss | 4 - components/splitbuttons/styles/material3.scss | 4 - .../styles/split-button/_layout.scss | 249 - .../_material3-dark-definition.scss | 1 - .../split-button/_material3-definition.scss | 27 - .../styles/split-button/material3-dark.scss | 2 - .../styles/split-button/material3.scss | 2 - components/svg-tooltip-component/CHANGELOG.md | 3 + .../README.md | 33 +- .../gulpfile.js | 0 .../{icons => svg-tooltip-component}/license | 4 +- .../package.json | 11 +- components/svg-tooltip-component/src/index.ts | 1 + .../src/svg-tooltip/SVG-Tooltip.tsx | 1279 +++++ .../src/svg-tooltip/enum.ts | 55 + .../src/svg-tooltip/helper.tsx | 381 ++ .../src/svg-tooltip/index.ts | 1 + .../src/svg-tooltip/models.ts | 405 ++ .../svg-tooltip-component/tsconfig.json | 42 + 1160 files changed, 94505 insertions(+), 37918 deletions(-) create mode 100644 components/base/src/animate.tsx delete mode 100644 components/base/src/animation.tsx delete mode 100644 components/base/src/base.tsx delete mode 100644 components/base/src/browser.tsx delete mode 100644 components/base/src/component.tsx delete mode 100644 components/base/src/dom.tsx delete mode 100644 components/base/src/drag-util.tsx delete mode 100644 components/base/src/dragdrop.tsx delete mode 100644 components/base/src/draggable.tsx delete mode 100644 components/base/src/droppable.tsx create mode 100644 components/base/src/enums.tsx delete mode 100644 components/base/src/event-handler.tsx delete mode 100644 components/base/src/fetch.tsx delete mode 100644 components/base/src/hijri-parser.tsx delete mode 100644 components/base/src/index.ts delete mode 100644 components/base/src/internationalization.tsx delete mode 100644 components/base/src/intl/date-formatter.tsx delete mode 100644 components/base/src/intl/date-parser.tsx delete mode 100644 components/base/src/intl/index.ts delete mode 100644 components/base/src/intl/intl-base.tsx delete mode 100644 components/base/src/intl/number-formatter.tsx delete mode 100644 components/base/src/intl/number-parser.tsx delete mode 100644 components/base/src/intl/parser-base.tsx delete mode 100644 components/base/src/l10n.tsx delete mode 100644 components/base/src/observer.tsx delete mode 100644 components/base/src/provider.tsx delete mode 100644 components/base/src/ripple.tsx delete mode 100644 components/base/src/sanitize-helper.tsx delete mode 100644 components/base/src/svg-icon.tsx delete mode 100644 components/base/src/touch.tsx delete mode 100644 components/base/src/util.tsx delete mode 100644 components/base/src/validate-lic.tsx delete mode 100644 components/base/styles/_all.scss delete mode 100644 components/base/styles/_material3-dark-definition.scss delete mode 100644 components/base/styles/_material3-definition.scss create mode 100644 components/base/styles/border-radius/_radius.scss delete mode 100644 components/base/styles/common/_all.scss delete mode 100644 components/base/styles/common/_core.scss delete mode 100644 components/base/styles/definition/_material3-dark.scss create mode 100644 components/base/styles/functions/_all.scss create mode 100644 components/base/styles/functions/_calc.scss create mode 100644 components/base/styles/functions/_core.scss delete mode 100644 components/base/styles/material3-dark.scss delete mode 100644 components/base/styles/material3.scss create mode 100644 components/base/styles/mixins/_all.scss rename components/base/styles/{common/_mixin.scss => mixins/_core.scss} (100%) create mode 100644 components/base/styles/mixins/_decorator.scss create mode 100644 components/base/styles/mixins/_radius.scss create mode 100644 components/base/styles/styles/_all.scss rename components/base/styles/{animation/_all.scss => styles/_animation.scss} (89%) create mode 100644 components/base/styles/styles/_base.scss create mode 100644 components/base/styles/styles/_layout.scss create mode 100644 components/base/styles/styles/_resize.scss create mode 100644 components/base/styles/styles/_theme.scss rename components/base/styles/{definition => themes}/_material3.scss (64%) create mode 100644 components/base/styles/typography/_variables.scss delete mode 100644 components/buttons/CHANGELOG.md delete mode 100644 components/buttons/README.md delete mode 100644 components/buttons/src/button/button.tsx delete mode 100644 components/buttons/src/button/index.ts delete mode 100644 components/buttons/src/check-box/check-box.tsx delete mode 100644 components/buttons/src/check-box/index.ts rename components/buttons/src/{chipList => chip-list}/chip-list.tsx (80%) rename components/buttons/src/{chipList => chip-list}/index.ts (100%) delete mode 100644 components/buttons/src/chip/chip.tsx delete mode 100644 components/buttons/src/chip/index.ts delete mode 100644 components/buttons/src/floating-action-button/floating-action-button.tsx delete mode 100644 components/buttons/src/floating-action-button/index.ts delete mode 100644 components/buttons/src/index.ts delete mode 100644 components/buttons/src/radio-button/index.ts delete mode 100644 components/buttons/src/radio-button/radio-button.tsx delete mode 100644 components/buttons/styles/button/_layout.scss delete mode 100644 components/buttons/styles/button/_material3-dark-definition.scss delete mode 100644 components/buttons/styles/button/_material3-definition.scss delete mode 100644 components/buttons/styles/button/_mixin.scss delete mode 100644 components/buttons/styles/button/_theme.scss delete mode 100644 components/buttons/styles/button/material3-dark.scss delete mode 100644 components/buttons/styles/check-box/_layout.scss delete mode 100644 components/buttons/styles/check-box/_material3-dark-definition.scss delete mode 100644 components/buttons/styles/check-box/_material3-definition.scss delete mode 100644 components/buttons/styles/check-box/material3-dark.scss delete mode 100644 components/buttons/styles/chips/_layout.scss delete mode 100644 components/buttons/styles/chips/_material3-dark-definition.scss delete mode 100644 components/buttons/styles/chips/_material3-definition.scss delete mode 100644 components/buttons/styles/chips/_theme.scss delete mode 100644 components/buttons/styles/chips/material3-dark.scss delete mode 100644 components/buttons/styles/chips/material3.scss delete mode 100644 components/buttons/styles/floating-action-button/_layout.scss delete mode 100644 components/buttons/styles/floating-action-button/_material3-dark-definition.scss delete mode 100644 components/buttons/styles/floating-action-button/_material3-definition.scss create mode 100644 components/buttons/styles/floating-action-button/_mixin.scss delete mode 100644 components/buttons/styles/floating-action-button/_theme.scss delete mode 100644 components/buttons/styles/floating-action-button/material3-dark.scss delete mode 100644 components/buttons/styles/material3-dark.scss delete mode 100644 components/buttons/styles/material3.scss delete mode 100644 components/buttons/styles/radio-button/_layout.scss delete mode 100644 components/buttons/styles/radio-button/_material3-dark-definition.scss delete mode 100644 components/buttons/styles/radio-button/_material3-definition.scss delete mode 100644 components/buttons/styles/radio-button/material3-dark.scss delete mode 100644 components/buttons/styles/radio-button/material3.scss rename components/{base => calendars}/CHANGELOG.md (100%) create mode 100644 components/calendars/README.md rename components/{base => calendars}/gulpfile.js (100%) rename components/{buttons => calendars}/license (100%) rename components/{navigations => calendars}/package.json (56%) create mode 100644 components/calendars/src/calendar/calendar-cell.tsx create mode 100644 components/calendars/src/calendar/calendar.tsx create mode 100644 components/calendars/src/calendar/index.ts create mode 100644 components/calendars/src/datepicker/datepicker.tsx create mode 100644 components/calendars/src/datepicker/index.ts create mode 100644 components/calendars/src/index.ts create mode 100644 components/calendars/src/utils/calendar-util.ts create mode 100644 components/calendars/src/utils/index.ts rename components/{buttons/styles/button => calendars/styles/calendar}/_all.scss (100%) create mode 100644 components/calendars/styles/calendar/_layout.scss create mode 100644 components/calendars/styles/calendar/_material3-definition.scss create mode 100644 components/calendars/styles/calendar/_theme.scss create mode 100644 components/calendars/styles/calendar/material3.scss rename components/{notifications/styles/toast => calendars/styles/datepicker}/_all.scss (52%) create mode 100644 components/calendars/styles/datepicker/_layout.scss create mode 100644 components/calendars/styles/datepicker/_material3-definition.scss create mode 100644 components/calendars/styles/datepicker/_theme.scss create mode 100644 components/calendars/styles/datepicker/material3.scss create mode 100644 components/calendars/styles/material3.scss rename components/{buttons/styles/chips => calendars/styles/timepicker}/_all.scss (100%) create mode 100644 components/calendars/styles/timepicker/_layout.scss create mode 100644 components/calendars/styles/timepicker/_material3-definition.scss create mode 100644 components/calendars/styles/timepicker/_theme.scss create mode 100644 components/calendars/styles/timepicker/material3.scss rename components/{buttons => calendars}/tsconfig.json (100%) create mode 100644 components/charts/CHANGELOG.md create mode 100644 components/charts/README.md rename components/{buttons => charts}/gulpfile.js (100%) rename components/{inputs => charts}/license (100%) rename components/{notifications => charts}/package.json (71%) create mode 100644 components/charts/src/chart/Chart.tsx create mode 100644 components/charts/src/chart/base/Legend-base.tsx create mode 100644 components/charts/src/chart/base/default-properties.tsx create mode 100644 components/charts/src/chart/base/enum.tsx create mode 100644 components/charts/src/chart/base/interfaces.tsx create mode 100644 components/charts/src/chart/chart-area/ChartArea.tsx create mode 100644 components/charts/src/chart/chart-area/ChartStackLabels.tsx create mode 100644 components/charts/src/chart/chart-area/ChartSubTitle.tsx create mode 100644 components/charts/src/chart/chart-area/ChartTitle.tsx create mode 100644 components/charts/src/chart/chart-area/chart-interfaces.tsx create mode 100644 components/charts/src/chart/chart-axis/ChartAxes.tsx create mode 100644 components/charts/src/chart/chart-axis/Columns.tsx create mode 100644 components/charts/src/chart/chart-axis/LabelStyle.tsx create mode 100644 components/charts/src/chart/chart-axis/MajorGridLines.tsx create mode 100644 components/charts/src/chart/chart-axis/MajorTickLines.tsx create mode 100644 components/charts/src/chart/chart-axis/MinorGridLines.tsx create mode 100644 components/charts/src/chart/chart-axis/MinorTickLines.tsx create mode 100644 components/charts/src/chart/chart-axis/PrimaryXAxis.tsx create mode 100644 components/charts/src/chart/chart-axis/PrimaryYAxis.tsx create mode 100644 components/charts/src/chart/chart-axis/Rows.tsx create mode 100644 components/charts/src/chart/chart-axis/StripLines.tsx create mode 100644 components/charts/src/chart/chart-axis/TitleStyle.tsx create mode 100644 components/charts/src/chart/chart-axis/base.tsx create mode 100644 components/charts/src/chart/chart-legend/ChartLegend.tsx create mode 100644 components/charts/src/chart/chart-tooltip/ChartTooltip.tsx create mode 100644 components/charts/src/chart/common/base.tsx create mode 100644 components/charts/src/chart/common/data.tsx create mode 100644 components/charts/src/chart/hooks/useClipRect.tsx create mode 100644 components/charts/src/chart/hooks/useDeepCompare.tsx create mode 100644 components/charts/src/chart/index.ts create mode 100644 components/charts/src/chart/layout/ChartProvider.tsx create mode 100644 components/charts/src/chart/layout/LayoutContext.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/AxisOutsideRenderer.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/AxisRender.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/AxisUtils.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/CategoryAxisRenderer.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/DateTimeAxisRenderer.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/DoubleAxisRenderer.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/LogarithmicAxisRenderer.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/CartesianLayoutRender.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/ChartColumnsRender.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/ChartRowsRender.tsx create mode 100644 components/charts/src/chart/renderer/AxesRenderer/ChartStripLinesRender.tsx create mode 100644 components/charts/src/chart/renderer/ChartAreaRender.tsx create mode 100644 components/charts/src/chart/renderer/ChartRenderer.tsx create mode 100644 components/charts/src/chart/renderer/ChartStackLabelsRenderer.tsx create mode 100644 components/charts/src/chart/renderer/ChartSubtitleRender.tsx create mode 100644 components/charts/src/chart/renderer/ChartTitleRenderer.tsx create mode 100644 components/charts/src/chart/renderer/LegendRenderer/ChartLegendRenderer.tsx create mode 100644 components/charts/src/chart/renderer/LegendRenderer/CommonLegend.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/AreaSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/BarSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/BubbleSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/ColumnBase.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/ColumnSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/DataLabelRender.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/LineBase.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/MarkerBase.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/MarkerRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/ProcessData.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/ScatterSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/SeriesAnimation.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/SeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/SplineAreaSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/SplineSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/StackingBarSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/StackingColumnSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/StepLineSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/lineSeriesRenderer.tsx create mode 100644 components/charts/src/chart/renderer/SeriesRenderer/updatePoint.tsx create mode 100644 components/charts/src/chart/renderer/TooltipRenderer.tsx create mode 100644 components/charts/src/chart/renderer/TrackballRenderer.tsx create mode 100644 components/charts/src/chart/renderer/Zooming/zoom-toolbar.tsx create mode 100644 components/charts/src/chart/renderer/Zooming/zooming.tsx create mode 100644 components/charts/src/chart/series/DataLabel.tsx create mode 100644 components/charts/src/chart/series/Marker.tsx create mode 100644 components/charts/src/chart/series/Series.tsx create mode 100644 components/charts/src/chart/utils/constants.ts create mode 100644 components/charts/src/chart/utils/getData.tsx create mode 100644 components/charts/src/chart/utils/helper.tsx create mode 100644 components/charts/src/chart/utils/theme.tsx create mode 100644 components/charts/src/chart/zooming/ChartZooming.tsx create mode 100644 components/charts/src/index.ts rename components/{inputs => charts}/tsconfig.json (100%) create mode 100644 components/data/CHANGELOG.md rename components/{base => data}/README.md (70%) rename components/{icons => data}/gulpfile.js (100%) rename components/{base => data}/license (100%) rename components/{base => data}/package.json (71%) create mode 100644 components/data/src/adaptors.ts create mode 100644 components/data/src/index.ts create mode 100644 components/data/src/manager.ts create mode 100644 components/data/src/query.ts create mode 100644 components/data/src/schema.ts create mode 100644 components/data/src/util.ts rename components/{popups => data}/tsconfig.json (100%) create mode 100644 components/dropdowns/CHANGELOG.md create mode 100644 components/dropdowns/README.md rename components/{inputs => dropdowns}/gulpfile.js (100%) rename components/{navigations => dropdowns}/license (100%) rename components/{splitbuttons => dropdowns}/package.json (65%) create mode 100644 components/dropdowns/src/common/drop-down-base.tsx create mode 100644 components/dropdowns/src/common/index.ts create mode 100644 components/dropdowns/src/drop-down-list/drop-down-list.tsx create mode 100644 components/dropdowns/src/drop-down-list/index.ts rename components/{buttons/styles/floating-action-button => dropdowns/src/dropdowns/drop-down-base}/_all.scss (100%) create mode 100644 components/dropdowns/src/dropdowns/drop-down-base/_layout.scss create mode 100644 components/dropdowns/src/dropdowns/drop-down-base/_material3-definition.scss create mode 100644 components/dropdowns/src/dropdowns/drop-down-base/_theme.scss rename components/{navigations/styles/context-menu => dropdowns/src/dropdowns/drop-down-list}/_all.scss (100%) create mode 100644 components/dropdowns/src/dropdowns/drop-down-list/_layout.scss create mode 100644 components/dropdowns/src/dropdowns/drop-down-list/_material3-definition.scss create mode 100644 components/dropdowns/src/dropdowns/drop-down-list/_theme.scss create mode 100644 components/dropdowns/src/index.ts rename components/{navigations/styles/h-scroll => dropdowns/styles/drop-down-base}/_all.scss (100%) create mode 100644 components/dropdowns/styles/drop-down-base/_layout.scss create mode 100644 components/dropdowns/styles/drop-down-base/_material3-definition.scss create mode 100644 components/dropdowns/styles/drop-down-base/_theme.scss rename components/{buttons/styles/floating-action-button => dropdowns/styles/drop-down-base}/material3.scss (56%) rename components/{navigations/styles/toolbar => dropdowns/styles/drop-down-list}/_all.scss (100%) create mode 100644 components/dropdowns/styles/drop-down-list/_layout.scss create mode 100644 components/dropdowns/styles/drop-down-list/_material3-definition.scss create mode 100644 components/dropdowns/styles/drop-down-list/_theme.scss rename components/{buttons/styles/check-box => dropdowns/styles/drop-down-list}/material3.scss (56%) create mode 100644 components/dropdowns/styles/material3.scss rename components/{splitbuttons => dropdowns}/tsconfig.json (99%) create mode 100644 components/grids/CHANGELOG.md create mode 100644 components/grids/README.md rename components/{navigations => grids}/gulpfile.js (100%) rename components/{notifications => grids}/license (100%) rename components/{popups => grids}/package.json (56%) create mode 100644 components/grids/src/grid/components/Column.tsx create mode 100644 components/grids/src/grid/components/Grid.tsx create mode 100644 components/grids/src/grid/components/Row.tsx create mode 100644 components/grids/src/grid/components/index.ts create mode 100644 components/grids/src/grid/contexts/GridProviders.tsx create mode 100644 components/grids/src/grid/contexts/index.ts rename components/{navigations/styles/v-scroll => grids/src/grid/grid}/_all.scss (100%) create mode 100644 components/grids/src/grid/grid/_layout.scss create mode 100644 components/grids/src/grid/grid/_material3-definition.scss create mode 100644 components/grids/src/grid/grid/_theme.scss create mode 100644 components/grids/src/grid/hooks/index.ts create mode 100644 components/grids/src/grid/hooks/useColumn.ts create mode 100644 components/grids/src/grid/hooks/useEdit.ts create mode 100644 components/grids/src/grid/hooks/useEditDialog.ts create mode 100644 components/grids/src/grid/hooks/useFilter.ts create mode 100644 components/grids/src/grid/hooks/useFocusStrategy.ts create mode 100644 components/grids/src/grid/hooks/useGrid.tsx create mode 100644 components/grids/src/grid/hooks/useRender.tsx create mode 100644 components/grids/src/grid/hooks/useScroll.tsx create mode 100644 components/grids/src/grid/hooks/useSearch.ts create mode 100644 components/grids/src/grid/hooks/useSelection.ts create mode 100644 components/grids/src/grid/hooks/useSort.ts create mode 100644 components/grids/src/grid/hooks/useToolbar.ts create mode 100644 components/grids/src/grid/index.ts create mode 100644 components/grids/src/grid/models/index.ts create mode 100644 components/grids/src/grid/models/useData.ts create mode 100644 components/grids/src/grid/services/index.ts create mode 100644 components/grids/src/grid/services/service-locator.ts create mode 100644 components/grids/src/grid/services/value-formatter.ts create mode 100644 components/grids/src/grid/types/aggregate.interfaces.ts create mode 100644 components/grids/src/grid/types/column.interfaces.ts create mode 100644 components/grids/src/grid/types/edit.interfaces.ts create mode 100644 components/grids/src/grid/types/enum.ts create mode 100644 components/grids/src/grid/types/filter.interfaces.ts create mode 100644 components/grids/src/grid/types/focus.interfaces.ts create mode 100644 components/grids/src/grid/types/grid.interfaces.ts create mode 100644 components/grids/src/grid/types/index.ts create mode 100644 components/grids/src/grid/types/interfaces.ts create mode 100644 components/grids/src/grid/types/page.interfaces.ts create mode 100644 components/grids/src/grid/types/search.interfaces.ts create mode 100644 components/grids/src/grid/types/selection.interfaces.ts create mode 100644 components/grids/src/grid/types/sort.interfaces.ts create mode 100644 components/grids/src/grid/types/toolbar.interfaces.ts create mode 100644 components/grids/src/grid/utils/index.ts create mode 100644 components/grids/src/grid/utils/utils.ts create mode 100644 components/grids/src/grid/views/Aggregate.tsx create mode 100644 components/grids/src/grid/views/ContentPanel.tsx create mode 100644 components/grids/src/grid/views/ContentRows.tsx create mode 100644 components/grids/src/grid/views/ContentTable.tsx create mode 100644 components/grids/src/grid/views/FilterBar.tsx create mode 100644 components/grids/src/grid/views/FooterPanel.tsx create mode 100644 components/grids/src/grid/views/FooterRows.tsx create mode 100644 components/grids/src/grid/views/FooterTable.tsx create mode 100644 components/grids/src/grid/views/HeaderPanel.tsx create mode 100644 components/grids/src/grid/views/HeaderRows.tsx create mode 100644 components/grids/src/grid/views/HeaderTable.tsx create mode 100644 components/grids/src/grid/views/PagerPanel.tsx create mode 100644 components/grids/src/grid/views/Render.tsx create mode 100644 components/grids/src/grid/views/editing/ConfirmDialog.tsx create mode 100644 components/grids/src/grid/views/editing/EditCell.tsx create mode 100644 components/grids/src/grid/views/editing/InlineEditForm.tsx create mode 100644 components/grids/src/grid/views/editing/ToolBar.tsx create mode 100644 components/grids/src/grid/views/editing/ValidationTooltips.tsx create mode 100644 components/grids/src/grid/views/index.ts create mode 100644 components/grids/src/index.ts rename components/{popups/styles/tooltip => grids/styles/grid}/_all.scss (100%) create mode 100644 components/grids/styles/grid/_layout.scss create mode 100644 components/grids/styles/grid/_material3-definition.scss create mode 100644 components/grids/styles/grid/_theme.scss create mode 100644 components/grids/styles/grid/material3.scss create mode 100644 components/grids/styles/material3.scss rename components/{navigations => grids}/tsconfig.json (99%) delete mode 100644 components/icons/CHANGELOG.MD create mode 100644 components/icons/CHANGELOG.md delete mode 100644 components/icons/src/icon.tsx delete mode 100644 components/icons/src/icons/above-average.tsx delete mode 100644 components/icons/src/icons/activities.tsx delete mode 100644 components/icons/src/icons/add-chart-element.tsx delete mode 100644 components/icons/src/icons/add-notes.tsx delete mode 100644 components/icons/src/icons/adjustment.tsx delete mode 100644 components/icons/src/icons/agenda-date-range.tsx delete mode 100644 components/icons/src/icons/ai-chat.tsx delete mode 100644 components/icons/src/icons/align-bottom.tsx delete mode 100644 components/icons/src/icons/align-center.tsx delete mode 100644 components/icons/src/icons/align-left.tsx delete mode 100644 components/icons/src/icons/align-middle.tsx delete mode 100644 components/icons/src/icons/align-right.tsx delete mode 100644 components/icons/src/icons/align-top.tsx delete mode 100644 components/icons/src/icons/all.tsx delete mode 100644 components/icons/src/icons/animals.tsx delete mode 100644 components/icons/src/icons/annotation-edit.tsx delete mode 100644 components/icons/src/icons/area.tsx delete mode 100644 components/icons/src/icons/arrow-down.tsx delete mode 100644 components/icons/src/icons/arrow-head-fill.tsx delete mode 100644 components/icons/src/icons/arrow-head.tsx delete mode 100644 components/icons/src/icons/arrow-left.tsx delete mode 100644 components/icons/src/icons/arrow-right-up.tsx delete mode 100644 components/icons/src/icons/arrow-right.tsx delete mode 100644 components/icons/src/icons/arrow-tail-fill.tsx delete mode 100644 components/icons/src/icons/arrow-tail.tsx delete mode 100644 components/icons/src/icons/arrow-up.tsx delete mode 100644 components/icons/src/icons/audio.tsx delete mode 100644 components/icons/src/icons/auto-fit-all-column.tsx delete mode 100644 components/icons/src/icons/auto-fit-column.tsx delete mode 100644 components/icons/src/icons/auto-fit-content.tsx delete mode 100644 components/icons/src/icons/auto-fit-window.tsx delete mode 100644 components/icons/src/icons/bar-head.tsx delete mode 100644 components/icons/src/icons/bar-tail.tsx delete mode 100644 components/icons/src/icons/below-average.tsx delete mode 100644 components/icons/src/icons/between.tsx delete mode 100644 components/icons/src/icons/blockquote.tsx delete mode 100644 components/icons/src/icons/bold.tsx delete mode 100644 components/icons/src/icons/bookmark.tsx delete mode 100644 components/icons/src/icons/border-all.tsx delete mode 100644 components/icons/src/icons/border-bottom.tsx delete mode 100644 components/icons/src/icons/border-box.tsx delete mode 100644 components/icons/src/icons/border-center.tsx delete mode 100644 components/icons/src/icons/border-custom.tsx rename components/icons/src/icons/{border-diagonal-1.tsx => border-diagonal-backslash.tsx} (91%) delete mode 100644 components/icons/src/icons/border-diagonal-down.tsx rename components/icons/src/icons/{border-diagonal-2.tsx => border-diagonal-slash.tsx} (92%) delete mode 100644 components/icons/src/icons/border-diagonal-up.tsx delete mode 100644 components/icons/src/icons/border-frame.tsx rename components/icons/src/icons/{border-shadow-2.tsx => border-inner-shadow.tsx} (87%) delete mode 100644 components/icons/src/icons/border-inner.tsx delete mode 100644 components/icons/src/icons/border-left.tsx delete mode 100644 components/icons/src/icons/border-middle.tsx rename components/icons/src/icons/{border-none-1.tsx => border-none-content.tsx} (97%) delete mode 100644 components/icons/src/icons/border-none.tsx rename components/icons/src/icons/{border-shadow-1.tsx => border-outer-shadow.tsx} (98%) delete mode 100644 components/icons/src/icons/border-outer.tsx delete mode 100644 components/icons/src/icons/border-right.tsx delete mode 100644 components/icons/src/icons/border-top.tsx delete mode 100644 components/icons/src/icons/bottom-10-items.tsx delete mode 100644 components/icons/src/icons/bottom-10.tsx delete mode 100644 components/icons/src/icons/box.tsx delete mode 100644 components/icons/src/icons/break-page.tsx delete mode 100644 components/icons/src/icons/break-section.tsx delete mode 100644 components/icons/src/icons/break.tsx delete mode 100644 components/icons/src/icons/brightness.tsx delete mode 100644 components/icons/src/icons/bring-forward.tsx delete mode 100644 components/icons/src/icons/bring-to-center.tsx delete mode 100644 components/icons/src/icons/bring-to-front.tsx delete mode 100644 components/icons/src/icons/bring-to-view.tsx delete mode 100644 components/icons/src/icons/building-block.tsx rename components/icons/src/icons/{bullet-6.tsx => bullet-arrow.tsx} (81%) rename components/icons/src/icons/{bullet-5.tsx => bullet-check.tsx} (80%) rename components/icons/src/icons/{bullet-1.tsx => bullet-circle-outline.tsx} (86%) rename components/icons/src/icons/{bullet-2.tsx => bullet-circle.tsx} (81%) rename components/icons/src/icons/{bullet-4.tsx => bullet-diamond.tsx} (94%) rename components/icons/src/icons/{bullet-7.tsx => bullet-point.tsx} (81%) rename components/icons/src/icons/{bullet-3.tsx => bullet-square.tsx} (74%) delete mode 100644 components/icons/src/icons/button-field.tsx delete mode 100644 components/icons/src/icons/calculate-sheet.tsx delete mode 100644 components/icons/src/icons/calculated-member.tsx delete mode 100644 components/icons/src/icons/calculation.tsx delete mode 100644 components/icons/src/icons/caption.tsx delete mode 100644 components/icons/src/icons/cell.tsx delete mode 100644 components/icons/src/icons/change-case.tsx delete mode 100644 components/icons/src/icons/change-scale-ratio.tsx delete mode 100644 components/icons/src/icons/changes-accept.tsx delete mode 100644 components/icons/src/icons/changes-reject.tsx delete mode 100644 components/icons/src/icons/changes-track.tsx delete mode 100644 components/icons/src/icons/character-style.tsx delete mode 100644 components/icons/src/icons/chart-2d-100-percent-stacked-area.tsx delete mode 100644 components/icons/src/icons/chart-2d-100-percent-stacked-bar.tsx delete mode 100644 components/icons/src/icons/chart-2d-100-percent-stacked-column.tsx delete mode 100644 components/icons/src/icons/chart-2d-100-percent-stacked-line-marked.tsx delete mode 100644 components/icons/src/icons/chart-2d-100-percent-stacked-line.tsx delete mode 100644 components/icons/src/icons/chart-2d-area.tsx delete mode 100644 components/icons/src/icons/chart-2d-clustered-bar.tsx delete mode 100644 components/icons/src/icons/chart-2d-clustered-column.tsx delete mode 100644 components/icons/src/icons/chart-2d-line-marked.tsx delete mode 100644 components/icons/src/icons/chart-2d-line.tsx delete mode 100644 components/icons/src/icons/chart-2d-pie-2.tsx delete mode 100644 components/icons/src/icons/chart-2d-stacked-area.tsx delete mode 100644 components/icons/src/icons/chart-2d-stacked-bar.tsx delete mode 100644 components/icons/src/icons/chart-2d-stacked-column.tsx delete mode 100644 components/icons/src/icons/chart-2d-stacked-line-marked.tsx delete mode 100644 components/icons/src/icons/chart-2d-stacked-line.tsx delete mode 100644 components/icons/src/icons/chart-axes-primary-horizontal.tsx delete mode 100644 components/icons/src/icons/chart-axes-primary-vertical.tsx delete mode 100644 components/icons/src/icons/chart-axes.tsx delete mode 100644 components/icons/src/icons/chart-axis-titles-primary-horizontal.tsx delete mode 100644 components/icons/src/icons/chart-axis-titles-primary-vertical.tsx delete mode 100644 components/icons/src/icons/chart-axis-titles.tsx delete mode 100644 components/icons/src/icons/chart-data-labels-center.tsx delete mode 100644 components/icons/src/icons/chart-data-labels-inside-base.tsx delete mode 100644 components/icons/src/icons/chart-data-labels-inside-end.tsx delete mode 100644 components/icons/src/icons/chart-data-labels-none.tsx delete mode 100644 components/icons/src/icons/chart-data-labels-outside-end.tsx delete mode 100644 components/icons/src/icons/chart-data-labels.tsx delete mode 100644 components/icons/src/icons/chart-donut.tsx delete mode 100644 components/icons/src/icons/chart-gridlines.tsx delete mode 100644 components/icons/src/icons/chart-insert-bar.tsx delete mode 100644 components/icons/src/icons/chart-insert-column.tsx delete mode 100644 components/icons/src/icons/chart-insert-line.tsx delete mode 100644 components/icons/src/icons/chart-insert-pie.tsx delete mode 100644 components/icons/src/icons/chart-insert-x-y-scatter.tsx delete mode 100644 components/icons/src/icons/chart-legend-bottom.tsx delete mode 100644 components/icons/src/icons/chart-legend-left.tsx delete mode 100644 components/icons/src/icons/chart-legend-none.tsx delete mode 100644 components/icons/src/icons/chart-legend-right.tsx delete mode 100644 components/icons/src/icons/chart-legend-top.tsx delete mode 100644 components/icons/src/icons/chart-legend.tsx delete mode 100644 components/icons/src/icons/chart-lines.tsx delete mode 100644 components/icons/src/icons/chart-primary-major-horizontal.tsx delete mode 100644 components/icons/src/icons/chart-primary-major-vertical.tsx delete mode 100644 components/icons/src/icons/chart-primary-minor-horizontal.tsx delete mode 100644 components/icons/src/icons/chart-primary-minor-vertical.tsx delete mode 100644 components/icons/src/icons/chart-switch-row-column.tsx delete mode 100644 components/icons/src/icons/chart-title-centered-overlay.tsx delete mode 100644 components/icons/src/icons/chart-title-none.tsx delete mode 100644 components/icons/src/icons/chart-title.tsx delete mode 100644 components/icons/src/icons/chart.tsx delete mode 100644 components/icons/src/icons/check-box.tsx delete mode 100644 components/icons/src/icons/check-large.tsx delete mode 100644 components/icons/src/icons/check-small.tsx delete mode 100644 components/icons/src/icons/check-tick.tsx delete mode 100644 components/icons/src/icons/check.tsx rename components/icons/src/icons/{intermediate-state.tsx => checkbox-intermediate.tsx} (81%) delete mode 100644 components/icons/src/icons/chevron-down-double.tsx delete mode 100644 components/icons/src/icons/chevron-down-fill.tsx delete mode 100644 components/icons/src/icons/chevron-down-small.tsx delete mode 100644 components/icons/src/icons/chevron-down.tsx delete mode 100644 components/icons/src/icons/chevron-left-double.tsx delete mode 100644 components/icons/src/icons/chevron-left-fill.tsx delete mode 100644 components/icons/src/icons/chevron-left-small.tsx delete mode 100644 components/icons/src/icons/chevron-left.tsx delete mode 100644 components/icons/src/icons/chevron-right-double.tsx delete mode 100644 components/icons/src/icons/chevron-right-fill.tsx delete mode 100644 components/icons/src/icons/chevron-right-small.tsx delete mode 100644 components/icons/src/icons/chevron-right.tsx delete mode 100644 components/icons/src/icons/chevron-up-double.tsx delete mode 100644 components/icons/src/icons/chevron-up-fill.tsx delete mode 100644 components/icons/src/icons/chevron-up-small.tsx delete mode 100644 components/icons/src/icons/chevron-up.tsx delete mode 100644 components/icons/src/icons/circle-add.tsx delete mode 100644 components/icons/src/icons/circle-check.tsx delete mode 100644 components/icons/src/icons/circle-close.tsx delete mode 100644 components/icons/src/icons/circle-head-fill.tsx delete mode 100644 components/icons/src/icons/circle-head.tsx delete mode 100644 components/icons/src/icons/circle-info.tsx delete mode 100644 components/icons/src/icons/circle-remove.tsx delete mode 100644 components/icons/src/icons/circle-tail-fill.tsx delete mode 100644 components/icons/src/icons/circle-tail.tsx delete mode 100644 components/icons/src/icons/circle.tsx delete mode 100644 components/icons/src/icons/clear-form.tsx delete mode 100644 components/icons/src/icons/clear-format.tsx delete mode 100644 components/icons/src/icons/clear-rules.tsx delete mode 100644 components/icons/src/icons/clock.tsx delete mode 100644 components/icons/src/icons/close.tsx delete mode 100644 components/icons/src/icons/code-view.tsx rename components/icons/src/icons/{collapse-2.tsx => collapse.tsx} (88%) delete mode 100644 components/icons/src/icons/color-scales.tsx delete mode 100644 components/icons/src/icons/columns.tsx delete mode 100644 components/icons/src/icons/combo-box.tsx delete mode 100644 components/icons/src/icons/comment-add.tsx delete mode 100644 components/icons/src/icons/comment-reopen.tsx delete mode 100644 components/icons/src/icons/comment-resolve.tsx delete mode 100644 components/icons/src/icons/comment-show.tsx delete mode 100644 components/icons/src/icons/conditional-formatting-large.tsx delete mode 100644 components/icons/src/icons/conditional-formatting.tsx delete mode 100644 components/icons/src/icons/content-control.tsx delete mode 100644 components/icons/src/icons/continue-numbering.tsx delete mode 100644 components/icons/src/icons/continuous-page-break.tsx delete mode 100644 components/icons/src/icons/contrast.tsx delete mode 100644 components/icons/src/icons/copy.tsx delete mode 100644 components/icons/src/icons/critical-path.tsx delete mode 100644 components/icons/src/icons/crop.tsx delete mode 100644 components/icons/src/icons/cut.tsx delete mode 100644 components/icons/src/icons/data-bars.tsx delete mode 100644 components/icons/src/icons/data-validation.tsx delete mode 100644 components/icons/src/icons/date-occurring.tsx delete mode 100644 components/icons/src/icons/day.tsx delete mode 100644 components/icons/src/icons/decrease-indent-rtl.tsx delete mode 100644 components/icons/src/icons/decrease-indent.tsx delete mode 100644 components/icons/src/icons/delete-column.tsx delete mode 100644 components/icons/src/icons/delete-notes.tsx delete mode 100644 components/icons/src/icons/delete-row.tsx delete mode 100644 components/icons/src/icons/description.tsx delete mode 100644 components/icons/src/icons/dimension.tsx delete mode 100644 components/icons/src/icons/display.tsx delete mode 100644 components/icons/src/icons/double-check.tsx delete mode 100644 components/icons/src/icons/download.tsx delete mode 100644 components/icons/src/icons/drag-and-drop-indicator.tsx delete mode 100644 components/icons/src/icons/drag-and-drop.tsx delete mode 100644 components/icons/src/icons/drag-fill.tsx delete mode 100644 components/icons/src/icons/drop-down.tsx delete mode 100644 components/icons/src/icons/dropdown-list.tsx delete mode 100644 components/icons/src/icons/duplicate-cell.tsx delete mode 100644 components/icons/src/icons/duplicate.tsx delete mode 100644 components/icons/src/icons/edit-notes.tsx delete mode 100644 components/icons/src/icons/edit.tsx delete mode 100644 components/icons/src/icons/elaborate.tsx delete mode 100644 components/icons/src/icons/emoji.tsx delete mode 100644 components/icons/src/icons/end-footnote.tsx delete mode 100644 components/icons/src/icons/equalto.tsx delete mode 100644 components/icons/src/icons/erase.tsx delete mode 100644 components/icons/src/icons/error-treeview.tsx delete mode 100644 components/icons/src/icons/even-page-break.tsx delete mode 100644 components/icons/src/icons/exit-full-screen.tsx delete mode 100644 components/icons/src/icons/expand.tsx delete mode 100644 components/icons/src/icons/export-csv.tsx delete mode 100644 components/icons/src/icons/export-excel.tsx rename components/icons/src/icons/{export-pdf-1.tsx => export-pdf-arrow.tsx} (96%) delete mode 100644 components/icons/src/icons/export-pdf.tsx delete mode 100644 components/icons/src/icons/export-png.tsx delete mode 100644 components/icons/src/icons/export-svg.tsx rename components/icons/src/icons/{export-word-1.tsx => export-word.tsx} (88%) delete mode 100644 components/icons/src/icons/export-xls.tsx delete mode 100644 components/icons/src/icons/export.tsx delete mode 100644 components/icons/src/icons/eye-slash.tsx delete mode 100644 components/icons/src/icons/eye.tsx delete mode 100644 components/icons/src/icons/fade.tsx delete mode 100644 components/icons/src/icons/field-settings.tsx delete mode 100644 components/icons/src/icons/file-document.tsx delete mode 100644 components/icons/src/icons/file-new.tsx delete mode 100644 components/icons/src/icons/filter-active.tsx delete mode 100644 components/icons/src/icons/filter-clear.tsx delete mode 100644 components/icons/src/icons/filter-main.tsx delete mode 100644 components/icons/src/icons/filter.tsx delete mode 100644 components/icons/src/icons/filtered-sort-ascending.tsx delete mode 100644 components/icons/src/icons/filtered-sort-descending.tsx delete mode 100644 components/icons/src/icons/filtered.tsx delete mode 100644 components/icons/src/icons/filters.tsx delete mode 100644 components/icons/src/icons/first-page.tsx delete mode 100644 components/icons/src/icons/fixed-column-width.tsx delete mode 100644 components/icons/src/icons/flags.tsx delete mode 100644 components/icons/src/icons/flip-horizontal.tsx delete mode 100644 components/icons/src/icons/flip-vertical.tsx delete mode 100644 components/icons/src/icons/folder-open.tsx delete mode 100644 components/icons/src/icons/folder.tsx delete mode 100644 components/icons/src/icons/font-color.tsx delete mode 100644 components/icons/src/icons/font-name.tsx delete mode 100644 components/icons/src/icons/font-size.tsx delete mode 100644 components/icons/src/icons/food-and-drinks.tsx delete mode 100644 components/icons/src/icons/footer.tsx delete mode 100644 components/icons/src/icons/form-field.tsx delete mode 100644 components/icons/src/icons/format-painter.tsx delete mode 100644 components/icons/src/icons/frame-bevel.tsx delete mode 100644 components/icons/src/icons/frame-custom.tsx rename components/icons/src/icons/{frame-11.tsx => frame-extra-tall.tsx} (77%) rename components/icons/src/icons/{frame-5.tsx => frame-full-width.tsx} (82%) delete mode 100644 components/icons/src/icons/frame-hook.tsx rename components/icons/src/icons/{frame-2.tsx => frame-horizontal-narrow.tsx} (80%) delete mode 100644 components/icons/src/icons/frame-inset.tsx rename components/icons/src/icons/{frame-4.tsx => frame-large.tsx} (83%) delete mode 100644 components/icons/src/icons/frame-line.tsx delete mode 100644 components/icons/src/icons/frame-mat.tsx rename components/icons/src/icons/{frame-3.tsx => frame-medium.tsx} (83%) rename components/icons/src/icons/{frame-6.tsx => frame-narrow.tsx} (83%) delete mode 100644 components/icons/src/icons/frame-none.tsx rename components/icons/src/icons/{frame-1.tsx => frame-square.tsx} (83%) rename components/icons/src/icons/{frame-10.tsx => frame-tall.tsx} (78%) rename components/icons/src/icons/{frame-7.tsx => frame-vertical-narrow.tsx} (75%) rename components/icons/src/icons/{frame-9.tsx => frame-vertical-wide.tsx} (76%) rename components/icons/src/icons/{frame-8.tsx => frame-vertical.tsx} (77%) delete mode 100644 components/icons/src/icons/freeze-column.tsx delete mode 100644 components/icons/src/icons/freeze-pane.tsx delete mode 100644 components/icons/src/icons/freeze-row.tsx delete mode 100644 components/icons/src/icons/full-screen.tsx delete mode 100644 components/icons/src/icons/gantt-gripper.tsx delete mode 100644 components/icons/src/icons/grain.tsx delete mode 100644 components/icons/src/icons/grammar-check.tsx delete mode 100644 components/icons/src/icons/grand-total.tsx delete mode 100644 components/icons/src/icons/greater-than.tsx delete mode 100644 components/icons/src/icons/grid-view.tsx delete mode 100644 components/icons/src/icons/grip-vertical.tsx rename components/icons/src/icons/{group-2.tsx => group-combine.tsx} (91%) delete mode 100644 components/icons/src/icons/group-icon.tsx rename components/icons/src/icons/{group-1.tsx => group-items.tsx} (93%) delete mode 100644 components/icons/src/icons/hand-gestures.tsx delete mode 100644 components/icons/src/icons/header.tsx delete mode 100644 components/icons/src/icons/hide-formula-bar.tsx delete mode 100644 components/icons/src/icons/hide-gridlines.tsx delete mode 100644 components/icons/src/icons/hide-headings.tsx delete mode 100644 components/icons/src/icons/highlight-color.tsx delete mode 100644 components/icons/src/icons/highlight.tsx delete mode 100644 components/icons/src/icons/home.tsx delete mode 100644 components/icons/src/icons/hyperlink-copy.tsx delete mode 100644 components/icons/src/icons/hyperlink-edit.tsx delete mode 100644 components/icons/src/icons/hyperlink-open.tsx delete mode 100644 components/icons/src/icons/hyperlink-remove.tsx delete mode 100644 components/icons/src/icons/iconsets.tsx rename components/icons/src/icons/{caption-1.tsx => image-caption.tsx} (85%) delete mode 100644 components/icons/src/icons/image.tsx rename components/icons/src/icons/{import-1.tsx => import-turbo.tsx} (94%) delete mode 100644 components/icons/src/icons/import-word.tsx delete mode 100644 components/icons/src/icons/import.tsx delete mode 100644 components/icons/src/icons/increase-indent-rtl.tsx delete mode 100644 components/icons/src/icons/increase-indent.tsx delete mode 100644 components/icons/src/icons/index.ts delete mode 100644 components/icons/src/icons/insert-above.tsx delete mode 100644 components/icons/src/icons/insert-below.tsx delete mode 100644 components/icons/src/icons/insert-code.tsx delete mode 100644 components/icons/src/icons/insert-left.tsx delete mode 100644 components/icons/src/icons/insert-right.tsx delete mode 100644 components/icons/src/icons/insert-sheet.tsx rename components/icons/src/icons/{intermediate-state-2.tsx => intermediate-bar.tsx} (75%) delete mode 100644 components/icons/src/icons/italic.tsx delete mode 100644 components/icons/src/icons/justify.tsx delete mode 100644 components/icons/src/icons/kpi.tsx delete mode 100644 components/icons/src/icons/last-page.tsx delete mode 100644 components/icons/src/icons/launcher.tsx delete mode 100644 components/icons/src/icons/layers.tsx delete mode 100644 components/icons/src/icons/length.tsx delete mode 100644 components/icons/src/icons/less-than.tsx delete mode 100644 components/icons/src/icons/level-1.tsx delete mode 100644 components/icons/src/icons/level-2.tsx delete mode 100644 components/icons/src/icons/level-3.tsx delete mode 100644 components/icons/src/icons/level-4.tsx delete mode 100644 components/icons/src/icons/level-5.tsx delete mode 100644 components/icons/src/icons/line-normal.tsx delete mode 100644 components/icons/src/icons/line-small.tsx delete mode 100644 components/icons/src/icons/line-spacing.tsx delete mode 100644 components/icons/src/icons/line-very-small.tsx delete mode 100644 components/icons/src/icons/line.tsx delete mode 100644 components/icons/src/icons/link-remove.tsx delete mode 100644 components/icons/src/icons/link.tsx delete mode 100644 components/icons/src/icons/linked-style.tsx delete mode 100644 components/icons/src/icons/list-ordered-rtl.tsx delete mode 100644 components/icons/src/icons/list-ordered.tsx delete mode 100644 components/icons/src/icons/list-unordered-rtl.tsx delete mode 100644 components/icons/src/icons/list-unordered.tsx delete mode 100644 components/icons/src/icons/location.tsx delete mode 100644 components/icons/src/icons/lock.tsx delete mode 100644 components/icons/src/icons/lower-case.tsx delete mode 100644 components/icons/src/icons/mdx.tsx delete mode 100644 components/icons/src/icons/menu.tsx delete mode 100644 components/icons/src/icons/merge-cells.tsx delete mode 100644 components/icons/src/icons/microphone.tsx delete mode 100644 components/icons/src/icons/month-agenda.tsx delete mode 100644 components/icons/src/icons/month.tsx delete mode 100644 components/icons/src/icons/more-chevron.tsx rename components/icons/src/icons/{more-horizontal-1.tsx => more-horizontal-filled.tsx} (88%) delete mode 100644 components/icons/src/icons/more-scatter-charts.tsx rename components/icons/src/icons/{more-vertical-1.tsx => more-vertical-filled.tsx} (89%) rename components/icons/src/icons/{more-vertical-2.tsx => more-vertical-outlined.tsx} (92%) delete mode 100644 components/icons/src/icons/mouse-pointer.tsx delete mode 100644 components/icons/src/icons/multiple-comment-resolve.tsx delete mode 100644 components/icons/src/icons/multiple-comment.tsx delete mode 100644 components/icons/src/icons/named-set.tsx delete mode 100644 components/icons/src/icons/nature.tsx delete mode 100644 components/icons/src/icons/none.tsx delete mode 100644 components/icons/src/icons/notes.tsx delete mode 100644 components/icons/src/icons/number-formatting.tsx delete mode 100644 components/icons/src/icons/objects.tsx delete mode 100644 components/icons/src/icons/odd-page-break.tsx delete mode 100644 components/icons/src/icons/opacity.tsx delete mode 100644 components/icons/src/icons/open-link.tsx delete mode 100644 components/icons/src/icons/order.tsx delete mode 100644 components/icons/src/icons/organize-pdf.tsx delete mode 100644 components/icons/src/icons/page-column-left.tsx delete mode 100644 components/icons/src/icons/page-column-one.tsx delete mode 100644 components/icons/src/icons/page-column-right.tsx delete mode 100644 components/icons/src/icons/page-column-three.tsx delete mode 100644 components/icons/src/icons/page-column-two.tsx delete mode 100644 components/icons/src/icons/page-column.tsx delete mode 100644 components/icons/src/icons/page-columns.tsx delete mode 100644 components/icons/src/icons/page-numbering.tsx delete mode 100644 components/icons/src/icons/page-setup.tsx delete mode 100644 components/icons/src/icons/page-size.tsx delete mode 100644 components/icons/src/icons/page-text-wrap.tsx delete mode 100644 components/icons/src/icons/paint-bucket.tsx delete mode 100644 components/icons/src/icons/pan.tsx delete mode 100644 components/icons/src/icons/paragraph.tsx delete mode 100644 components/icons/src/icons/password.tsx delete mode 100644 components/icons/src/icons/paste-match-destination.tsx delete mode 100644 components/icons/src/icons/paste-style.tsx delete mode 100644 components/icons/src/icons/paste-text-only.tsx delete mode 100644 components/icons/src/icons/paste.tsx delete mode 100644 components/icons/src/icons/pause.tsx delete mode 100644 components/icons/src/icons/pentagon.tsx delete mode 100644 components/icons/src/icons/people.tsx delete mode 100644 components/icons/src/icons/perimeter.tsx delete mode 100644 components/icons/src/icons/play.tsx delete mode 100644 components/icons/src/icons/plus.tsx delete mode 100644 components/icons/src/icons/preformat-code.tsx delete mode 100644 components/icons/src/icons/print-layout.tsx delete mode 100644 components/icons/src/icons/print.tsx rename components/icons/src/icons/{properties-2.tsx => properties-panel.tsx} (92%) rename components/icons/src/icons/{properties-1.tsx => properties-tools.tsx} (96%) delete mode 100644 components/icons/src/icons/protect-sheet.tsx delete mode 100644 components/icons/src/icons/protect-workbook.tsx delete mode 100644 components/icons/src/icons/radio-button.tsx delete mode 100644 components/icons/src/icons/radius.tsx delete mode 100644 components/icons/src/icons/reapply.tsx delete mode 100644 components/icons/src/icons/rectangle.tsx delete mode 100644 components/icons/src/icons/recurrence-edit.tsx delete mode 100644 components/icons/src/icons/redact.tsx delete mode 100644 components/icons/src/icons/redaction.tsx delete mode 100644 components/icons/src/icons/redo.tsx delete mode 100644 components/icons/src/icons/refresh.tsx delete mode 100644 components/icons/src/icons/rename.tsx delete mode 100644 components/icons/src/icons/repeat.tsx delete mode 100644 components/icons/src/icons/repeating-section.tsx delete mode 100644 components/icons/src/icons/rephrase.tsx delete mode 100644 components/icons/src/icons/replace.tsx delete mode 100644 components/icons/src/icons/reset.tsx delete mode 100644 components/icons/src/icons/resize.tsx delete mode 100644 components/icons/src/icons/resizer-horizontal.tsx delete mode 100644 components/icons/src/icons/resizer-right.tsx delete mode 100644 components/icons/src/icons/resizer-vertical.tsx delete mode 100644 components/icons/src/icons/resizer.tsx rename components/icons/src/icons/{restart-at-1.tsx => restart-at.tsx} (92%) delete mode 100644 components/icons/src/icons/saturation.tsx delete mode 100644 components/icons/src/icons/save-as.tsx delete mode 100644 components/icons/src/icons/save.tsx delete mode 100644 components/icons/src/icons/search.tsx delete mode 100644 components/icons/src/icons/select-all.tsx delete mode 100644 components/icons/src/icons/selection.tsx delete mode 100644 components/icons/src/icons/send-backward.tsx delete mode 100644 components/icons/src/icons/send-to-back.tsx delete mode 100644 components/icons/src/icons/send.tsx delete mode 100644 components/icons/src/icons/settings.tsx delete mode 100644 components/icons/src/icons/shapes.tsx delete mode 100644 components/icons/src/icons/sharpness.tsx delete mode 100644 components/icons/src/icons/shorten.tsx delete mode 100644 components/icons/src/icons/show-hide-panel.tsx delete mode 100644 components/icons/src/icons/signature.tsx delete mode 100644 components/icons/src/icons/smart-paste.tsx rename components/icons/src/icons/{sort-ascending-2.tsx => sort-ascending-boxed.tsx} (89%) rename components/icons/src/icons/{sorting-2.tsx => sort-ascending-list.tsx} (85%) delete mode 100644 components/icons/src/icons/sort-ascending.tsx rename components/icons/src/icons/{sorting-1.tsx => sort-bidirectional.tsx} (86%) rename components/icons/src/icons/{sort-descending-2.tsx => sort-descending-boxed.tsx} (89%) rename components/icons/src/icons/{sorting-3.tsx => sort-descending-list.tsx} (83%) delete mode 100644 components/icons/src/icons/sort-descending.tsx delete mode 100644 components/icons/src/icons/spacing-after.tsx delete mode 100644 components/icons/src/icons/spacing-before.tsx delete mode 100644 components/icons/src/icons/spell-check.tsx delete mode 100644 components/icons/src/icons/split-horizontal.tsx delete mode 100644 components/icons/src/icons/split-vertical.tsx delete mode 100644 components/icons/src/icons/square-head-fill.tsx delete mode 100644 components/icons/src/icons/square-head.tsx delete mode 100644 components/icons/src/icons/square-tail-fill.tsx delete mode 100644 components/icons/src/icons/square-tail.tsx delete mode 100644 components/icons/src/icons/squiggly.tsx delete mode 100644 components/icons/src/icons/stamp.tsx delete mode 100644 components/icons/src/icons/star-filled.tsx delete mode 100644 components/icons/src/icons/stop-rectangle.tsx delete mode 100644 components/icons/src/icons/strikethrough.tsx delete mode 100644 components/icons/src/icons/stroke-width.tsx delete mode 100644 components/icons/src/icons/style.tsx delete mode 100644 components/icons/src/icons/sub-total.tsx delete mode 100644 components/icons/src/icons/subscript.tsx delete mode 100644 components/icons/src/icons/sum.tsx delete mode 100644 components/icons/src/icons/superscript.tsx delete mode 100644 components/icons/src/icons/symbols.tsx delete mode 100644 components/icons/src/icons/table-align-center.tsx delete mode 100644 components/icons/src/icons/table-align-left.tsx delete mode 100644 components/icons/src/icons/table-align-right.tsx delete mode 100644 components/icons/src/icons/table-border-custom.tsx delete mode 100644 components/icons/src/icons/table-cell-none.tsx delete mode 100644 components/icons/src/icons/table-cell.tsx delete mode 100644 components/icons/src/icons/table-delete.tsx rename components/icons/src/icons/{table-2.tsx => table-grid.tsx} (87%) delete mode 100644 components/icons/src/icons/table-header.tsx delete mode 100644 components/icons/src/icons/table-insert-column.tsx delete mode 100644 components/icons/src/icons/table-insert-row.tsx delete mode 100644 components/icons/src/icons/table-merge.tsx delete mode 100644 components/icons/src/icons/table-nested.tsx delete mode 100644 components/icons/src/icons/table-of-content.tsx delete mode 100644 components/icons/src/icons/table-overwrite-cells.tsx delete mode 100644 components/icons/src/icons/table-update.tsx delete mode 100644 components/icons/src/icons/table.tsx delete mode 100644 components/icons/src/icons/text-alternative.tsx delete mode 100644 components/icons/src/icons/text-annotation.tsx delete mode 100644 components/icons/src/icons/text-form.tsx delete mode 100644 components/icons/src/icons/text-header.tsx delete mode 100644 components/icons/src/icons/text-outline.tsx delete mode 100644 components/icons/src/icons/text-that-contains.tsx delete mode 100644 components/icons/src/icons/text-wrap.tsx delete mode 100644 components/icons/src/icons/thumbnail.tsx delete mode 100644 components/icons/src/icons/thumbs-down-fill.tsx delete mode 100644 components/icons/src/icons/thumbs-down.tsx delete mode 100644 components/icons/src/icons/thumbs-up-fill.tsx delete mode 100644 components/icons/src/icons/thumbs-up.tsx delete mode 100644 components/icons/src/icons/time-zone.tsx delete mode 100644 components/icons/src/icons/timeline-day.tsx delete mode 100644 components/icons/src/icons/timeline-month.tsx delete mode 100644 components/icons/src/icons/timeline-today.tsx delete mode 100644 components/icons/src/icons/timeline-week.tsx delete mode 100644 components/icons/src/icons/timeline-work-week.tsx delete mode 100644 components/icons/src/icons/tint.tsx delete mode 100644 components/icons/src/icons/top-10.tsx delete mode 100644 components/icons/src/icons/top-bottom-rules.tsx delete mode 100644 components/icons/src/icons/transform-left.tsx delete mode 100644 components/icons/src/icons/transform-right.tsx delete mode 100644 components/icons/src/icons/transform.tsx delete mode 100644 components/icons/src/icons/translate.tsx delete mode 100644 components/icons/src/icons/trash.tsx delete mode 100644 components/icons/src/icons/travel-and-places.tsx delete mode 100644 components/icons/src/icons/triangle.tsx delete mode 100644 components/icons/src/icons/two-column.tsx delete mode 100644 components/icons/src/icons/two-row.tsx delete mode 100644 components/icons/src/icons/underline.tsx delete mode 100644 components/icons/src/icons/undo.tsx delete mode 100644 components/icons/src/icons/unfiltered.tsx rename components/icons/src/icons/{ungroup-2.tsx => ungroup-divide.tsx} (92%) rename components/icons/src/icons/{ungroup-1.tsx => ungroup-items.tsx} (90%) delete mode 100644 components/icons/src/icons/unlock.tsx rename components/icons/src/icons/{upload-1.tsx => upload.tsx} (83%) delete mode 100644 components/icons/src/icons/upper-case.tsx delete mode 100644 components/icons/src/icons/user-defined.tsx delete mode 100644 components/icons/src/icons/user.tsx delete mode 100644 components/icons/src/icons/vertical-align-bottom.tsx delete mode 100644 components/icons/src/icons/vertical-align-center.tsx delete mode 100644 components/icons/src/icons/vertical-align-top.tsx delete mode 100644 components/icons/src/icons/video.tsx delete mode 100644 components/icons/src/icons/view-side.tsx delete mode 100644 components/icons/src/icons/volume.tsx delete mode 100644 components/icons/src/icons/warning.tsx delete mode 100644 components/icons/src/icons/web-layout.tsx delete mode 100644 components/icons/src/icons/week.tsx delete mode 100644 components/icons/src/icons/xml-mapping.tsx delete mode 100644 components/icons/src/icons/zoom-in.tsx delete mode 100644 components/icons/src/icons/zoom-out.tsx delete mode 100644 components/icons/src/icons/zoom-to-fit.tsx delete mode 100644 components/icons/src/index.ts delete mode 100644 components/icons/src/svg-icon.tsx delete mode 100644 components/icons/tsconfig.json delete mode 100644 components/inputs/CHANGELOG.md delete mode 100644 components/inputs/README.md delete mode 100644 components/inputs/package.json delete mode 100644 components/inputs/src/common/index.ts delete mode 100644 components/inputs/src/common/inputbase.tsx create mode 100644 components/inputs/src/form-validator/form-validator.tsx create mode 100644 components/inputs/src/form-validator/index.ts delete mode 100644 components/inputs/src/index.ts rename components/{buttons/styles/check-box => inputs/src/inputs/input}/_all.scss (100%) rename components/inputs/{styles => src/inputs}/input/_layout.scss (56%) rename components/inputs/{styles => src/inputs}/input/_material3-definition.scss (64%) rename components/{buttons/styles/radio-button => inputs/src/inputs/numerictextbox}/_all.scss (100%) create mode 100644 components/inputs/src/inputs/numerictextbox/_layout.scss create mode 100644 components/inputs/src/inputs/numerictextbox/_material3-definition.scss rename components/inputs/{styles/numerictextbox => src/inputs/textarea}/_all.scss (100%) rename components/inputs/{styles => src/inputs}/textarea/_layout.scss (81%) rename components/{popups/styles/popup => inputs/src/inputs/textarea}/_material3-definition.scss (100%) rename components/inputs/{styles/textarea => src/inputs/textbox}/_all.scss (100%) rename components/inputs/{styles => src/inputs}/textbox/_layout.scss (51%) create mode 100644 components/inputs/src/inputs/textbox/_material3-definition.scss create mode 100644 components/inputs/src/numeric-textbox/index.ts rename components/inputs/src/{numerictextbox/numerictextbox.tsx => numeric-textbox/numeric-textbox.tsx} (72%) delete mode 100644 components/inputs/src/numerictextbox/index.ts delete mode 100644 components/inputs/src/textarea/index.ts delete mode 100644 components/inputs/src/textarea/textarea.tsx delete mode 100644 components/inputs/src/textbox/index.ts delete mode 100644 components/inputs/src/textbox/textbox.tsx delete mode 100644 components/inputs/styles/input/_all.scss delete mode 100644 components/inputs/styles/input/_material3-dark-definition.scss delete mode 100644 components/inputs/styles/input/_responsive.scss delete mode 100644 components/inputs/styles/input/material3-dark.scss delete mode 100644 components/inputs/styles/input/material3.scss delete mode 100644 components/inputs/styles/material3-dark.scss delete mode 100644 components/inputs/styles/material3.scss delete mode 100644 components/inputs/styles/numerictextbox/_layout.scss delete mode 100644 components/inputs/styles/numerictextbox/_material3-dark-definition.scss delete mode 100644 components/inputs/styles/numerictextbox/_material3-definition.scss delete mode 100644 components/inputs/styles/numerictextbox/material3-dark.scss delete mode 100644 components/inputs/styles/numerictextbox/material3.scss delete mode 100644 components/inputs/styles/textarea/_material3-dark-definition.scss delete mode 100644 components/inputs/styles/textarea/_material3-definition.scss delete mode 100644 components/inputs/styles/textarea/material3-dark.scss delete mode 100644 components/inputs/styles/textarea/material3.scss delete mode 100644 components/inputs/styles/textbox/_material3-dark-definition.scss delete mode 100644 components/inputs/styles/textbox/_material3-definition.scss delete mode 100644 components/inputs/styles/textbox/material3-dark.scss delete mode 100644 components/inputs/styles/textbox/material3.scss create mode 100644 components/lists/CHANGELOG.md create mode 100644 components/lists/README.md rename components/{notifications => lists}/gulpfile.js (100%) rename components/{popups => lists}/license (100%) rename components/{buttons => lists}/package.json (54%) create mode 100644 components/lists/src/common/index.ts create mode 100644 components/lists/src/common/list-base.tsx create mode 100644 components/lists/src/index.ts create mode 100644 components/lists/src/list-view/index.ts create mode 100644 components/lists/src/list-view/list-view.tsx rename components/{splitbuttons/styles/drop-down-button => lists/src/lists/list-base}/_all.scss (100%) create mode 100644 components/lists/src/lists/list-base/_layout.scss create mode 100644 components/lists/src/lists/list-base/_material3-definition.scss create mode 100644 components/lists/src/lists/list-base/_theme.scss create mode 100644 components/lists/src/lists/list-view/_all.scss create mode 100644 components/lists/src/lists/list-view/_layout.scss create mode 100644 components/lists/src/lists/list-view/_material3-definition.scss create mode 100644 components/lists/src/lists/list-view/_theme.scss create mode 100644 components/lists/styles/list-base/_all.scss create mode 100644 components/lists/styles/list-base/_layout.scss create mode 100644 components/lists/styles/list-base/_material3-definition.scss create mode 100644 components/lists/styles/list-base/_theme.scss create mode 100644 components/lists/styles/list-view/_all.scss create mode 100644 components/lists/styles/list-view/_layout.scss create mode 100644 components/lists/styles/list-view/_material3-definition.scss create mode 100644 components/lists/styles/list-view/_theme.scss rename components/{notifications => lists}/tsconfig.json (99%) delete mode 100644 components/navigations/CHANGELOG.md delete mode 100644 components/navigations/README.md delete mode 100644 components/navigations/src/common/h-scroll.tsx delete mode 100644 components/navigations/src/common/index.ts delete mode 100644 components/navigations/src/common/v-scroll.tsx rename components/navigations/src/{contextMenu => context-menu}/context-menu.tsx (97%) rename components/navigations/src/{contextMenu => context-menu}/index.ts (100%) delete mode 100644 components/navigations/src/index.ts create mode 100644 components/navigations/src/navigations/context-menu/_all.scss create mode 100644 components/navigations/src/navigations/context-menu/_layout.scss create mode 100644 components/navigations/src/navigations/context-menu/_material3-definition.scss create mode 100644 components/navigations/src/navigations/context-menu/_theme.scss create mode 100644 components/navigations/src/navigations/h-scroll/_all.scss create mode 100644 components/navigations/src/navigations/h-scroll/_layout.scss create mode 100644 components/navigations/src/navigations/h-scroll/_material3-definition.scss create mode 100644 components/navigations/src/navigations/h-scroll/_theme.scss create mode 100644 components/navigations/src/navigations/toolbar/_all.scss create mode 100644 components/navigations/src/navigations/toolbar/_layout.scss create mode 100644 components/navigations/src/navigations/toolbar/_material3-definition.scss create mode 100644 components/navigations/src/navigations/toolbar/_theme.scss create mode 100644 components/navigations/src/navigations/v-scroll/_all.scss create mode 100644 components/navigations/src/navigations/v-scroll/_layout.scss create mode 100644 components/navigations/src/navigations/v-scroll/_material3-definition.scss create mode 100644 components/navigations/src/navigations/v-scroll/_theme.scss delete mode 100644 components/navigations/src/toolbar/index.ts delete mode 100644 components/navigations/src/toolbar/toolbar-item.tsx delete mode 100644 components/navigations/src/toolbar/toolbar-multi-row.tsx delete mode 100644 components/navigations/src/toolbar/toolbar-popup.tsx delete mode 100644 components/navigations/src/toolbar/toolbar-scrollable.tsx delete mode 100644 components/navigations/src/toolbar/toolbar-separator.tsx delete mode 100644 components/navigations/src/toolbar/toolbar-spacer.tsx delete mode 100644 components/navigations/src/toolbar/toolbar.tsx delete mode 100644 components/navigations/styles/context-menu/_layout.scss delete mode 100644 components/navigations/styles/context-menu/_material3-dark-definition.scss delete mode 100644 components/navigations/styles/context-menu/_material3-definition.scss delete mode 100644 components/navigations/styles/context-menu/_theme-mixin.scss delete mode 100644 components/navigations/styles/context-menu/_theme.scss delete mode 100644 components/navigations/styles/context-menu/material3-dark.scss delete mode 100644 components/navigations/styles/context-menu/material3.scss delete mode 100644 components/navigations/styles/h-scroll/_layout.scss delete mode 100644 components/navigations/styles/h-scroll/_material3-dark-definition.scss delete mode 100644 components/navigations/styles/h-scroll/_material3-definition.scss delete mode 100644 components/navigations/styles/h-scroll/_theme.scss delete mode 100644 components/navigations/styles/h-scroll/material3-dark.scss delete mode 100644 components/navigations/styles/h-scroll/material3.scss delete mode 100644 components/navigations/styles/material3-dark.scss delete mode 100644 components/navigations/styles/material3.scss delete mode 100644 components/navigations/styles/toolbar/_layout.scss delete mode 100644 components/navigations/styles/toolbar/_material3-dark-definition.scss delete mode 100644 components/navigations/styles/toolbar/_material3-definition.scss delete mode 100644 components/navigations/styles/toolbar/_theme.scss delete mode 100644 components/navigations/styles/toolbar/material3-dark.scss delete mode 100644 components/navigations/styles/toolbar/material3.scss delete mode 100644 components/navigations/styles/v-scroll/_layout.scss delete mode 100644 components/navigations/styles/v-scroll/_material3-dark-definition.scss delete mode 100644 components/navigations/styles/v-scroll/_material3-definition.scss delete mode 100644 components/navigations/styles/v-scroll/_theme.scss delete mode 100644 components/navigations/styles/v-scroll/material3-dark.scss delete mode 100644 components/navigations/styles/v-scroll/material3.scss delete mode 100644 components/notifications/CHANGELOG.md delete mode 100644 components/notifications/README.md delete mode 100644 components/notifications/src/index.ts delete mode 100644 components/notifications/src/message/index.ts delete mode 100644 components/notifications/src/message/message.tsx rename components/notifications/{styles => src/notifications}/message/_all.scss (100%) create mode 100644 components/notifications/src/notifications/message/_layout.scss create mode 100644 components/notifications/src/notifications/message/_material3-definition.scss rename components/notifications/{styles => src/notifications}/skeleton/_all.scss (100%) create mode 100644 components/notifications/src/notifications/skeleton/_layout.scss create mode 100644 components/notifications/src/notifications/skeleton/_material3-definition.scss create mode 100644 components/notifications/src/notifications/toast/_all.scss create mode 100644 components/notifications/src/notifications/toast/_layout.scss create mode 100644 components/notifications/src/notifications/toast/_material3-definition.scss create mode 100644 components/notifications/src/notifications/toast/_theme.scss delete mode 100644 components/notifications/src/skeleton/index.ts delete mode 100644 components/notifications/src/skeleton/skeleton.tsx delete mode 100644 components/notifications/src/toast/index.ts delete mode 100644 components/notifications/src/toast/toast.tsx delete mode 100644 components/notifications/styles/material3-dark.scss delete mode 100644 components/notifications/styles/material3.scss delete mode 100644 components/notifications/styles/message/_layout.scss delete mode 100644 components/notifications/styles/message/_material3-dark-definition.scss delete mode 100644 components/notifications/styles/message/_material3-definition.scss delete mode 100644 components/notifications/styles/message/material3-dark.scss delete mode 100644 components/notifications/styles/message/material3.scss delete mode 100644 components/notifications/styles/skeleton/_layout.scss delete mode 100644 components/notifications/styles/skeleton/_material3-dark-definition.scss delete mode 100644 components/notifications/styles/skeleton/_material3-definition.scss delete mode 100644 components/notifications/styles/skeleton/material3-dark.scss delete mode 100644 components/notifications/styles/skeleton/material3.scss delete mode 100644 components/notifications/styles/toast/_layout.scss delete mode 100644 components/notifications/styles/toast/_material3-dark-definition.scss delete mode 100644 components/notifications/styles/toast/_material3-definition.scss create mode 100644 components/notifications/styles/toast/_theme.scss delete mode 100644 components/notifications/styles/toast/material3-dark.scss delete mode 100644 components/notifications/styles/toast/material3.scss create mode 100644 components/pager/CHANGELOG.md rename components/{icons => pager}/README.md (55%) rename components/{popups => pager}/gulpfile.js (100%) rename components/{splitbuttons => pager}/license (100%) create mode 100644 components/pager/package.json create mode 100644 components/pager/src/index.ts create mode 100644 components/pager/src/numericContainer.tsx create mode 100644 components/pager/src/page.tsx create mode 100644 components/pager/src/pager/pager/_all.scss create mode 100644 components/pager/src/pager/pager/_layout.scss create mode 100644 components/pager/src/pager/pager/_material3-definition.scss create mode 100644 components/pager/src/pager/pager/_theme.scss create mode 100644 components/pager/src/usePager.tsx create mode 100644 components/pager/src/usePagerFocus.ts create mode 100644 components/pager/styles/material3.scss create mode 100644 components/pager/styles/pager/_all.scss create mode 100644 components/pager/styles/pager/_layout.scss create mode 100644 components/pager/styles/pager/_material3-definition.scss create mode 100644 components/pager/styles/pager/_theme.scss rename components/{buttons/styles/button => pager/styles/pager}/material3.scss (56%) rename components/{base => pager}/tsconfig.json (70%) delete mode 100644 components/popups/CHANGELOG.md delete mode 100644 components/popups/src/common/collision.tsx delete mode 100644 components/popups/src/common/index.ts delete mode 100644 components/popups/src/common/position.tsx create mode 100644 components/popups/src/common/resize.tsx create mode 100644 components/popups/src/dialog/dialog.tsx create mode 100644 components/popups/src/dialog/index.ts delete mode 100644 components/popups/src/index.ts delete mode 100644 components/popups/src/popup/index.ts delete mode 100644 components/popups/src/popup/popup.tsx rename components/popups/{styles/spinner => src/popups/dialog}/_all.scss (52%) create mode 100644 components/popups/src/popups/dialog/_layout.scss create mode 100644 components/popups/src/popups/dialog/_material3-definition.scss create mode 100644 components/popups/src/popups/dialog/_theme.scss rename components/popups/{styles => src/popups}/popup/_all.scss (100%) rename components/popups/{styles => src/popups}/popup/_layout.scss (51%) rename components/popups/{styles/popup/_material3-dark-definition.scss => src/popups/popup/_material3-definition.scss} (100%) rename components/{inputs/styles/textbox => popups/src/popups/spinner}/_all.scss (100%) rename components/popups/{styles => src/popups}/spinner/_layout.scss (100%) rename components/popups/{styles => src/popups}/spinner/_material3-definition.scss (100%) create mode 100644 components/popups/src/popups/tooltip/_all.scss create mode 100644 components/popups/src/popups/tooltip/_layout.scss create mode 100644 components/popups/src/popups/tooltip/_material3-definition.scss create mode 100644 components/popups/src/popups/tooltip/_theme.scss delete mode 100644 components/popups/src/spinner/index.ts delete mode 100644 components/popups/src/spinner/spinner.tsx delete mode 100644 components/popups/src/tooltip/index.ts delete mode 100644 components/popups/src/tooltip/tooltip.tsx rename components/{splitbuttons/styles/split-button => popups/styles/dialog}/_all.scss (52%) create mode 100644 components/popups/styles/dialog/_layout.scss create mode 100644 components/popups/styles/dialog/_material3-definition.scss create mode 100644 components/popups/styles/dialog/_theme.scss create mode 100644 components/popups/styles/dialog/material3.scss delete mode 100644 components/popups/styles/material3-dark.scss delete mode 100644 components/popups/styles/material3.scss delete mode 100644 components/popups/styles/popup/material3-dark.scss delete mode 100644 components/popups/styles/popup/material3.scss delete mode 100644 components/popups/styles/spinner/_material3-dark-definition.scss delete mode 100644 components/popups/styles/spinner/material3-dark.scss delete mode 100644 components/popups/styles/spinner/material3.scss delete mode 100644 components/popups/styles/tooltip/_layout.scss delete mode 100644 components/popups/styles/tooltip/_material3-dark-definition.scss delete mode 100644 components/popups/styles/tooltip/_material3-definition.scss delete mode 100644 components/popups/styles/tooltip/_theme.scss delete mode 100644 components/popups/styles/tooltip/material3-dark.scss delete mode 100644 components/popups/styles/tooltip/material3.scss delete mode 100644 components/splitbuttons/CHANGELOG.md delete mode 100644 components/splitbuttons/README.md delete mode 100644 components/splitbuttons/src/dropdown-button/dropdown-button.tsx delete mode 100644 components/splitbuttons/src/dropdown-button/index.ts delete mode 100644 components/splitbuttons/src/index.ts delete mode 100644 components/splitbuttons/src/split-button/index.ts delete mode 100644 components/splitbuttons/src/split-button/split-button.tsx delete mode 100644 components/splitbuttons/styles/drop-down-button/_layout.scss delete mode 100644 components/splitbuttons/styles/drop-down-button/_material3-dark-definition.scss delete mode 100644 components/splitbuttons/styles/drop-down-button/_material3-definition.scss create mode 100644 components/splitbuttons/styles/drop-down-button/_mixin.scss delete mode 100644 components/splitbuttons/styles/drop-down-button/_theme.scss delete mode 100644 components/splitbuttons/styles/drop-down-button/material3-dark.scss delete mode 100644 components/splitbuttons/styles/drop-down-button/material3.scss delete mode 100644 components/splitbuttons/styles/material3-dark.scss delete mode 100644 components/splitbuttons/styles/material3.scss delete mode 100644 components/splitbuttons/styles/split-button/_layout.scss delete mode 100644 components/splitbuttons/styles/split-button/_material3-dark-definition.scss delete mode 100644 components/splitbuttons/styles/split-button/_material3-definition.scss delete mode 100644 components/splitbuttons/styles/split-button/material3-dark.scss delete mode 100644 components/splitbuttons/styles/split-button/material3.scss create mode 100644 components/svg-tooltip-component/CHANGELOG.md rename components/{popups => svg-tooltip-component}/README.md (50%) rename components/{splitbuttons => svg-tooltip-component}/gulpfile.js (100%) rename components/{icons => svg-tooltip-component}/license (78%) rename components/{icons => svg-tooltip-component}/package.json (77%) create mode 100644 components/svg-tooltip-component/src/index.ts create mode 100644 components/svg-tooltip-component/src/svg-tooltip/SVG-Tooltip.tsx create mode 100644 components/svg-tooltip-component/src/svg-tooltip/enum.ts create mode 100644 components/svg-tooltip-component/src/svg-tooltip/helper.tsx create mode 100644 components/svg-tooltip-component/src/svg-tooltip/index.ts create mode 100644 components/svg-tooltip-component/src/svg-tooltip/models.ts create mode 100644 components/svg-tooltip-component/tsconfig.json diff --git a/README.md b/README.md index 18b384f..a8306ff 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,46 @@ From architecture to execution, every detail has been considered to boost perfor ### Material 3 Theming Built-in Material 3 stylesheets provide a modern, accessible design out of the box. Enjoy sharp visuals, consistent theming, and minimal setup. -## Control List +## Component List + +### Data Visualization + + + + + + + + +
+ Chart + + npm package @syncfusion/react-charts + + Source + + Live demo +
+ +### Grid + + + + + + + + +
+ Data Grid + + npm package @syncfusion/react-grid + + Source + + Live demo +
+ ### Buttons @@ -163,7 +202,7 @@ Built-in Material 3 stylesheets provide a modern, accessible design out of the b Numeric TextBox - + npm package @syncfusion/react-inputs @@ -195,6 +234,18 @@ Built-in Material 3 stylesheets provide a modern, accessible design out of the b Live demo + + + Form + + + Source + + + Live demo + + + Checkbox @@ -220,6 +271,7 @@ Built-in Material 3 stylesheets provide a modern, accessible design out of the b Live demo + ### Layout @@ -229,7 +281,7 @@ Built-in Material 3 stylesheets provide a modern, accessible design out of the b Tooltip - + npm package @syncfusion/react-popups @@ -239,6 +291,17 @@ Built-in Material 3 stylesheets provide a modern, accessible design out of the b Live demo + + + Dialog + + + Source + + + Live demo + + ### Navigations @@ -280,6 +343,6 @@ Built-in Material 3 stylesheets provide a modern, accessible design out of the b Check the license detail [here](https://github.com/syncfusion/react-ui-components/blob/master/license). ## Changelog -Check the changelog [here](https://react-api.syncfusion.com/release-notes/30.1.37). +Check the changelog [here](https://react-api.syncfusion.com/release-notes/31.1.17). © Copyright 2025 Syncfusion, Inc. All Rights Reserved. The Syncfusion Essential Studio license and copyright applies to this distribution. \ No newline at end of file diff --git a/components/base/src/animate.tsx b/components/base/src/animate.tsx new file mode 100644 index 0000000..1b325bd --- /dev/null +++ b/components/base/src/animate.tsx @@ -0,0 +1,767 @@ +import { forwardRef, cloneElement, useRef, useState, useMemo, useCallback, useEffect, useImperativeHandle, Ref, ReactElement, RefObject, JSX } from 'react'; +import { useProviderContext } from './provider'; +import { getActualElement, getElementRef } from './util'; +import { Effect } from './animation'; + +/** + * Interface for animate component props + */ +export interface AnimateProps { + /** + * Controls whether the animation plays the "in" (enter) or "out" (exit) effect. + * + * @default true + */ + in?: boolean; + + /** + * Determines if the animation should run on the initial mount. + * + * @default true + */ + appear?: boolean; + + /** + * Specifies the duration of the animate in milliseconds. + * + * @default 400 + */ + duration?: number; + + /** + * Specifies the timing function for the animate. + * + * @default 'ease' + */ + timingFunction?: string; + + /** + * Specifies the delay before the animate starts in milliseconds. + * + * @default 0 + */ + delay?: number; + + /** + * The content to be animated - must be a single React element. + */ + children: ReactElement; + + /** + * Additional CSS class names to apply to the animate element. + */ + className?: string; + + /** + * Triggers when animate is in-progress, providing progress information. + * + * @event progress + */ + onProgress?: (args: AnimateEvent) => void; + + /** + * Triggers when the animate starts, allowing execution of logic at animate start. + * + * @event begin + */ + onBegin?: (args: AnimateEvent) => void; + + /** + * Triggers when animate is completed, allowing execution of logic after animate finishes. + * + * @event end + */ + onEnd?: (args: AnimateEvent) => void; + + /** + * Triggers when animate fails due to errors, providing error information. + * + * @event fail + */ + onFail?: (args: AnimateEvent) => void; + + /** + * Internal prop to identify the component type for validation + * + * @private + */ + componentType?: 'Fade' | 'Flip' | 'Slide' | 'Zoom'; + + /** + * Internal prop for style declaration for smooth animation + * + * @private + */ + style?: CSSStyleDeclaration; +} + +/** + * Internal interface to trigger animate events with type validation + * + * @private + */ +export interface AnimateEvent { + /** + * Current timestamp of the animate. + */ + timeStamp?: number; + + /** + * The element being animated. + */ + element?: HTMLElement | { element?: HTMLElement }; + + /** + * animate type. + */ + effect?: Effect; + + /** + * animate duration in milliseconds. + */ + duration?: number; + + /** + * animate delay in milliseconds. + */ + delay?: number; + + /** + * animate timing function. + */ + timingFunction?: string; +} + +export interface IAnimate extends AnimateProps { + /** + * animate element reference. + * + * @private + */ + element: HTMLElement | null; + + /** + * Stops the current animate. + * + * @returns {void} + */ + stop?(): void; +} + +/** + * Collection of easing functions represented as cubic-bezier values + * + * @type {Record} + */ +const easing: Record = { + ease: 'cubic-bezier(0.250, 0.100, 0.250, 1.000)', + linear: 'cubic-bezier(0.250, 0.250, 0.750, 0.750)', + easeIn: 'cubic-bezier(0.420, 0.000, 1.000, 1.000)', + easeOut: 'cubic-bezier(0.000, 0.000, 0.580, 1.000)', + easeInOut: 'cubic-bezier(0.420, 0.000, 0.580, 1.000)', + elasticInOut: 'cubic-bezier(0.5,-0.58,0.38,1.81)', + elasticIn: 'cubic-bezier(0.17,0.67,0.59,1.81)', + elasticOut: 'cubic-bezier(0.7,-0.75,0.99,1.01)' +}; + +/** + * Maps animate categories to their corresponding effect types + * + * @type {Record} + */ +const validEffectGroups: Record = { + 'Fade': ['FadeIn', 'FadeOut', 'FadeZoomIn', 'FadeZoomOut'], + 'Flip': ['FlipLeftDownIn', 'FlipLeftDownOut', 'FlipLeftUpIn', 'FlipLeftUpOut', + 'FlipRightDownIn', 'FlipRightDownOut', 'FlipRightUpIn', 'FlipRightUpOut', + 'FlipXDownIn', 'FlipXDownOut', 'FlipXUpIn', 'FlipXUpOut', + 'FlipYLeftIn', 'FlipYLeftOut', 'FlipYRightIn', 'FlipYRightOut'], + 'Slide': ['SlideBottomIn', 'SlideBottomOut', 'SlideDown', 'SlideLeft', + 'SlideLeftIn', 'SlideLeftOut', 'SlideRight', 'SlideRightIn', + 'SlideRightOut', 'SlideTopIn', 'SlideTopOut', 'SlideUp'], + 'Zoom': ['ZoomIn', 'ZoomOut'] +}; + +type GlobalAnimateProps = { effect?: Effect; } & AnimateProps; +/** + * The Animate component provides options to animate HTML DOM elements. + * It allows wrapping content with animation effects that can be controlled via props. + * + * ```tsx + * + *
Animation content
+ *
+ * ``` + */ +export const Animate: React.ForwardRefExoticComponent> = + forwardRef((props: GlobalAnimateProps, ref: Ref>) => { + const { + effect = 'FadeIn', + duration = 400, + timingFunction = 'ease', + delay = 0, + children, + onProgress, + onBegin, + onEnd, + onFail, + componentType = 'Animation', + className, + style, + appear = true, + ...rest + } = props; + + const elementRef: RefObject = useRef(null); + const animationIdRef: RefObject = useRef(0); + const isAnimatingRef: RefObject = useRef(false); + const timeStampRef: RefObject = useRef(0); + const prevTimeStampRef: RefObject = useRef(0); + const { animate } = useProviderContext(); + const combinedRef: (node: HTMLElement | null) => void = (node: HTMLElement) => { + elementRef.current = node; + const childRef: React.RefCallback | React.RefObject | null = getElementRef(children); + if (typeof childRef === 'function') { + childRef(node); + } + else if (childRef && 'current' in childRef) { + (childRef as React.RefObject).current = node; + } + }; + const reflow: Function = (node: Element) => node.scrollTop; + + const getTimingFunction: () => string = useCallback((): string => { + return (timingFunction in easing) + ? easing[timingFunction as keyof typeof easing] + : timingFunction; + }, [timingFunction]); + + const validateAnimationType: () => boolean = useCallback(() => { + if (componentType === 'Animation') { + return true; + } + return validEffectGroups[`${componentType}`]?.includes(effect as Effect) || false; + }, [effect, componentType]); + + const stopAnimation: () => void = useCallback((): void => { + if (elementRef.current) { + elementRef.current = getActualElement(elementRef) as HTMLElement; + elementRef.current.style.animation = ''; + isAnimatingRef.current = false; + if (animationIdRef.current) { + cancelAnimationFrame(animationIdRef.current); + animationIdRef.current = 0; + } + if (onEnd) { + const eventData: AnimateEvent = { + element: elementRef.current, + effect, + duration, + delay, + timingFunction + }; + onEnd(eventData); + } + } + }, [duration, delay, effect, onEnd, timingFunction]); + + const animationStep: (timestamp: number) => void = useCallback((timestamp: number): void => { + try { + if (!elementRef.current || !isAnimatingRef.current) { + return; + } + elementRef.current = getActualElement(elementRef) as HTMLElement; + prevTimeStampRef.current = prevTimeStampRef.current === 0 ? timestamp : prevTimeStampRef.current; + timeStampRef.current = timestamp - prevTimeStampRef.current; + if (timeStampRef.current === 0 && onBegin) { + const eventData: AnimateEvent = { + element: elementRef.current, + effect, + duration, + delay, + timingFunction: getTimingFunction(), + timeStamp: 0 + }; + onBegin(eventData); + } + if (timeStampRef.current < duration && isAnimatingRef.current) { + elementRef.current.style.animation = `${effect} ${duration}ms ${getTimingFunction()}`; + if (onProgress) { + const eventData: AnimateEvent = { + element: elementRef.current, + effect, + duration, + delay, + timingFunction: getTimingFunction(), + timeStamp: timeStampRef.current + }; + onProgress(eventData); + } + animationIdRef.current = requestAnimationFrame(animationStep); + } else { + if (elementRef.current) { + elementRef.current.style.animation = ''; + isAnimatingRef.current = false; + } + if (animationIdRef.current) { + cancelAnimationFrame(animationIdRef.current); + animationIdRef.current = 0; + } + if (onEnd && elementRef.current) { + const eventData: AnimateEvent = { + element: elementRef.current, + effect, + duration, + delay, + timingFunction: getTimingFunction(), + timeStamp: timeStampRef.current + }; + onEnd(eventData); + } + } + } catch (e) { + if (animationIdRef.current) { + cancelAnimationFrame(animationIdRef.current); + animationIdRef.current = 0; + } + isAnimatingRef.current = false; + if (onFail) { + onFail(e); + } + } + }, [duration, delay, effect, onBegin, onEnd, onFail, onProgress, getTimingFunction]); + + const startAnimation: () => void = useCallback((): void => { + if (!elementRef.current) { + return undefined; + } + if (!validateAnimationType()) { + if (onFail) { + onFail({ + element: getActualElement(elementRef) as HTMLElement, + effect, + duration, + delay, + timingFunction + }); + } + return undefined; + } + elementRef.current = getActualElement(elementRef) as HTMLElement; + timeStampRef.current = 0; + prevTimeStampRef.current = 0; + isAnimatingRef.current = true; + reflow(elementRef.current); + animationIdRef.current = requestAnimationFrame(animationStep); + }, [animationStep]); + + useEffect(() => { + if (!children) { + return undefined; + } + if (!animate || !appear) { + if (onBegin) { + const eventData: AnimateEvent = { + element: getActualElement(elementRef), + effect, + duration, + delay, + timingFunction + }; + onBegin(eventData); + } + + if (onEnd) { + const eventData: AnimateEvent = { + element: getActualElement(elementRef), + effect, + duration, + delay, + timingFunction + }; + onEnd(eventData); + } + return undefined; + } + let timeoutId: number; + if (delay > 0) { + timeoutId = window.setTimeout(() => { + startAnimation(); + }, delay); + } else { + startAnimation(); + } + return () => { + if (timeoutId) { + window.clearTimeout(timeoutId); + } + if (elementRef.current) { + elementRef.current = getActualElement(elementRef) as HTMLElement; + elementRef.current.style.animation = ''; + isAnimatingRef.current = false; + if (animationIdRef.current) { + cancelAnimationFrame(animationIdRef.current); + animationIdRef.current = 0; + } + } + }; + }, [children, animate, delay, effect, duration, timingFunction, onBegin, onEnd, startAnimation, stopAnimation]); + + useImperativeHandle(ref, () => ({ + element: getActualElement(elementRef) as HTMLElement, + stop: stopAnimation + })); + + return cloneElement( + children, + { + ref: combinedRef, + className: [ + (children as JSX.Element).props.className, + className + ].filter(Boolean).join(' ') || undefined, + style: { + ...(children as JSX.Element).props.style, + ...style + }, + ...rest + } as React.HTMLAttributes & { ref: React.Ref } + ); + }); + +export default Animate; + +/** + * Fade component with in/out transition control + */ +export type FadeProps = AnimateProps; + +/** + * Fade component that automatically handles in/out transitions. + * Uses FadeIn when `in=true` and FadeOut when `in=false`. + * + * @example + * ```tsx + * import { Fade } from '@syncfusion/react-base'; + * + * + *
Content to fade
+ *
+ * ``` + */ +export const Fade: React.ForwardRefExoticComponent> = + forwardRef((props: FadeProps, ref: Ref): JSX.Element => { + const { + in: inProp = true, + appear = true, + duration = 400, + timingFunction = 'ease', + delay = 0, + onEnd, + ...rest + } = props; + const [hidden, setHidden] = useState(() => { + return !(inProp); + }); + const isExiting: boolean = useMemo(() => !inProp && !hidden, [inProp, hidden]); + const prevInProp: React.RefObject = useRef(inProp); + + useEffect(() => { + if (prevInProp.current !== inProp) { + if (inProp && hidden) { + setHidden(false); + } + prevInProp.current = inProp; + } + }, [inProp, hidden]); + + const handleAnimationEnd: (args: AnimateEvent) => void = useCallback((args: AnimateEvent) => { + if (!inProp) { + setHidden(true); + } + if (onEnd) { + onEnd(args); + } + }, [inProp, onEnd]); + + if (hidden) { + return null; + } + + const effect: Effect = isExiting ? 'FadeOut' : 'FadeIn'; + + return ( + + ); + }); + +/** + * Zoom component with in/out transition control + */ +export type ZoomProps = AnimateProps; + +/** + * Zoom component that automatically handles in/out transitions. + * Uses ZoomIn when `in=true` and ZoomOut when `in=false`. + * + * @example + * ```tsx + * import { Zoom } from '@syncfusion/react-base'; + * + * + *
Content to zoom
+ *
+ * ``` + */ +export const Zoom: React.ForwardRefExoticComponent> = + forwardRef((props: ZoomProps, ref: Ref): JSX.Element => { + const { + in: inProp = true, + appear = true, + duration = 400, + timingFunction = 'ease', + delay = 0, + onEnd, + ...rest + } = props; + const [hidden, setHidden] = useState(() => { + return !(inProp); + }); + const isExiting: boolean = useMemo(() => !inProp && !hidden, [inProp, hidden]); + const prevInProp: React.RefObject = useRef(inProp); + + useEffect(() => { + if (prevInProp.current !== inProp) { + if (inProp && hidden) { + setHidden(false); + } + prevInProp.current = inProp; + } + }, [inProp, hidden]); + + const handleAnimationEnd: (args: AnimateEvent) => void = useCallback((args: AnimateEvent) => { + if (!inProp) { + requestAnimationFrame(() => { + setHidden(true); + }); + } + if (onEnd) { + onEnd(args); + } + }, [inProp, onEnd]); + + if (hidden) { + return null; + } + + const effect: Effect = isExiting ? 'ZoomOut' : 'ZoomIn'; + + return ( + + ); + }); + +/** + * Slide component with in/out transition control and direction support + */ +export interface SlideProps extends AnimateProps { + /** + * Specifies the direction of the slide animation. + * Applicable values: 'Top', 'Bottom', 'Left', 'Right' + * + * @default 'Right' + */ + direction?: 'Top' | 'Bottom' | 'Left' | 'Right'; +} + +/** + * Slide component that automatically handles in/out transitions with direction control. + * Uses Slide{Direction}In when `in=true` and Slide{Direction}Out when `in=false`. + * + * @example + * ```tsx + * import { Slide } from '@syncfusion/react-base'; + * + * + *
Content to slide
+ *
+ * ``` + */ +export const Slide: React.ForwardRefExoticComponent> = + forwardRef((props: SlideProps, ref: Ref): JSX.Element => { + const { + in: inProp = true, + appear = true, + direction = 'Top', + duration = 400, + timingFunction = 'ease', + delay = 0, + onEnd, + ...rest + } = props; + const [hidden, setHidden] = useState(() => { + return !(inProp); + }); + const isExiting: boolean = useMemo(() => !inProp && !hidden, [inProp, hidden]); + const prevInProp: React.RefObject = useRef(inProp); + + useEffect(() => { + if (prevInProp.current !== inProp) { + if (inProp && hidden) { + setHidden(false); + } + prevInProp.current = inProp; + } + }, [inProp, hidden]); + + const handleAnimationEnd: (args: AnimateEvent) => void = useCallback((args: AnimateEvent) => { + if (!inProp) { + setHidden(true); + } + if (onEnd) { + onEnd(args); + } + }, [inProp, onEnd]); + + if (hidden) { + return null; + } + + const effect: Effect = isExiting + ? `Slide${direction}Out` as Effect + : `Slide${direction}In` as Effect; + + return ( + + ); + }); + +/** + * Flip component with in/out transition control and direction support + */ +export interface FlipProps extends AnimateProps { + /** + * Specifies the direction of the flip animation. + * Applicable values: 'LeftDown', 'LeftUp', 'RightDown', 'RightUp', 'XDown', 'XUp', 'YLeft', 'YRight' + * + * @default 'XUp' + */ + direction?: 'LeftDown' | 'LeftUp' | 'RightDown' | 'RightUp' | 'XDown' | 'XUp' | 'YLeft' | 'YRight'; +} + +/** + * Flip component that automatically handles in/out transitions with direction control. + * Uses Flip{Direction}In when `in=true` and Flip{Direction}Out when `in=false`. + * + * @example + * ```tsx + * import { Flip } from '@syncfusion/react-base'; + * + * + *
Content to flip
+ *
+ * ``` + */ +export const Flip: React.ForwardRefExoticComponent> = + forwardRef((props: FlipProps, ref: Ref): JSX.Element => { + const { + in: inProp = true, + appear = true, + direction = 'XUp', + duration = 400, + timingFunction = 'ease', + delay = 0, + onEnd, + ...rest + } = props; + const [hidden, setHidden] = useState(() => { + return !(inProp); + }); + const isExiting: boolean = useMemo(() => !inProp && !hidden, [inProp, hidden]); + const prevInProp: React.RefObject = useRef(inProp); + + useEffect(() => { + if (prevInProp.current !== inProp) { + if (inProp && hidden) { + setHidden(false); + } + prevInProp.current = inProp; + } + }, [inProp, hidden]); + + const handleAnimationEnd: (args: AnimateEvent) => void = useCallback((args: AnimateEvent) => { + if (!inProp) { + setHidden(true); + } + if (onEnd) { + onEnd(args); + } + }, [inProp, onEnd]); + + if (hidden) { + return null; + } + + const effect: Effect = isExiting + ? `Flip${direction}Out` as Effect + : `Flip${direction}In` as Effect; + + return ( + + ); + }); diff --git a/components/base/src/animation.tsx b/components/base/src/animation.tsx deleted file mode 100644 index 86ee468..0000000 --- a/components/base/src/animation.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { selectAll } from './dom'; - -/** - * Animation effect names - */ -export type Effect = 'FadeIn' | 'FadeOut' | 'FadeZoomIn' | 'FadeZoomOut' | 'FlipLeftDownIn' | 'FlipLeftDownOut' | 'FlipLeftUpIn' | -'FlipLeftUpOut' | 'FlipRightDownIn' | 'FlipRightDownOut' | 'FlipRightUpIn' | 'FlipRightUpOut' | 'FlipXDownIn' | 'FlipXDownOut' | -'FlipXUpIn' | 'FlipXUpOut' | 'FlipYLeftIn' | 'FlipYLeftOut' | 'FlipYRightIn' | 'FlipYRightOut' | 'SlideBottomIn' | 'SlideBottomOut' | -'SlideDown' | 'SlideLeft' | 'SlideLeftIn' | 'SlideLeftOut' | 'SlideRight' | 'SlideRightIn' | 'SlideRightOut' | 'SlideTopIn' | -'SlideTopOut' | 'SlideUp' | 'ZoomIn' | 'ZoomOut'; - -/** - * Interface for Animation propsRef - */ -export interface AnimationOptions { - /** - * Specify the type of animation - * - * @default FadeIn - */ - name?: Effect; - - /** - * Specify the duration to animate - * - * @default 400 - */ - duration?: number; - - /** - * Specify the animation timing function - * - * @default ease - */ - timingFunction?: string; - - /** - * Specify the delay to start animation - * - * @default 0 - */ - delay?: number; - - /** - * Triggers when animation is in-progress - * - * @event progress - */ - progress?: (args: AnimationOptions) => void; - - /** - * Triggers when the animation is started - * - * @event begin - */ - begin?: (args: AnimationOptions) => void; - - /** - * Triggers when animation is completed - * - * @event end - */ - end?: (args: AnimationOptions) => void; - - /** - * Triggers when animation is failed due to any scripts - * - * @event fail - */ - fail?: (args: AnimationOptions) => void; - /** - * Get current time-stamp in progress EventHandler - */ - timeStamp?: number; - /** - * Get current animation element in progress EventHandler - */ - element?: HTMLElement; -} - -export interface IAnimation extends AnimationOptions { - /** - * Returns module name as animation - * - * @private - * @returns {void} ? - */ - getModuleName(): string; - animate(element: HTMLElement | string, props?: AnimationOptions): void; - easing: { [key: string]: string }; -} - -export let animationMode: string | GlobalAnimationMode; - -/** - * The Animation function provide options to animate the html DOM elements - * - * @param {AnimationOptions} props - The animation options - * @returns {Animation} The animation object - */ -export function Animation(props: AnimationOptions): IAnimation { - /** - * @param {AnimationOptions} props - The animation options - * @returns {Animation} The animation object - */ - - const propsRef: IAnimation = {...props} as IAnimation; - - propsRef.easing = { - ease: 'cubic-bezier(0.250, 0.100, 0.250, 1.000)', - linear: 'cubic-bezier(0.250, 0.250, 0.750, 0.750)', - easeIn: 'cubic-bezier(0.420, 0.000, 1.000, 1.000)', - easeOut: 'cubic-bezier(0.000, 0.000, 0.580, 1.000)', - easeInOut: 'cubic-bezier(0.420, 0.000, 0.580, 1.000)', - elasticInOut: 'cubic-bezier(0.5,-0.58,0.38,1.81)', - elasticIn: 'cubic-bezier(0.17,0.67,0.59,1.81)', - elasticOut: 'cubic-bezier(0.7,-0.75,0.99,1.01)' - }; - - /** - * Applies animation to the current element. - * - * @param {string | HTMLElement} element - Element which needs to be animated. - * @param {AnimationOptions} props - Animation options. - * @returns {void} - */ - propsRef.animate = (element: HTMLElement, props?: AnimationOptions): void => { - const model: AnimationOptions = getModel(props || {}); - if (typeof element === 'string') { - const elements: HTMLElement[] = Array.from(selectAll(element, document)); - elements.forEach((ele: HTMLElement) => { - model.element = ele ; - Animation.delayAnimation(model); - }); - } else { - model.element = element; - Animation.delayAnimation(model); - } - }; - - /** - * Returns Animation Model - * - * @param {AnimationOptions} props - Animation options - * @returns {AnimationOptions} The animation model - */ - function getModel(props: AnimationOptions): AnimationOptions { - return { - name: props.name || propsRef.name || 'FadeIn', - delay: props.delay || propsRef.delay || 0, - duration: props.duration !== undefined ? props.duration : propsRef.duration || 400, - begin: props.begin || propsRef.begin, - end: props.end || propsRef.end, - fail: props.fail || propsRef.fail, - progress: props.progress || propsRef.progress, - timingFunction: props.timingFunction && propsRef.easing[props.timingFunction] ? propsRef.easing[props.timingFunction] : (props.timingFunction || propsRef.timingFunction) || 'ease' - }; - } - - /** - * Returns the module name for animation. - * - * @returns {string} The module name. - */ - propsRef.getModuleName = (): string => 'animation'; - - return propsRef; -} - -/** - * Stop the animation effect on animated element. - * - * @param {HTMLElement} element - Element which needs to be stop the animation. - * @param {AnimationOptions} model - Handling the animation model at stop function. - * @returns {void} - */ -Animation.stop = (element: HTMLElement, model?: AnimationOptions) => { - element.style.animation = ''; - element.removeAttribute('sf-animate'); - const animationId: string | null = element.getAttribute('sf-animation-id'); - if (animationId) { - const frameId: number = parseInt(animationId, 10); - cancelAnimationFrame(frameId); - element.removeAttribute('sf-animation-id'); - } - if (model && model.end) { - model.end(model); - } -}; - -/** - * Set delay to animation element - * - * @param {AnimationOptions} model ? - * @returns {void} - */ -Animation.delayAnimation = (model: AnimationOptions): void => { - if (animationMode === 'Disable' || animationMode === GlobalAnimationMode.Disable) { - if (model.begin) { - model.begin(model); - } - if (model.end) { - model.end(model); - } - } else { - if (model.delay) { - setTimeout(() => { Animation.applyAnimation(model); }, model.delay); - } else { - Animation.applyAnimation(model); - } - } -}; - -/** - * Triggers animation - * - * @param {AnimationOptions} model ? - * @returns {void} - */ -Animation.applyAnimation = (model: AnimationOptions): void => { - model.timeStamp = 0; - let step: number = 0; - let timerId: number = 0; - let prevTimeStamp: number = 0; - const duration: number | null = model.duration || null; - model.element.setAttribute('sf-animate', 'true'); - - const startAnimation: (timeStamp?: number) => void = (timeStamp?: number): void => { - try { - if (timeStamp) { - prevTimeStamp = prevTimeStamp === 0 ? timeStamp : prevTimeStamp; - model.timeStamp = (timeStamp + (model.timeStamp || 0)) - prevTimeStamp; - prevTimeStamp = timeStamp; - - if (!step && model.begin) { - model.begin(model); - } - - step = step + 1; - const avg: number = model.timeStamp / step; - - if (duration && model.timeStamp < duration && model.timeStamp + avg < duration && model.element && model.element.getAttribute('sf-animate')) { - model.element.style.animation = `${model.name} ${model.duration}ms ${model.timingFunction}`; - if (model.progress) { - model.progress(model); - } - requestAnimationFrame(startAnimation); - } else { - cancelAnimationFrame(timerId); - model.element.removeAttribute('sf-animation-id'); - model.element.removeAttribute('sf-animate'); - model.element.style.animation = ''; - if (model.end) { - model.end(model); - } - } - } else { - timerId = requestAnimationFrame(startAnimation); - model.element.setAttribute('sf-animation-id', timerId.toString()); - } - } catch (e) { - cancelAnimationFrame(timerId); - model.element.removeAttribute('sf-animation-id'); - if (model.fail) { - model.fail(e); - } - } - }; - - startAnimation(); -}; - -/** - * This method is used to enable or disable the animation for all components. - * - * @param {string|GlobalAnimationMode} value - Specifies the value to enable or disable the animation for all components. When set to 'enable', it enables the animation for all components, regardless of the individual component's animation settings. When set to 'disable', it disables the animation for all components, regardless of the individual component's animation settings. - * @returns {void} - */ -export function setGlobalAnimation(value: string | GlobalAnimationMode): void { - animationMode = value; -} - -/** - * Defines the global animation modes for all components. - */ -export enum GlobalAnimationMode { - /** - * Defines the global animation mode as Default. Animation is enabled or disabled based on the component's animation settings. - */ - Default = 'Default', - /** - * Defines the global animation mode as Enable. Enables the animation for all components, regardless of the individual component's animation settings. - */ - Enable = 'Enable', - /** - * Defines the global animation mode as Disable. Disables the animation for all components, regardless of the individual component's animation settings. - */ - Disable = 'Disable' -} diff --git a/components/base/src/base.tsx b/components/base/src/base.tsx deleted file mode 100644 index a7f77cb..0000000 --- a/components/base/src/base.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { RefObject, useLayoutEffect, useRef } from 'react'; -import { Observer, IObserver } from './observer'; -import { isNullOrUndefined, getValue } from './util'; - -const isColEName: RegExp = new RegExp(']'); - -export declare type EmitType = ((arg?: T, ...rest: unknown[]) => void); - -/** - * Main interface for public and protected properties and methods in Base. - * - * @private - */ -export interface IBase { - /** - * Associated HTML element. - * - * @private - */ - element?: RefObject<(ElementType) | null>; - - /** - * Determines if the instance is destroyed. - * - * @private - */ - isDestroyed?: boolean; - - /** - * Checks if changes are protected. - * - * @private - */ - isProtectedOnChange?: boolean; - - /** - * Observer for the component model. - * - * @private - */ - modelObserver?: IObserver; - - /** - * Indicates if the component is refreshing. - * - * @private - */ - refreshing?: boolean; - - /** - * Adds an event listener. - * - * @private - * @param eventName - The name of the event to listen for. - * @param handler - The handler function to execute when the event is triggered. - */ - addEventListener?(eventName: string, handler: Function): void; - - /** - * Removes an event listener. - * - * @private - * @param eventName - The name of the event to stop listening for. - * @param handler - The handler function to remove. - */ - removeEventListener?(eventName: string, handler: Function): void; - - /** - * Triggers event listeners for a specified event. - * - * @private - * @param eventName - The name of the event to trigger. - * @param eventProp - Properties of the event. - * @param successHandler - Function to call on successful event execution. - * @param errorHandler - Function to call on failed event execution. - * @returns {void | object} - */ - trigger?(eventName: string, eventProp?: Object, successHandler?: Function, errorHandler?: Function): void | object; - - /** - * Destroys the instance and cleans up resources. - */ - destroy?(): void; -} - -/** - * Base component function that initializes and manages component properties. - * - * @private - * @template ElementType - The type of the element reference. - * @param {IBase} [props] - The initial properties for the component. - * @param {RefObject} [element] - The reference object for the element. - * @returns {IBase} The initialized component properties. - */ -export function Base(props?: IBase, element?: RefObject): IBase { - const modelObserver: IObserver = Observer(); - - const propsRef: IBase = { - isDestroyed: false, - isProtectedOnChange: true, - modelObserver, - ...props - }; - - propsRef.element = useRef(null); - - /** - * Adds the handler to the given event listener. - * - * @param {string} eventName - A String that specifies the name of the event. - * @param {Function} handler - Specifies the function to run when the event occurs. - * @returns {void} - */ - propsRef.addEventListener = (eventName: string, handler: Function) => { - propsRef.modelObserver.on(eventName, handler); - }; - - /** - * Removes the handler from the given event listener. - * - * @param {string} eventName - A String that specifies the name of the event to remove. - * @param {Function} handler - Specifies the function to remove. - * @returns {void} - */ - propsRef.removeEventListener = (eventName: string, handler: Function): void => { - propsRef.modelObserver.off(eventName, handler); - }; - - /** - * Triggers the handlers in the specified event. - * - * @param {string} eventName - Specifies the event to trigger for the specified component properties. - * @param {Object} [eventProp] - Additional parameters to pass on to the event properties. - * @param {Function} [successHandler] - This function will invoke after the event successfully triggered. - * @param {Function} [errorHandler] - This function will invoke if the event fails to trigger. - * @returns {void | object} ? - */ - propsRef.trigger = ( - eventName: string, - eventProp?: Object, - successHandler?: Function, - errorHandler?: Function - ): void | object => { - if (propsRef.isDestroyed !== true) { - const prevDetection: boolean = propsRef.isProtectedOnChange; - propsRef.isProtectedOnChange = false; - const data: object = propsRef.modelObserver.notify(eventName, eventProp, successHandler, errorHandler) as object; - if (isColEName.test(eventName)) { - const handler: Function = getValue(eventName, propsRef); - if (handler) { - handler.call(propsRef, eventProp); - if (successHandler) { - successHandler.call(propsRef, eventProp); - } - } else if (successHandler) { - successHandler.call(propsRef, eventProp); - } - } - propsRef.isProtectedOnChange = prevDetection; - return data; - } - }; - - /** - * Destroys the instance and cleans up resources. - * - * @returns {void} - */ - propsRef.destroy = () => { - propsRef.modelObserver.destroy(); - propsRef.isDestroyed = true; - }; - - useLayoutEffect(() => { - /** - * Initialize the instance. - */ - if (element && !isNullOrUndefined(element.current)) { - propsRef.element.current = element.current; - propsRef.isProtectedOnChange = false; - } - propsRef.isDestroyed = false; - }, []); - - return propsRef; -} diff --git a/components/base/src/browser.tsx b/components/base/src/browser.tsx deleted file mode 100644 index c3cdca6..0000000 --- a/components/base/src/browser.tsx +++ /dev/null @@ -1,504 +0,0 @@ -import { isUndefined } from './util'; -const REGX_MOBILE: RegExp = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i; -const REGX_IE: RegExp = /msie|trident/i; -const REGX_IE11: RegExp = /Trident\/7\./; -const REGX_IOS: RegExp = /(ipad|iphone|ipod touch)/i; -const REGX_IOS7: RegExp = /(ipad|iphone|ipod touch);.*os 7_\d|(ipad|iphone|ipod touch);.*os 8_\d/i; -const REGX_ANDROID: RegExp = /android/i; -const REGX_WINDOWS: RegExp = /trident|windows phone|edge/i; -const REGX_VERSION: RegExp = /(version)[ /]([\w.]+)/i; -const REGX_BROWSER: { [key: string]: RegExp } = { - OPERA: /(opera|opr)(?:.*version|)[ /]([\w.]+)/i, - EDGE: /(edge)(?:.*version|)[ /]([\w.]+)/i, - CHROME: /(chrome|crios)[ /]([\w.]+)/i, - PANTHOMEJS: /(phantomjs)[ /]([\w.]+)/i, - SAFARI: /(safari)[ /]([\w.]+)/i, - WEBKIT: /(webkit)[ /]([\w.]+)/i, - MSIE: /(msie|trident) ([\w.]+)/i, - MOZILLA: /(mozilla)(?:.*? rv:([\w.]+)|)/i -}; - -interface MyWindow extends Window { - browserDetails: BrowserDetails; - cordova: Object; - PhoneGap: Object; - phonegap: Object; - forge: Object; - Capacitor?: { getPlatform: () => string }; -} -declare let window: MyWindow; - -/* istanbul ignore else */ -if (typeof window !== 'undefined') { - window.browserDetails = window.browserDetails || {}; -} - -/** - * Interface for BrowserType. - */ -interface IBrowser { - /** - * Property specifies the userAgent of the browser. Default userAgent value is based on the browser. - * Also we can set our own userAgent. - */ - userAgent: string; - - /** - * Property is to get the browser information like Name, Version and Language. - */ - info: BrowserInfo; - - /** - * Property is to get whether the userAgent is based IE. - */ - isIE: boolean; - - /** - * Property is to get whether the browser has touch support. - */ - isTouch: boolean; - - /** - * Property is to get whether the browser has Pointer support. - */ - isPointer: boolean; - - /** - * Property is to get whether the browser has MSPointer support. - */ - isMSPointer: boolean; - - /** - * Property is to get whether the userAgent is device based. - */ - isDevice: boolean; - - /** - * Property is to get whether the userAgent is Ios. - */ - isIos: boolean; - - /** - * Property is to get whether the userAgent is Ios7. - */ - isIos7: boolean; - - /** - * Property is to get whether the userAgent is Android. - */ - isAndroid: boolean; - - /** - * Property is to identify whether application ran in web view. - */ - isWebView: boolean; - - /** - * Property is to get whether the userAgent is Windows. - */ - isWindows: boolean; - - /** - * Property is to get the touch start event. It returns event name based on browser. - */ - touchStartEvent: string; - - /** - * Property is to get the touch move event. It returns event name based on browser. - */ - touchMoveEvent: string; - - /** - * Property is to get the touch end event. It returns event name based on browser. - */ - touchEndEvent: string; - - /** - * Property is to cancel the touch end event. - */ - touchCancelEvent: string; - - /** - * Method to check whether the browser on the iPad device is Safari or not. - */ - isSafari: () => boolean; - - /** - * Property specifies the userAgent of the browser. - */ - uA: string; -} - -/** - * Get configuration details for Browser - * - * @private - */ -export const Browser: IBrowser = (() => { - const uA: string = typeof navigator !== 'undefined' ? navigator.userAgent : ''; - - /** - * Extract browser detail. - * - * @returns {BrowserInfo} ? - */ - function extractBrowserDetail(): BrowserInfo { - const browserInfo: BrowserInfo = { culture: {} }; - const keys: string[] = Object.keys(REGX_BROWSER); - let clientInfo: string[] = []; - for (const key of keys) { - clientInfo = Browser.userAgent.match(REGX_BROWSER[`${key}`]); - if (clientInfo) { - browserInfo.name = (clientInfo[1].toLowerCase() === 'opr' ? 'opera' : clientInfo[1].toLowerCase()); - browserInfo.name = (clientInfo[1].toLowerCase() === 'crios' ? 'chrome' : browserInfo.name); - browserInfo.version = clientInfo[2]; - browserInfo.culture.name = browserInfo.culture.language = navigator.language; - if (Browser.userAgent.match(REGX_IE11)) { - browserInfo.name = 'msie'; - break; - } - const version: RegExpMatchArray | null = Browser.userAgent.match(REGX_VERSION); - if (browserInfo.name === 'safari' && version) { - browserInfo.version = version[2]; - } - break; - } - } - return browserInfo; - } - - /** - * Types of events that can be triggered. - * - * @typedef {('start' | 'move' | 'end' | 'cancel')} EventTypes - */ - type EventTypes = 'start' | 'move' | 'end' | 'cancel'; - - /** - * Names of the event categories based on the input device. - * - * @typedef {('isPointer' | 'isTouch' | 'isDevice')} EventNames - */ - type EventNames = 'isPointer' | 'isTouch' | 'isDevice'; - - /** - * To get events from the browser - * - * @param {EventTypes} event - type of event triggered. - * @returns {string} ? - */ - function getEvent(event: EventTypes): string { - const events: Record> = { - start: { - isPointer: 'pointerdown', - isTouch: 'touchstart', - isDevice: 'mousedown' - }, - move: { - isPointer: 'pointermove', - isTouch: 'touchmove', - isDevice: 'mousemove' - }, - end: { - isPointer: 'pointerup', - isTouch: 'touchend', - isDevice: 'mouseup' - }, - cancel: { - isPointer: 'pointercancel', - isTouch: 'touchcancel', - isDevice: 'mouseleave' - } - }; - - return Browser.isPointer - ? events[`${event}`].isPointer - : (Browser.isTouch - ? events[`${event}`].isTouch + (!Browser.isDevice ? ' ' + events[`${event}`].isDevice : '') - : events[`${event}`].isDevice); - } - - /** - * To get the Touch start event from browser - * - * @returns {string} ? - */ - function getTouchStartEvent(): string { - return getEvent('start'); - } - - /** - * To get the Touch end event from browser - * - * @returns {string} ? - */ - function getTouchEndEvent(): string { - return getEvent('end'); - } - - /** - * To get the Touch move event from browser - * - * @returns {string} ? - */ - function getTouchMoveEvent(): string { - return getEvent('move'); - } - - /** - * To cancel the touch event from browser - * - * @returns {string} ? - */ - function getTouchCancelEvent(): string { - return getEvent('cancel'); - } - - /** - * Check whether the browser on the iPad device is Safari or not - * - * @returns {boolean} ? - */ - function isSafari(): boolean { - return ( - Browser.isDevice && - Browser.isIos && - Browser.isTouch && - typeof window !== 'undefined' && - window.navigator.userAgent.toLowerCase().indexOf('iphone') === -1 && - window.navigator.userAgent.toLowerCase().indexOf('safari') > -1 - ); - } - - /** - * To get the value based on provided key and regX - * - * @param {string} key ? - * @param {RegExp} regX ? - * @returns {Object} ? - */ - function getValue(key: string, regX: RegExp): Object { - const browserDetails: {} = typeof window !== 'undefined' ? window.browserDetails : {}; - if (typeof navigator !== 'undefined' && navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 && Browser.isTouch === true && !REGX_BROWSER.CHROME.test(navigator.userAgent)) { - browserDetails['isIos'] = true; - browserDetails['isDevice'] = true; - browserDetails['isTouch'] = true; - browserDetails['isPointer'] = true; - // Set 'isPointer' for pointer-enabled devices (e.g., iPad on Safari) - browserDetails['isPointer'] = ('pointerEnabled' in window.navigator); - } - if (typeof window !== 'undefined' && window.Capacitor && window.Capacitor.getPlatform() === 'ios') { - browserDetails['isPointer'] = false; - } - if ('undefined' === typeof browserDetails[`${key}`]) { - return browserDetails[`${key}`] = regX.test(Browser.userAgent); - } - return browserDetails[`${key}`]; - } - - return { - // Properties - /** - * Property specifies the userAgent of the browser. Default userAgent value is based on the browser. - * Also we can set our own userAgent. - * - * @param {string} uA ? - */ - set userAgent(uA: string) { - Browser.uA = uA; - window.browserDetails = {}; - }, - get userAgent(): string { - return Browser.uA; - }, - - //Read Only Properties - - /** - * Property is to get the browser information like Name, Version and Language - * - * @returns {BrowserInfo} ? - */ - get info(): BrowserInfo { - if (isUndefined(window.browserDetails.info)) { - window.browserDetails.info = extractBrowserDetail(); - } - return window.browserDetails.info; - }, - - /** - * Property is to get whether the userAgent is based IE. - * - * @returns {boolean} ? - */ - get isIE(): boolean { - return getValue('isIE', REGX_IE) as boolean; - }, - - /** - * Property is to get whether the browser has touch support. - * - * @returns {boolean} ? - */ - get isTouch(): boolean { - if (isUndefined(window.browserDetails.isTouch)) { - window.browserDetails.isTouch = ('ontouchstart' in window.navigator) || (window && - window.navigator && - (window.navigator.maxTouchPoints > 0)) || ('ontouchstart' in window); - } - return window.browserDetails.isTouch; - }, - - /** - * Property is to get whether the browser has Pointer support. - * - * @returns {boolean} ? - */ - get isPointer(): boolean { - if (isUndefined(window.browserDetails.isPointer)) { - window.browserDetails.isPointer = ('pointerEnabled' in window.navigator); - } - return window.browserDetails.isPointer; - }, - /** - * Property is to get whether the browser has MSPointer support. - * - * @returns {boolean} ? - */ - get isMSPointer(): boolean { - if (isUndefined(window.browserDetails.isMSPointer)) { - window.browserDetails.isMSPointer = ('msPointerEnabled' in window.navigator); - } - return window.browserDetails.isMSPointer; - }, - /** - * Property is to get whether the userAgent is device based. - * - * @returns {boolean} ? - */ - get isDevice(): boolean { - return getValue('isDevice', REGX_MOBILE) as boolean; - }, - /** - * Property is to get whether the userAgent is Ios. - * - * @returns {boolean} ? - */ - get isIos(): boolean { - return getValue('isIos', REGX_IOS) as boolean; - }, - /** - * Property is to get whether the userAgent is Ios7. - * - * @returns {boolean} ? - */ - get isIos7(): boolean { - return getValue('isIos7', REGX_IOS7) as boolean; - }, - /** - * Property is to get whether the userAgent is Android. - * - * @returns {boolean} ? - */ - get isAndroid(): boolean { - return getValue('isAndroid', REGX_ANDROID) as boolean; - }, - /** - * Property is to identify whether application ran in web view. - * - * @returns {boolean} ? - */ - get isWebView(): boolean { - if (isUndefined(window.browserDetails.isWebView)) { - window.browserDetails.isWebView = !(isUndefined(window.cordova) && isUndefined(window.PhoneGap) - && isUndefined(window.phonegap) && window.forge !== 'object'); - } - return window.browserDetails.isWebView; - }, - /** - * Property is to get whether the userAgent is Windows. - * - * @returns {boolean} ? - */ - get isWindows(): boolean { - return getValue('isWindows', REGX_WINDOWS) as boolean; - }, - /** - * Property is to get the touch start event. It returns event name based on browser. - * - * @returns {string} ? - */ - get touchStartEvent(): string { - if (isUndefined(window.browserDetails.touchStartEvent)) { - window.browserDetails.touchStartEvent = getTouchStartEvent(); - } - return window.browserDetails.touchStartEvent; - }, - /** - * Property is to get the touch move event. It returns event name based on browser. - * - * @returns {string} ? - */ - get touchMoveEvent(): string { - if (isUndefined(window.browserDetails.touchMoveEvent)) { - window.browserDetails.touchMoveEvent = getTouchMoveEvent(); - } - return window.browserDetails.touchMoveEvent; - }, - /** - * Property is to get the touch end event. It returns event name based on browser. - * - * @returns {string} ? - */ - get touchEndEvent(): string { - if (isUndefined(window.browserDetails.touchEndEvent)) { - window.browserDetails.touchEndEvent = getTouchEndEvent(); - } - return window.browserDetails.touchEndEvent; - }, - /** - * Property is to cancel the touch end event. - * - * @returns {string} ? - */ - get touchCancelEvent(): string { - if (isUndefined(window.browserDetails.touchCancelEvent)) { - window.browserDetails.touchCancelEvent = getTouchCancelEvent(); - } - return window.browserDetails.touchCancelEvent; - }, - isSafari, - uA - }; -})(); - -/** - * Browser details for the window object. - */ -interface BrowserDetails { - isAndroid?: boolean; - isDevice?: boolean; - isIE?: boolean; - isIos?: boolean; - isIos7?: boolean; - isMSPointer?: boolean; - isPointer?: boolean; - isTouch?: boolean; - isWebView?: boolean; - isWindows?: boolean; - isSafari?: boolean; - info?: BrowserInfo; - touchStartEvent?: string; - touchMoveEvent?: string; - touchEndEvent?: string; - touchCancelEvent?: string; -} - -/** - * Information about the browser. - */ -interface BrowserInfo { - name?: string; - version?: string; - culture?: { name?: string, language?: string }; -} diff --git a/components/base/src/component.tsx b/components/base/src/component.tsx deleted file mode 100644 index c618533..0000000 --- a/components/base/src/component.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { validateLicense, createLicenseOverlay, componentList } from './validate-lic'; - -let componentCount: number = 0; -let lastPageID: number; -let lastHistoryLen: number = 0; -// Declare the static variable to count the instance -let instancecount: number = 0; -// Declare the static variable to find if control limit exceed or not -let isvalid: boolean = true; -// We have added styles to inline type so here declare the static variable to detect if banner is added or not -let isBannerAdded: boolean = false; - -//Function handling for page navigation detection -/* istanbul ignore next */ -(() => { - if (typeof window !== 'undefined') { - window.addEventListener( - 'popstate', - /* istanbul ignore next */ - () => { - componentCount = 0; - }); - } -})(); - -/** - * Checks if the browsing history has changed based on the current page ID or history length. - * - * @returns {boolean} - Returns true if history has changed, otherwise false. - */ -function isHistoryChanged(): boolean { - return lastPageID !== pageID(window.location.href) || lastHistoryLen !== window.history.length; -} - -/** - * Computes a unique page ID based on the provided URL. - * - * @param {string} url - The URL from which to generate the page ID. - * @returns {number} - The calculated page ID. - */ -function pageID(url: string): number { - let hash: number = 0; - if (url.length === 0) { return hash; } - for (let i: number = 0; i < url.length; i++) { - const char: number = url.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; // Convert to 32bit integer - } - return Math.abs(hash); -} - -/** - * Generates a unique ID for the component instance based on the current page and instance count. - * - * @private - * @param {string} [definedName] - The name to prefix the unique ID. - * @returns {string} - The generated unique ID. - */ -export function componentUniqueID(definedName?: string): string { - if (isHistoryChanged()) { - componentCount = 0; - } - lastPageID = pageID(window.location.href); - lastHistoryLen = window.history.length; - return `${definedName}_${lastPageID}_${componentCount++}`; -} - -/** - * Pre-render function to manage license validation and instance counting. - * This function ensures that a license overlay is created if necessary - * and tracks the number of instances for specific modules. - * - * @private - * @param {string} moduleName - The name of the module being rendered. - * @returns {void} - */ -export function preRender(moduleName: string): void { - if (!isvalid && !isBannerAdded) { - createLicenseOverlay(); - isBannerAdded = true; - } - // Based on the considered control list we have count the instance - if (window && document && !validateLicense()) { - if (componentList.indexOf(moduleName) !== -1) { - instancecount = instancecount + 1; - if (instancecount > 5) { - isvalid = false; - } - } - } -} diff --git a/components/base/src/dom.tsx b/components/base/src/dom.tsx deleted file mode 100644 index 4494de6..0000000 --- a/components/base/src/dom.tsx +++ /dev/null @@ -1,486 +0,0 @@ -import { EventHandler } from './event-handler'; -import { isNullOrUndefined, getValue, setValue, isObject, extend } from './util'; - -const SVG_REG: RegExp = /^svg|^path|^g/; -export interface ElementProperties { - id?: string; - className?: string; - innerHTML?: string; - styles?: string; - attrs?: { [key: string]: string }; -} - -/** - * Function to create Html element. - * - * @param {string} tagName - Name of the tag, id and class names. - * @param {ElementProperties} properties - Object to set properties in the element. - * @param {ElementProperties} properties.id - To set the id to the created element. - * @param {ElementProperties} properties.className - To add classes to the element. - * @param {ElementProperties} properties.innerHTML - To set the innerHTML to element. - * @param {ElementProperties} properties.styles - To set the some custom styles to element. - * @param {ElementProperties} properties.attrs - To set the attributes to element. - * @returns {any} ? - * @private - */ -export function createElement(tagName: string, properties?: ElementProperties): HTMLElement { - const element: HTMLElement = (SVG_REG.test(tagName) ? document.createElementNS('http://www.w3.org/2000/svg', tagName) : document.createElement(tagName)) as HTMLElement; - if (typeof (properties) === 'undefined') { - return element as HTMLElement; - } - element.innerHTML = (properties.innerHTML ? properties.innerHTML : ''); - - if (properties.className !== undefined) { - element.className = properties.className; - } - if (properties.id !== undefined) { - element.id = properties.id; - } - if (properties.styles !== undefined) { - element.setAttribute('style', properties.styles); - } - if (properties.attrs !== undefined) { - attributes(element, properties.attrs); - } - return element; -} - -/** - * The function used to add the classes to array of elements - * - * @param {Element[]|NodeList} elements - An array of elements that need to add a list of classes - * @param {string|string[]} classes - String or array of string that need to add an individual element as a class - * @returns {any} . - * @private - */ -export function addClass(elements: Element[] | NodeList, classes: string | string[]): Element[] | NodeList { - const classList: string[] = getClassList(classes); - const regExp: RegExpConstructor = RegExp; - for (const ele of (elements as Element[])) { - for (const className of classList) { - if (isObject(ele)) { - const curClass: string = getValue('attributes.className', ele); - if (isNullOrUndefined(curClass)) { - setValue('attributes.className', className, ele); - } else if (!new regExp('\\b' + className + '\\b', 'i').test(curClass)) { - setValue('attributes.className', curClass + ' ' + className, ele); - } - } else { - if (!ele.classList.contains(className)) { - ele.classList.add(className); - } - } - } - } - return elements; -} - -/** - * The function used to add the classes to array of elements - * - * @param {Element[]|NodeList} elements - An array of elements that need to remove a list of classes - * @param {string|string[]} classes - String or array of string that need to add an individual element as a class - * @returns {any} . - * @private - */ -export function removeClass(elements: Element[] | NodeList, classes: string | string[]): Element[] | NodeList { - const classList: string[] = getClassList(classes); - for (const ele of (elements as Element[])) { - const flag: boolean = isObject(ele); - const canRemove: boolean = flag ? getValue('attributes.className', ele) : ele.className !== ''; - if (canRemove) { - for (const className of classList) { - if (flag) { - const classes: string = getValue('attributes.className', ele); - const classArr: string[] = classes.split(' '); - const index: number = classArr.indexOf(className); - if (index !== -1) { - classArr.splice(index, 1); - } - setValue('attributes.className', classArr.join(' '), ele); - } else { - ele.classList.remove(className); - } - } - } - } - return elements; -} - -/** - * The function used to get classlist. - * - * @param {string | string[]} classes - An element the need to check visibility - * @returns {string[]} ? - * @private - */ -function getClassList(classes: string | string[]): string[] { - if (typeof classes === 'string') { - return [classes]; - } - return classes; -} - -/** - * The function used to check element is visible or not. - * - * @param {Element|Node} element - An element the need to check visibility - * @returns {boolean} ? - * @private - */ -export function isVisible(element: Element | Node): boolean { - const ele: HTMLElement = element as HTMLElement; - return ele.style.visibility === '' && ele.offsetWidth > 0; -} - -/** - * The function used to insert an array of elements into a first of the element. - * - * @param {Element[]|NodeList} fromElements - An array of elements that need to prepend. - * @param {Element} toElement - An element that is going to prepend. - * @param {boolean} isEval - ? - * @returns {Element[] | NodeList} ? - * @private - */ -export function prepend( - fromElements: HTMLElement[] | NodeListOf, - toElement: Element, isEval?: boolean -): Element[] | NodeList { - const docFrag: DocumentFragment = document.createDocumentFragment(); - for (const ele of (fromElements as Element[])) { - docFrag.appendChild(ele); - } - toElement.insertBefore(docFrag, toElement.firstElementChild); - if (isEval) { - executeScript(toElement); - } - return fromElements; -} - -/** - * The function used to insert an array of elements into last of the element. - * - * @param {Element[]|NodeList} fromElements - An array of elements that need to append. - * @param {Element} toElement - An element that is going to prepend. - * @param {boolean} isEval - ? - * @returns {Element[] | NodeList} ? - * @private - */ -export function append(fromElements: Element[] | NodeList, toElement: Element, isEval?: boolean): Element[] | NodeList { - const docFrag: DocumentFragment = document.createDocumentFragment(); - if (fromElements instanceof NodeList) { - while (fromElements.length > 0) { - docFrag.appendChild(fromElements[0]); - } - } else { - for (const ele of fromElements) { - docFrag.appendChild(ele); - } - } - toElement.appendChild(docFrag); - if (isEval) { - executeScript(toElement); - } - return fromElements; -} - -/** - * The function is used to evaluate script from Ajax request - * - * @param {Element} ele - An element is going to evaluate the script - * @returns {void} ? - */ -function executeScript(ele: Element): void { - if (!document) { - return; - } - const scripts: NodeListOf = ele.querySelectorAll('script'); - scripts.forEach((scriptElement: HTMLScriptElement) => { - const script: HTMLScriptElement = document.createElement('script'); - script.text = scriptElement.innerHTML; - document.head.appendChild(script).parentNode.removeChild(script); - }); -} - - -/** - * The function used to remove the element from parentnode - * - * @param {Element|Node|HTMLElement} element - An element that is going to detach from the Dom - * @returns {any} ? - * @private - */ -export function detach(element: Element | Node | HTMLElement): Element | null { - const parentNode: Node = element.parentNode as ParentNode; - if (parentNode) { - return parentNode.removeChild(element) as Element; - } - return null; -} - -/** - * The function used to remove the element from Dom also clear the bounded events - * - * @param {Element|Node|HTMLElement} element - An element remove from the Dom - * @returns {void} ? - * @private - */ -export function remove(element: Element | Node | HTMLElement): void { - const parentNode: Node = element.parentNode as ParentNode; - EventHandler.clearEvents(element as Element); - if (parentNode) { - parentNode.removeChild(element); - } -} - -/** - * The function helps to set multiple attributes to an element - * - * @param {Element|Node} element - An element that need to set attributes. - * @param {string} attributes - JSON Object that is going to as attributes. - * @returns {Element} ? - * @private - */ -export function attributes(element: Element | Node, attributes: { [key: string]: string }): Element { - const ele: Element = element as Element; - Object.keys(attributes).forEach((key: string) => { - if (isObject(ele)) { - let iKey: string = key; - if (key === 'tabindex') { - iKey = 'tabIndex'; - } - ele.attributes[`${iKey}`] = attributes[`${key}`]; - } else { - ele.setAttribute(key, attributes[`${key}`]); - } - }); - return ele; -} - -/** - * The function selects the element from giving context. - * - * @param {string} selector - Selector string need fetch element - * @param {Document|Element} context - It is an optional type, That specifies a Dom context. - * @returns {any} ? - * @private - */ -export function select(selector: string, context: Document | Element = document): Element | null { - if (!document) { - return null; - } - selector = querySelectId(selector); - return context.querySelector(selector); -} - -/** - * The function selects an array of element from the given context. - * - * @param {string} selector - Selector string need fetch element - * @param {Document|Element} context - It is an optional type, That specifies a Dom context. - * @returns {HTMLElement[]} ? - * @private - */ -export function selectAll(selector: string, context: Document | Element = document): HTMLElement[] | [] { - if (!document) { - return []; - } - selector = querySelectId(selector); - const nodeList: NodeListOf = context.querySelectorAll(selector); - return Array.from(nodeList) as HTMLElement[]; -} - -/** - * The function selects an id of element from the given context. - * - * @param {string} selector - Selector string need fetch element - * @returns {string} ? - */ -function querySelectId(selector: string): string { - const charRegex: RegExp = /(!|"|\$|%|&|'|\(|\)|\*|\/|:|;|<|=|\?|@|\]|\^|`|{|}|\||\+|~)/g; - if (selector.match(/#[0-9]/g) || selector.match(charRegex)) { - const idList: string[] = selector.split(','); - for (let i: number = 0; i < idList.length; i++) { - const list: string[] = idList[parseInt(i.toString(), 10)].split(' '); - for (let j: number = 0; j < list.length; j++) { - if (list[parseInt(j.toString(), 10)].indexOf('#') > -1) { - if (!list[parseInt(j.toString(), 10)].match(/\[.*\]/)) { - const splitId: string[] = list[parseInt(j.toString(), 10)].split('#'); - if (splitId[1].match(/^\d/) || splitId[1].match(charRegex)) { - const setId: string[] = list[parseInt(j.toString(), 10)].split('.'); - setId[0] = setId[0].replace(/#/, '[id=\'') + '\']'; - list[parseInt(j.toString(), 10)] = setId.join('.'); - } - } - } - } - idList[parseInt(i.toString(), 10)] = list.join(' '); - } - return idList.join(','); - } - return selector; -} - -/** - * Returns the closest ancestor of the current element (or the current element itself) - * that matches the specified CSS selector. - * - * @param {Element} element - An element that need to find the closest element. - * @param {string} selector - A classSelector of closest element. - * @returns {Element} ? - * @private - */ -export function closest(element: Element | Node, selector: string): Element | null { - let el: Element = element as Element; - if (el && typeof el.closest === 'function') { - return el.closest(selector); - } - - while (el && el.nodeType === 1) { - if (matches(el, selector)) { - return el; - } - - el = el.parentElement as Element; - } - - return null; -} - -/** - * Returns all sibling elements of the given element. - * - * @param {Element|Node} element - An element that need to get siblings. - * @returns {Element[]} ? - * @private - */ -export function siblings(element: Element | Node): Element[] { - const siblings: Element[] = []; - const siblingNodes: NodeListOf = (element.parentNode.childNodes || []) as NodeListOf; - - siblingNodes.forEach((curNode: Element) => { - if (curNode.nodeType === Node.ELEMENT_NODE && element !== curNode) { - siblings.push(curNode as Element); - } - }); - - return siblings; -} - -/** - * set the value if not exist. Otherwise set the existing value - * - * @param {HTMLElement} element - An element to which we need to set value. - * @param {string} property - Property need to get or set. - * @param {string} value - value need to set. - * @returns {string} ? - * @private - */ -export function getAttributeOrDefault(element: HTMLElement, property: string, value: string): string { - let attrVal: string = element.getAttribute(property); - if (isNullOrUndefined(attrVal) && value) { - element.setAttribute(property, value.toString()); - attrVal = value; - } - return attrVal; -} - -/** - * Set the style attributes to Html element. - * - * @param {HTMLElement} element - Element which we want to set attributes - * @param {any} attrs - Set the given attributes to element - * @returns {void} ? - * @private - */ -export function setStyleAttribute(element: HTMLElement, attrs: { [key: string]: Object }): void { - if (!isNullOrUndefined(attrs)) { - Object.keys(attrs).forEach((key: string) => { - element.style[`${key}`] = attrs[`${key}`]; - }); - } -} - -/** - * Method for add and remove classes to a dom element. - * - * @param {Element} element - Element for add and remove classes - * @param {string[]} addClasses - List of classes need to be add to the element - * @param {string[]} removeClasses - List of classes need to be remove from the element - * @returns {void} ? - * @private - */ -export function classList(element: Element, addClasses: string[], removeClasses: string[]): void { - addClass([element], addClasses); - removeClass([element], removeClasses); -} - -/** - * Method to check whether the element matches the given selector. - * - * @param {Element} element - Element to compare with the selector. - * @param {string} selector - String selector which element will satisfy. - * @returns {void} ? - * @private - */ -export function matches(element: Element, selector: string): boolean { - if (!document) { - return false; - } - const matchesFn: Function = element.matches - || (element as { msMatchesSelector?: (selector: string) => boolean }).msMatchesSelector - || element.webkitMatchesSelector; - if (matchesFn) { - return matchesFn.call(element, selector); - } else { - return [].indexOf.call(document.querySelectorAll(selector), element) !== -1; - } -} - -/** - * Method to get the html text from DOM. - * - * @param {HTMLElement} ele - Element to compare with the selector. - * @param {string} innerHTML - String selector which element will satisfy. - * @returns {void} ? - * @private - */ -export function includeInnerHTML(ele: HTMLElement, innerHTML: string): void { - ele.innerHTML = innerHTML; -} - -/** - * Method to get the containsclass. - * - * @param {HTMLElement} ele - Element to compare with the selector. - * @param {string} className - String selector which element will satisfy. - * @returns {boolean} ? - * @private - */ -export function containsClass(ele: HTMLElement, className: string): boolean { - if (isObject(ele)) { - const regExp: RegExpConstructor = RegExp; - return new regExp('\\b' + className + '\\b', 'i').test(ele.attributes.getNamedItem('class')?.value || ''); - } else { - return ele.classList.contains(className); - } -} - -/** - * Method to check whether the element matches the given selector. - * - * @param {Object} element - Element to compare with the selector. - * @param {boolean} deep ? - * @returns {any} ? - * @private - */ -// eslint-disable-next-line -export function cloneNode(element: Object, deep?: boolean): any { - if (isObject(element)) { - if (deep) { - return extend({}, {}, element, true); - } - } else { - return (element as HTMLElement).cloneNode(deep); - } -} diff --git a/components/base/src/drag-util.tsx b/components/base/src/drag-util.tsx deleted file mode 100644 index b27faa4..0000000 --- a/components/base/src/drag-util.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import { select } from './dom'; -import { IPosition } from './draggable'; -import { isNullOrUndefined } from './util'; - -/** - * Position coordinates - */ -export interface PositionCoordinates { - left?: number; - top?: number; - bottom?: number; - right?: number; -} - -/** - * Coordinates for element position - * - * @private - */ -export interface Coordinates { - /** - * Defines the x Coordinate of page. - */ - pageX?: number; - /** - * Defines the y Coordinate of page. - */ - pageY?: number; - /** - * Defines the x Coordinate of client. - */ - clientX?: number; - /** - * Defines the y Coordinate of client. - */ - clientY?: number; -} - -let left: number; -let top: number; -let width: number; -let height: number; - -/** - * Sets the permitted drag area boundaries based on the defined dragArea. - * - * @private - * @param {HTMLElement | string} dragArea - The element or selector string defining the drag area - * @param {Element} helperElement - The helper element used in dragging - * @param {PositionCoordinates} borderWidth - The border width of the drag area - * @param {PositionCoordinates} padding - The padding of the drag area - * @param {PositionCoordinates} dragLimit - The object to store the calculated drag limits - * @returns {void} - */ -export function setDragArea( - dragArea: HTMLElement | string, helperElement: Element, - borderWidth: PositionCoordinates, padding: PositionCoordinates, - dragLimit: PositionCoordinates -): void { - let eleWidthBound: number; - let eleHeightBound: number; - let top: number = 0; - let left: number = 0; - let ele: HTMLElement; - const type: string = typeof dragArea; - if (type === 'string') { - ele = select(dragArea as string) as HTMLElement; - } else { - ele = dragArea as HTMLElement; - } - if (ele) { - const elementArea: ClientRect | DOMRect = ele.getBoundingClientRect(); - eleWidthBound = ele.scrollWidth ? ele.scrollWidth : elementArea.right - elementArea.left; - eleHeightBound = ele.scrollHeight ? (dragArea && !isNullOrUndefined(helperElement) && helperElement.classList.contains('sf-treeview')) ? ele.clientHeight : ele.scrollHeight : elementArea.bottom - elementArea.top; - const keys: string[] = ['Top', 'Left', 'Bottom', 'Right']; - const styles: CSSStyleDeclaration = getComputedStyle(ele); - for (let i: number = 0; i < keys.length; i++) { - const key: string = keys[parseInt(i.toString(), 10)]; - const tborder: string = styles['border' + key + 'Width']; - const tpadding: string = styles['padding' + key]; - const lowerKey: string = key.toLowerCase(); - (borderWidth as Record)[`${lowerKey}`] = isNaN(parseFloat(tborder)) ? 0 : parseFloat(tborder); - (padding as Record)[`${lowerKey}`] = isNaN(parseFloat(tpadding)) ? 0 : parseFloat(tpadding); - } - if (dragArea && !isNullOrUndefined(helperElement) && helperElement.classList.contains('sf-treeview')) { - top = elementArea.top + document.scrollingElement.scrollTop; - } else { - top = elementArea.top; - } - left = elementArea.left; - dragLimit.left = left + borderWidth.left + padding.left; - dragLimit.top = ele.offsetTop + borderWidth.top + padding.top; - dragLimit.right = left + eleWidthBound - (borderWidth.right + padding.right); - dragLimit.bottom = top + eleHeightBound - (borderWidth.bottom + padding.bottom); - } -} - -/** - * Retrieves the document's full height or width, considering the scroll and offset values. - * - * @private - * @param {string} str - The dimension type ('Height' or 'Width') to calculate. - * @returns {number} - The maximum value across scroll, offset, and client dimensions. - */ -export function getDocumentWidthHeight(str: 'Height' | 'Width'): number { - const docBody: HTMLElement = document.body; - const docEle: HTMLElement = document.documentElement; - return Math.max( - docBody['scroll' + str], docEle['scroll' + str], - docBody['offset' + str], docEle['offset' + str], - docEle['client' + str] - ); -} - -/** - * Determines if a given element is within the bounds of the viewport. - * - * @private - * @param {HTMLElement} el - The element to check. - * @returns {boolean} - True if the element is in the viewport, false otherwise. - */ -export function elementInViewport(el: HTMLElement): boolean { - top = el.offsetTop; - left = el.offsetLeft; - width = el.offsetWidth; - height = el.offsetHeight; - while (el.offsetParent) { - el = el.offsetParent as HTMLElement; - top += el.offsetTop; - left += el.offsetLeft; - } - return ( - top >= window.pageYOffset && - left >= window.pageXOffset && - (top + height) <= (window.pageYOffset + window.innerHeight) && - (left + width) <= (window.pageXOffset + window.innerWidth) - ); -} - -/** - * Gets the coordinates of a mouse or touch event. - * - * @private - * @param {MouseEvent | TouchEvent} evt - The event object. - * @returns {Coordinates} - The x and y coordinates of the page and client. - */ -export function getCoordinates(evt: MouseEvent & TouchEvent): Coordinates { - if (evt.type.indexOf('touch') > -1) { - return evt.changedTouches[0]; - } - return evt as Coordinates; -} - -/** - * Calculates the parent position of the element relative to the document. - * - * @private - * @param {Element} ele - The element for which the parent position is calculated. - * @returns {IPosition} - The calculated left and top position. - */ -export function calculateParentPosition(ele: Element): IPosition { - if (isNullOrUndefined(ele)) { - return { left: 0, top: 0 }; - } - const rect: ClientRect | DOMRect = ele.getBoundingClientRect(); - const style: CSSStyleDeclaration = getComputedStyle(ele); - return { - left: (rect.left + window.pageXOffset) - parseInt(style.marginLeft, 10), - top: (rect.top + window.pageYOffset) - parseInt(style.marginTop, 10) - }; -} - -/** - * Retrieves all elements from a point defined by event coordinates. - * - * @private - * @param {MouseEvent | TouchEvent} evt - The event object containing coordinates. - * @returns {Element[]} - An array of elements located at the event's point. - */ -export function getPathElements(evt: MouseEvent & TouchEvent): Element[] { - const elementTop: number = evt.clientX > 0 ? evt.clientX : 0; - const elementLeft: number = evt.clientY > 0 ? evt.clientY : 0; - return document.elementsFromPoint(elementTop, elementLeft); -} - -/** - * Identifies the scrollable parent of the current node element. - * - * @private - * @param {Element[]} nodes - The path of elements to check. - * @param {boolean} reverse - Whether to reverse the array to check from bottom to top. - * @returns {Element | null} - The first scrollable parent element or null. - */ -export function getScrollParent(nodes: Element[], reverse: boolean): Element | null { - const nodeList: Element[] = reverse ? [...nodes].reverse() : nodes; - for (const node of nodeList) { - const computedStyle: CSSStyleDeclaration = window.getComputedStyle(node); - const overflowY: string = computedStyle.overflowY; - if ((overflowY === 'auto' || overflowY === 'scroll') && - node.scrollHeight > node.clientHeight) { - return node; - } - } - const scrollingElement: HTMLElement = document.scrollingElement as HTMLElement; - const docOverflowY: string = window.getComputedStyle(scrollingElement).overflowY; - if (docOverflowY === 'visible') { - scrollingElement.style.overflow = 'auto'; - return scrollingElement; - } - return null; -} diff --git a/components/base/src/dragdrop.tsx b/components/base/src/dragdrop.tsx deleted file mode 100644 index 7e06ac5..0000000 --- a/components/base/src/dragdrop.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { createContext, useContext, useState, ReactNode, RefObject, useRef } from 'react'; -import { IDroppable } from './droppable'; - -/** - * Interface defining the methods available in the DragDropContext. - * - * @private - */ -export interface DragDropContextProps { - /** - * Registers a droppable instance with a unique identifier. - * - * @param {string} id - The unique identifier for the droppable instance - * @param {DragDropContext} instance - The droppable instance to register - * @returns {void} - */ - registerDroppable: (id: string, instance: DroppableContext) => void; - - /** - * Unregisters a droppable instance by its identifier. - * - * @param {string} id - The unique identifier of the droppable instance to unregister - * @returns {void} - */ - unregisterDroppable: (id: string) => void; - - /** - * Retrieves all registered droppable instances. - * - * @returns {Record} A record of all droppable instances indexed by their identifiers - */ - getAllDroppables: () => Record; -} - -/** - * Interface defining the Droppable instance reference with element. - * - * @private - */ -export interface DroppableContext extends IDroppable { - element?: RefObject; -} - -const DragDropContext: React.Context = createContext(undefined); - -/** - * Custom hook that provides access to the droppable context functionality. - * - * @private - * @returns {DragDropContextProps} The droppable context methods and state - */ -export const useDragDropContext: () => DragDropContextProps | undefined = (): DragDropContextProps | undefined => { - const context: DragDropContextProps | undefined = useContext(DragDropContext); - if (!context) { - return undefined; - } - return context; -}; - -/** - * Props for the DragDrop component. - */ -interface DragDropProps { - /** - * The child components that will have access to the droppable context. - */ - children: ReactNode; -} - -/** - * Provider component that manages droppable instances throughout the application, provides registration and retrieval methods for droppable elements. - * - * @param {DragDropProps} props - The component props - * @param {ReactNode} props.children - The child elements to render within the provider - * @returns {Element} The rendered DragDrop provider component - */ -export const DragDrop: React.FC = ({ children }: { children: ReactNode }) => { - const [droppables, setDroppables] = useState>({}); - const currentDroppables: RefObject> = useRef>({}); - - /** - * Registers a droppable instance with a unique identifier. - * - * @param {string} id - The unique identifier for the droppable instance - * @param {DragDropContext} instance - The droppable instance to register - * @returns {void} - */ - const registerDroppable: (id: string, instance: DroppableContext) => void = (id: string, instance: DroppableContext): void => { - setDroppables((prev: Record) => { - const updated: {[x: string]: DroppableContext} = { - ...prev, - [id]: instance - }; - currentDroppables.current = updated; - return updated; - }); - }; - - /** - * Unregisters a droppable instance by its identifier. - * - * @param {string} id - The unique identifier of the droppable instance to unregister - * @returns {void} - */ - const unregisterDroppable: (id: string) => void = (id: string): void => { - setDroppables((prev: Record) => { - const newDroppables: Record = { ...prev }; - delete newDroppables[`${id}`]; - return newDroppables; - }); - }; - - /** - * Retrieves all registered droppable instances. - * - * @returns {Record} A record of all droppable instances indexed by their identifiers - */ - const getAllDroppables: () => Record = (): Record => { - return currentDroppables.current || droppables; - }; - - return ( - - {children} - - ); -}; diff --git a/components/base/src/draggable.tsx b/components/base/src/draggable.tsx deleted file mode 100644 index 5d5fb9a..0000000 --- a/components/base/src/draggable.tsx +++ /dev/null @@ -1,1212 +0,0 @@ -import { RefObject, useLayoutEffect } from 'react'; -import { extend, isUndefined, isNullOrUndefined, compareElementParent } from './util'; -import { closest, setStyleAttribute, createElement, addClass, isVisible, select } from './dom'; -import { Browser } from './browser'; -import { EventHandler } from './event-handler'; -import { setDragArea, PositionCoordinates, elementInViewport, getDocumentWidthHeight, Coordinates, getCoordinates, calculateParentPosition, getPathElements, getScrollParent } from './drag-util'; -import { DragDropContextProps, DroppableContext, useDragDropContext } from './dragdrop'; - -/** - * The default position coordinates used for initializing or resetting positions. - */ -const defaultPosition: PositionCoordinates = { left: 0, top: 0, bottom: 0, right: 0 }; -/** - * Drag state object to check if dragging has started. - */ -const isDraggedObject: DragObject = { isDragged: false }; - -/** - * Specifies the Direction in which drag movement happen. - */ -export type DragDirection = 'x' | 'y'; - -/** - * Drag object interface - */ -interface DragObject { - isDragged?: boolean; -} - -/** - * Specifies the position coordinates. - */ -export interface IPosition { - /** - * Specifies the left position of cursor in draggable. - */ - left?: number; - - /** - * Specifies the top position of cursor in draggable. - */ - top?: number; -} - -/** - * Hook to manage Position. - * - * @private - * @param {Partial} props - Initial values for the position properties. - * @returns {IPosition} - The initialized position properties. - */ -export function Position(props?: IPosition): IPosition { - const propsRef: IPosition = { - left: 0, - top: 0, - ...props - }; - return propsRef; -} -/** - * Page information - */ -interface PageInfo { - x?: number; - y?: number; -} - -/** - * Interface to specify the drag data in the droppable. - */ -export interface DropInfo { - /** - * Specifies the current draggable element - */ - draggable?: HTMLElement; - /** - * Specifies the current helper element. - */ - helper?: HTMLElement; - /** - * Specifies the drag target element - */ - draggedElement?: HTMLElement; -} - -export interface DropObject { - target?: HTMLElement; - instance?: DropOption; -} - -/** - * Used to access values - * - * @private - */ -export interface DragPosition { - left?: string; - top?: string; -} - -/** - * Droppable function to be invoked from draggable - * - * @private - */ -export interface DropOption { - /** - * Used to triggers over function while draggable element is over the droppable element. - */ - intOver?: Function; - /** - * Used to triggers out function while draggable element is out of the droppable element. - */ - intOut?: Function; - /** - * Used to triggers out function while draggable element is dropped on the droppable element. - */ - intDrop?: Function; - /** - * Specifies the information about the drag element. - */ - dragData?: DropInfo; - /** - * Specifies the status of the drag of drag stop calling. - */ - dragStopCalled?: boolean; -} - -/** - * Drag Event arguments - */ -export interface DragEventArgs { - /** - * Specifies the actual event. - */ - event?: MouseEvent & TouchEvent; - /** - * Specifies the current drag element. - */ - element?: HTMLElement; - /** - * Specifies the current target element. - */ - target?: HTMLElement; - /** - * 'true' if the drag or drop action is to be prevented; otherwise false. - */ - cancel?: boolean; -} - -/** - * Draggable interface for public and protected properties and methods. - */ -export interface IDraggable { - /** - * Defines the distance between the cursor and the draggable element. - */ - cursorAt?: IPosition; - /** - * If `clone` set to true, drag operations are performed in duplicate element of the draggable element. - * - * @default true - */ - clone?: boolean; - /** - * Defines the parent element in which draggable element movement will be restricted. - */ - dragArea?: HTMLElement | string; - /** - * Defines the dragArea is scrollable or not. - */ - isDragScroll?: boolean; - /** - * Defines whether to replace drag element by currentStateTarget. - * - * @private - */ - isReplaceDragEle?: boolean; - /** - * Defines whether to add prevent select class to body or not. - * - * @private - */ - isPreventSelect?: boolean; - /** - * Specifies the callback function for drag event. - * - * @event drag - */ - drag?: Function; - /** - * Specifies the callback function for dragStart event. - * - * @event dragStart - */ - dragStart?: Function; - /** - * Specifies the callback function for dragStop event. - * - * @event dragStop - */ - dragStop?: Function; - /** - * Defines the minimum distance draggable element to be moved to trigger the drag operation. - * - * @default 1 - */ - distance?: number; - /** - * Defines the child element selector which will act as drag handle. - */ - handle?: string; - /** - * Defines the child element selector which will prevent dragging of element. - */ - abort?: string | string[]; - /** - * Defines the callback function for customizing the cloned element. - */ - helper?: Function; - /** - * Defines the scope value to group sets of draggable and droppable items. - * A draggable with the same scope value will be accepted by the droppable. - * - * @default 'default' - */ - scope?: string; - /** - * Specifies the dragTarget by which the clone element is positioned if not given current context element will be considered. - * - * @private - */ - dragTarget?: string; - /** - * Defines the axis to limit the draggable element drag path. The possible axis path values are - * * `x` - Allows drag movement in horizontal direction only. - * * `y` - Allows drag movement in vertical direction only. - */ - axis?: DragDirection; - /** - * Defines the function to change the position value. - * - * @private - */ - queryPositionInfo?: Function; - /** - * Defines whether the drag clone element will be split form the cursor pointer. - * - * @private - */ - enableTailMode?: boolean; - /** - * Defines whether to skip the previous drag movement comparison. - * - * @private - */ - skipDistanceCheck?: boolean; - /** - * - * @private - */ - preventDefault?: boolean; - /** - * Defines whether to enable autoscroll on drag movement of draggable element. - * enableAutoScroll - * - * @private - */ - enableAutoScroll?: boolean; - /** - * Gets the element of the draggable. - */ - element?: RefObject; - /** - * Defines whether to enable taphold on mobile devices. - * enableAutoScroll - * - * @private - */ - enableTapHold?: boolean; - /** - * Specifies the time delay for tap hold. - * - * @default 750 - * @private - */ - tapHoldThreshold?: number; - /** - * - * @private - */ - enableScrollHandler?: boolean; - /** - * Destroys the draggable instance by removing event listeners and cleaning up resources. - * - * @private - */ - intDestroy?(): void; - /** - * Method to clean up and remove event handlers on the component destruction. - * - * @private - */ - destroy?(): void; -} - -/** - * Draggable function provides support to enable draggable functionality in Dom Elements. - * - * @param {RefObject} element - The reference to the HTML element to be made draggable - * @param {IDraggable} [props] - Optional properties to configure the draggable behavior - * @returns {IDraggable} A Draggable object with draggable functionality - */ -export function useDraggable(element: RefObject, props?: IDraggable): IDraggable { - const droppableContext: DragDropContextProps = useDragDropContext(); - const propsRef: IDraggable = { - cursorAt: Position({}), - clone: true, - dragArea: null, - isDragScroll: false, - isReplaceDragEle: false, - isPreventSelect: true, - distance: 1, - handle: '', - abort: '', - helper: null, - scope: 'default', - dragTarget: '', - axis: null, - queryPositionInfo: null, - enableTailMode: false, - skipDistanceCheck: false, - preventDefault: true, - enableAutoScroll: false, - enableTapHold: false, - tapHoldThreshold: 750, - enableScrollHandler: false, - element: element, - ...props - }; - - /* Global Variables */ - let target: HTMLElement; - let initialPosition: PageInfo; - let relativeXPosition: number; - let relativeYPosition: number; - let margin: PositionCoordinates; - let offset: PositionCoordinates; - let position: PositionCoordinates; - let dragLimit: PositionCoordinates = useDraggable.getDefaultPosition(); - let borderWidth: PositionCoordinates = useDraggable.getDefaultPosition(); - const padding: PositionCoordinates = useDraggable.getDefaultPosition(); - let pageX: number; - let diffX: number = 0; - let prevLeft: number = 0; - let prevTop: number = 0; - let dragProcessStarted: boolean = false; - let tapHoldTimer: ReturnType | null = null; - let dragElePosition: { top: number, left: number }; - let currentStateTarget: HTMLElement; - let externalInitialize: boolean = false; - let diffY: number = 0; - let pageY: number; - let helperElement: HTMLElement; - let hoverObject: DropObject; - let parentClientRect: IPosition; - let parentScrollX: number = 0; - let parentScrollY: number = 0; - let initialScrollX: number = 0; - let initialScrollY: number = 0; - const droppables: { [key: string]: DropInfo } = {}; - - /** - * Toggles event listeners for the draggable element. - * - * @param {boolean} [isUnWire] - Flag to determine if events should be removed. - * @returns {void} - */ - function toggleEvents(isUnWire?: boolean): void { - let ele: Element; - if (!isNullOrUndefined(propsRef.handle) && propsRef.handle !== '') { - ele = select(propsRef.handle, element.current); - } - const handler: Function = (propsRef.enableTapHold && Browser.isDevice && Browser.isTouch) ? mobileInitialize : initialize; - if (isUnWire) { - EventHandler.remove(ele || element.current, Browser.isSafari() ? 'touchstart' : Browser.touchStartEvent, handler); - } else { - EventHandler.add(ele || element.current, Browser.isSafari() ? 'touchstart' : Browser.touchStartEvent, handler); - } - } - - /** - * Initializes drag events for mobile devices with tap hold support. - * - * @param {MouseEvent | TouchEvent} evt - The initial event that triggered the drag. - * @returns {void} - */ - function mobileInitialize(evt: MouseEvent & TouchEvent): void { - const target: EventTarget = evt.currentTarget; - tapHoldTimer = setTimeout( - () => { - externalInitialize = true; - removeTapholdTimer(); - initialize(evt, target); - }, - propsRef.tapHoldThreshold - ); - EventHandler.add(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, removeTapholdTimer, this); - EventHandler.add(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, removeTapholdTimer, this); - } - - /** - * Binds drag-related events to the drag target element. - * - * @param {HTMLElement} dragTargetElement - The element that will act as the drag target. - * @returns {void} - */ - function bindDragEvents(dragTargetElement: HTMLElement): void { - if (isVisible(dragTargetElement)) { - EventHandler.add(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDrag, this); - EventHandler.add(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, intDragStop, this); - setGlobalDroppables(false, element.current, dragTargetElement); - } else { - toggleEvents(); - document.body.classList.remove('sf-prevent-select'); - } - } - - /** - * Removes the tap hold timer and detaches related event listeners. - * - * @returns {void} - */ - function removeTapholdTimer(): void { - clearTimeout(tapHoldTimer); - EventHandler.remove(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, removeTapholdTimer); - EventHandler.remove(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, removeTapholdTimer); - } - - /** - * Retrieves the scrollable parent of a given element along a specified axis. - * - * @param {HTMLElement} element - The element whose scrollable parent is to be found. - * @param {string} axis - The axis ('vertical' or 'horizontal') to check for scrollability. - * @returns {HTMLElement | null} - The scrollable parent element, or null if none found. - */ - // eslint-disable-next-line - const getScrollableParent: Function = (element: HTMLElement, axis: string): HTMLElement | null => { - const scroll: { [key: string]: string } = { 'vertical': 'scrollHeight', 'horizontal': 'scrollWidth' }; - const client: { [key: string]: string } = { 'vertical': 'clientHeight', 'horizontal': 'clientWidth' }; - if (isNullOrUndefined(element)) { - return null; - } - if ((element as unknown as { [key: string]: number })[scroll[`${axis}`]] > - ((element as unknown as { [key: string]: number })[client[`${axis}`]]) - ) { - if (axis === 'vertical' ? element.scrollTop > 0 : element.scrollLeft > 0) { - if (axis === 'vertical') { - parentScrollY += (parentScrollY === 0 ? element.scrollTop : element.scrollTop - parentScrollY); - } else { - parentScrollX += (parentScrollX === 0 ? element.scrollLeft : element.scrollLeft - parentScrollX); - } - if (!isNullOrUndefined(element)) { - return getScrollableParent(element.parentNode as HTMLElement, axis); - } else { - return element; - } - } else { - return getScrollableParent(element.parentNode as HTMLElement, axis); - } - } else { - return getScrollableParent(element.parentNode as HTMLElement, axis); - } - }; - - /** - * Calculates and stores scrollable values for the draggable element. - * - * @returns {void} - */ - function getScrollableValues(): void { - parentScrollX = 0; - parentScrollY = 0; - } - - /** - * Initializes the drag operation. - * - * @param {MouseEvent | TouchEvent} evt - The event that initiated the drag action. - * @param {EventTarget} [curTarget] - The current target element of the event. - * @returns {void} - */ - function initialize(evt: MouseEvent & TouchEvent, curTarget?: EventTarget): void { - element.current = getActualElement(element) as HTMLElement; - currentStateTarget = evt.target as HTMLElement; - if (isDragStarted()) { - return; - } else { - isDragStarted(true); - externalInitialize = false; - } - target = evt.currentTarget as HTMLElement || curTarget as HTMLElement; - dragProcessStarted = false; - if (propsRef.abort) { - let abortSelectors: string | string[] = propsRef.abort; - if (typeof abortSelectors === 'string') { - abortSelectors = [abortSelectors]; - } - for (let i: number = 0; i < abortSelectors.length; i++) { - if (!isNullOrUndefined(closest((evt.target as Element), abortSelectors[`${i}`]))) { - if (isDragStarted()) { - isDragStarted(true); - } - return; - } - } - } - if (propsRef.preventDefault && !isUndefined(evt.changedTouches) && evt.type !== 'touchstart') { - evt.preventDefault(); - } - element.current.setAttribute('aria-grabbed', 'true'); - const intCoord: Coordinates = getCoordinates(evt); - initialPosition = { x: intCoord.pageX, y: intCoord.pageY }; - if (!propsRef.clone) { - const pos: IPosition = element.current.getBoundingClientRect(); - getScrollableValues(); - relativeXPosition = intCoord.pageX - (pos.left + parentScrollX); - relativeYPosition = intCoord.pageY - (pos.top + parentScrollY); - } - - if (externalInitialize) { - intDragStart(evt); - } else { - EventHandler.add(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDragStart, this); - EventHandler.add(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDestroy, this); - } - toggleEvents(true); - if (evt.type !== 'touchstart' && propsRef.isPreventSelect) { - document.body.classList.add('sf-prevent-select'); - } - externalInitialize = false; - EventHandler.trigger(document.documentElement, Browser.isSafari() ? 'touchstart' : Browser.touchStartEvent, evt); - } - - /** - * Initiates the drag start operation. - * - * @param {MouseEvent | TouchEvent} evt - The event that initiates the drag start. - * @returns {void} - */ - function intDragStart(evt: MouseEvent & TouchEvent): void { - removeTapholdTimer(); - if (document.scrollingElement) { - initialScrollX = document.scrollingElement.scrollLeft; - initialScrollY = document.scrollingElement.scrollTop; - } - const isChangeTouch: boolean = !isUndefined(evt.changedTouches); - if (isChangeTouch && (evt.changedTouches.length !== 1)) { - return; - } - const intCordinate: Coordinates = getCoordinates(evt); - let pos: IPosition; - const styleProp: CSSStyleDeclaration = getComputedStyle(element.current); - margin = { - left: parseInt(styleProp.marginLeft, 10), - top: parseInt(styleProp.marginTop, 10), - right: parseInt(styleProp.marginRight, 10), - bottom: parseInt(styleProp.marginBottom, 10) - }; - let dragElement: HTMLElement = element.current; - if (propsRef.clone && propsRef.dragTarget) { - const intClosest: HTMLElement = closest(evt.target as Element, propsRef.dragTarget) as HTMLElement; - if (!isNullOrUndefined(intClosest)) { - dragElement = intClosest; - } - } - if (propsRef.isReplaceDragEle) { - dragElement = currentStateCheck(evt.target as HTMLElement, dragElement); - } - offset = calculateParentPosition(dragElement); - position = getMousePosition(evt, propsRef.isDragScroll); - const x: number = initialPosition.x - intCordinate.pageX; - const y: number = initialPosition.y - intCordinate.pageY; - const distance: number = Math.sqrt((x * x) + (y * y)); - if ((distance >= propsRef.distance || externalInitialize)) { - const ele: HTMLElement = getHelperElement(evt); - if (!ele) { - return; - } - if (isChangeTouch) { - evt.preventDefault(); - } - const dragTargetElement: HTMLElement = helperElement = ele; - parentClientRect = calculateParentPosition(dragTargetElement.offsetParent); - if (propsRef.dragStart) { - const curTarget: HTMLElement = getProperTargetElement(evt); - const args: object = { - event: evt, - element: dragElement, - target: curTarget, - bindEvents: null, - dragElement: dragTargetElement - }; - propsRef.dragStart(args as DragEventArgs); - if ((args as DragEventArgs).cancel) { - return undefined; - } - } - if (propsRef.dragArea) { - setDragArea(propsRef.dragArea, helperElement, borderWidth, padding, dragLimit); - } else { - dragLimit = { left: 0, right: 0, bottom: 0, top: 0 }; - borderWidth = { top: 0, left: 0 }; - } - pos = { left: position.left - parentClientRect.left, top: position.top - parentClientRect.top }; - if (propsRef.clone && !propsRef.enableTailMode) { - diffX = position.left - offset.left; - diffY = position.top - offset.top; - } - - getScrollableValues(); - const styles: CSSStyleDeclaration = getComputedStyle(dragElement); - const marginTop: number = parseFloat(styles.marginTop); - if (propsRef.clone && marginTop !== 0) { - pos.top += marginTop; - } - if (propsRef.enableScrollHandler && !propsRef.clone) { - pos.top -= parentScrollY; - pos.left -= parentScrollX; - } - const posValue: DragPosition = getProcessedPositionValue({ - top: `${pos.top - diffY}px`, - left: `${pos.left - diffX}px` - }); - if (propsRef.dragArea && typeof propsRef.dragArea !== 'string' && propsRef.dragArea.classList.contains('sf-kanban-content') && propsRef.dragArea.style.position === 'relative') { - pos.top += propsRef.dragArea.scrollTop; - } - dragElePosition = { top: pos.top, left: pos.left }; - setStyleAttribute(dragTargetElement, getDragPosition({ position: 'absolute', left: posValue.left, top: posValue.top })); - EventHandler.remove(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDragStart); - EventHandler.remove(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDestroy); - bindDragEvents(dragTargetElement); - } - } - - /** - * Initializes the variables and manages the drag operation progress. - * - * @param {MouseEvent |TouchEvent} evt - The event that triggers the drag. - * @returns {void} - */ - function intDrag(evt: MouseEvent & TouchEvent): void { - if (!isUndefined(evt.changedTouches) && (evt.changedTouches.length !== 1)) { - return; - } - if (propsRef.clone && evt.changedTouches && Browser.isDevice && Browser.isTouch) { - evt.preventDefault(); - } - let left: number; - let top: number; - position = getMousePosition(evt, propsRef.isDragScroll); - const docHeight: number = getDocumentWidthHeight('Height'); - if (docHeight < position.top) { - position.top = docHeight; - } - const docWidth: number = getDocumentWidthHeight('Width'); - if (docWidth < position.left) { - position.left = docWidth; - } - if (propsRef.drag) { - const curTarget: HTMLElement = getProperTargetElement(evt); - propsRef.drag({ event: evt, element: element.current, target: curTarget } as DragEventArgs); - } - const eleObj: DropObject = checkTargetElement(evt); - if (eleObj.target && eleObj.instance) { - let flag: boolean = true; - if (hoverObject) { - if (hoverObject.instance !== eleObj.instance) { - triggerOutFunction(evt, eleObj); - } else { - flag = false; - } - } - if (flag) { - eleObj.instance.dragData[propsRef.scope] = droppables[propsRef.scope]; - eleObj.instance.intOver(evt, eleObj.target); - hoverObject = eleObj; - } - } else if (hoverObject) { - triggerOutFunction(evt, eleObj); - } - const helperElement: HTMLElement = droppables[propsRef.scope].helper; - parentClientRect = calculateParentPosition(helperElement.offsetParent); - const tLeft: number = parentClientRect.left; - const tTop: number = parentClientRect.top; - const intCoord: Coordinates = getCoordinates(evt); - const pagex: number = intCoord.pageX; - const pagey: number = intCoord.pageY; - const dLeft: number = position.left - diffX; - const dTop: number = position.top - diffY; - const styles: CSSStyleDeclaration = getComputedStyle(helperElement); - if (propsRef.dragArea) { - if (propsRef.enableAutoScroll) { - setDragArea(propsRef.dragArea, helperElement, borderWidth, padding, dragLimit); - } - if (pageX !== pagex || propsRef.skipDistanceCheck) { - const helperWidth: number = helperElement.offsetWidth + (parseFloat(styles.marginLeft) + parseFloat(styles.marginRight)); - if (dragLimit.left > dLeft && dLeft > 0) { - left = dragLimit.left; - } else if (dragLimit.right + window.pageXOffset < dLeft + helperWidth && dLeft > 0) { - left = dLeft - (dLeft - dragLimit.right) + window.pageXOffset - helperWidth; - } else { - left = dLeft < 0 ? dragLimit.left : dLeft; - } - } - if (pageY !== pagey || propsRef.skipDistanceCheck) { - const helperHeight: number = helperElement.offsetHeight + (parseFloat(styles.marginTop) + parseFloat(styles.marginBottom)); - if (dragLimit.top > dTop && dTop > 0) { - top = dragLimit.top; - } else if (dragLimit.bottom + window.pageYOffset < dTop + helperHeight && dTop > 0) { - top = dTop - (dTop - dragLimit.bottom) + window.pageYOffset - helperHeight; - } else { - top = dTop < 0 ? dragLimit.top : dTop; - } - } - } else { - left = dLeft; - top = dTop; - } - const iTop: number = tTop + borderWidth.top; - const iLeft: number = tLeft + borderWidth.left; - if (dragProcessStarted) { - if (isNullOrUndefined(top)) { - top = prevTop; - } - if (isNullOrUndefined(left)) { - left = prevLeft; - } - } - let draEleTop: number; - let draEleLeft: number; - if (helperElement.classList.contains('sf-treeview')) { - if (propsRef.dragArea) { - dragLimit.top = propsRef.clone ? dragLimit.top : 0; - draEleTop = (top - iTop) < 0 ? dragLimit.top : (top - borderWidth.top); - draEleLeft = (left - iLeft) < 0 ? dragLimit.left : (left - borderWidth.left); - } else { - draEleTop = top - borderWidth.top; - draEleLeft = left - borderWidth.left; - } - } else { - if (propsRef.dragArea) { - const isDialogEle: boolean = helperElement.classList.contains('sf-dialog'); - dragLimit.top = propsRef.clone ? dragLimit.top : 0; - draEleTop = (top - iTop) < 0 ? dragLimit.top : (top - iTop); - draEleLeft = (left - iLeft) < 0 ? isDialogEle ? (left - (iLeft - borderWidth.left)) : dragElePosition.left : (left - iLeft); - } else { - draEleTop = top - iTop; - draEleLeft = left - iLeft; - } - } - const marginTop: number = parseFloat(getComputedStyle(element.current).marginTop); - if (marginTop > 0) { - if (propsRef.clone) { - draEleTop += marginTop; - if (dTop < 0) { - if ((marginTop + dTop) >= 0) { - draEleTop = marginTop + dTop; - } else { - draEleTop -= marginTop; - } - } - if (propsRef.dragArea) { - draEleTop = (dragLimit.bottom < draEleTop) ? dragLimit.bottom : draEleTop; - } - } - if ((top - iTop) < 0) { - if (dTop + marginTop + (helperElement.offsetHeight - iTop) >= 0) { - const tempDraEleTop: number = dragLimit.top + dTop - iTop; - if ((tempDraEleTop + marginTop + iTop) < 0) { - draEleTop -= marginTop + iTop; - } else { - draEleTop = tempDraEleTop; - } - } else { - draEleTop -= marginTop + iTop; - } - } - } - - if (propsRef.dragArea && helperElement.classList.contains('sf-treeview')) { - const helperHeight: number = helperElement.offsetHeight + (parseFloat(styles.marginTop) + parseFloat(styles.marginBottom)); - draEleTop = (draEleTop + helperHeight) > dragLimit.bottom ? (dragLimit.bottom - helperHeight) : draEleTop; - } - - if (propsRef.enableScrollHandler && !propsRef.clone) { - draEleTop -= parentScrollY; - draEleLeft -= parentScrollX; - } - if (propsRef.dragArea && typeof propsRef.dragArea !== 'string' && propsRef.dragArea.classList.contains('sf-kanban-content') && propsRef.dragArea.style.position === 'relative') { - draEleTop += propsRef.dragArea.scrollTop; - } - const dragValue: DragPosition = getProcessedPositionValue({ top: draEleTop + 'px', left: draEleLeft + 'px' }); - setStyleAttribute(helperElement, getDragPosition(dragValue)); - if (!elementInViewport(helperElement) && propsRef.enableAutoScroll && !helperElement.classList.contains('sf-treeview')) { - helperElement.scrollIntoView(); - } - - let elements: NodeList | Element[] = document.querySelectorAll(':hover'); - if (propsRef.enableAutoScroll && helperElement.classList.contains('sf-treeview')) { - if (elements.length === 0) { - elements = getPathElements(evt); - } - let scrollParent: Element | null = getScrollParent(elements as Element[], false); - if (elementInViewport(helperElement)) { - getScrollPosition(scrollParent as HTMLElement, draEleTop); - } else if (!elementInViewport(helperElement)) { - elements = [].slice.call(document.querySelectorAll(':hover')); - if (elements.length === 0) { - elements = getPathElements(evt); - } - scrollParent = getScrollParent(elements as Element[], true); - getScrollPosition(scrollParent as HTMLElement, draEleTop); - } - } - - dragProcessStarted = true; - prevLeft = left; - prevTop = top; - position.left = left; - position.top = top; - pageX = pagex; - pageY = pagey; - } - - /** - * Stops the drag operation and performs cleanup. - * - * @param {MouseEvent | TouchEvent} evt - The event that initiated the drag stop. - * @returns {void} - */ - function intDragStop(evt: MouseEvent & TouchEvent): void { - dragProcessStarted = false; - initialScrollX = 0; - initialScrollY = 0; - if (!isUndefined(evt.changedTouches) && (evt.changedTouches.length !== 1)) { - return; - } - const type: string[] = ['touchend', 'pointerup', 'mouseup']; - if (type.indexOf(evt.type) !== -1) { - if (propsRef.dragStop) { - const curTarget: HTMLElement = getProperTargetElement(evt); - propsRef.dragStop({ event: evt, element: element.current, target: curTarget, helper: helperElement } as DragEventArgs); - } - propsRef.intDestroy(); - } else { - element.current.setAttribute('aria-grabbed', 'false'); - } - const eleObj: DropObject = checkTargetElement(evt); - if (eleObj.target && eleObj.instance) { - eleObj.instance.dragStopCalled = true; - eleObj.instance.dragData[propsRef.scope] = droppables[propsRef.scope]; - eleObj.instance.intDrop(evt, eleObj.target); - } - setGlobalDroppables(true); - document.body.classList.remove('sf-prevent-select'); - } - - /** - * Method to bind events. - * - * @returns {void} - */ - function bind(): void { - toggleEvents(); - if (Browser.isIE) { - addClass([propsRef.element.current], 'sf-block-touch'); - } - droppables[propsRef.scope] = {}; - } - /** - * Destroys the draggable instance by removing event listeners and cleaning up resources. - * - * @returns {void} - */ - propsRef.intDestroy = (): void => { - dragProcessStarted = false; - toggleEvents(); - document.body.classList.remove('sf-prevent-select'); - element.current.setAttribute('aria-grabbed', 'false'); - EventHandler.remove(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDragStart); - EventHandler.remove(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, intDragStop); - EventHandler.remove(document, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDestroy); - EventHandler.remove(document, Browser.isSafari() ? 'touchmove' : Browser.touchMoveEvent, intDrag); - if (isDragStarted()) { - isDragStarted(true); - } - }; - - /** - * Method to clean up and remove event handlers on the component destruction. - * - * @returns {void} - */ - propsRef.destroy = (): void => { - toggleEvents(true); - }; - - /** - * Triggers the out function for the previous hover target when a new draggable - * target is detected or when the pointer is out of the current drop zone. - * - * @param {MouseEvent | TouchEvent} evt - The event object. - * @param {DropObject} eleObj - The drop object containing target and instance. - * @returns {void} - */ - function triggerOutFunction(evt: MouseEvent & TouchEvent, eleObj: DropObject): void { - hoverObject.instance.intOut(evt, eleObj.target); - hoverObject.instance.dragData[propsRef.scope] = null; - hoverObject = null; - } - - /** - * Checks and retrieves the correct target element under the pointer during a drag operation. - * - * @param {MouseEvent | TouchEvent} evt - The event object. - * @returns {HTMLElement} - The correct target element. - */ - function getProperTargetElement(evt: MouseEvent & TouchEvent): HTMLElement { - const intCoord: Coordinates = getCoordinates(evt); - let ele: HTMLElement; - const prevStyle: string = helperElement.style.pointerEvents || ''; - const isPointer: boolean = evt.type.indexOf('pointer') !== -1 && Browser.info.name === 'safari' && parseInt(Browser.info.version, 10) > 12; - if (compareElementParent(evt.target as Element, helperElement) || evt.type.indexOf('touch') !== -1 || isPointer) { - helperElement.style.pointerEvents = 'none'; - ele = document.elementFromPoint(intCoord.clientX, intCoord.clientY) as HTMLElement; - helperElement.style.pointerEvents = prevStyle; - } else { - ele = evt.target as HTMLElement; - } - return ele; - } - - /** - * Retrieves the position of the mouse or touch event relative to the document or parent element. - * - * @param {MouseEvent | TouchEvent} evt - The drag event. - * @param {boolean} [isdragscroll] - Indicates if the dragging is performed with scrolling. - * @returns {IPosition} - The left and top coordinates of the drag event. - */ - function getMousePosition(evt: MouseEvent & TouchEvent, isdragscroll?: boolean): IPosition { - const dragEle: EventTarget | null = evt.srcElement !== undefined ? evt.srcElement : evt.target; - const intCoord: Coordinates = getCoordinates(evt); - let pageX: number; - let pageY: number; - const isOffsetParent: boolean = isNullOrUndefined((dragEle as HTMLElement).offsetParent); - if (isdragscroll) { - pageX = propsRef.clone ? intCoord.pageX - : (intCoord.pageX + (isOffsetParent ? 0 : (dragEle as HTMLElement).offsetParent.scrollLeft)) - relativeXPosition; - pageY = propsRef.clone ? intCoord.pageY - : (intCoord.pageY + (isOffsetParent ? 0 : (dragEle as HTMLElement).offsetParent.scrollTop)) - relativeYPosition; - if (!propsRef.clone) { - const offsetParent: HTMLElement = (dragEle as HTMLElement).offsetParent as HTMLElement; - if (!isOffsetParent && offsetParent) { - const currentScrollLeft: number = offsetParent.scrollLeft; - const currentScrollTop: number = offsetParent.scrollTop; - const scrollDeltaX: number = currentScrollLeft - initialScrollX; - const scrollDeltaY: number = currentScrollTop - initialScrollY; - pageX = pageX - scrollDeltaX; - pageY = pageY - scrollDeltaY; - } - } - } else { - pageX = propsRef.clone ? intCoord.pageX : (intCoord.pageX + window.pageXOffset) - relativeXPosition; - pageY = propsRef.clone ? intCoord.pageY : (intCoord.pageY + window.pageYOffset) - relativeYPosition; - if (document.scrollingElement && (!propsRef.clone)) { - const ele: Element = document.scrollingElement; - const currentScrollX: number = ele.scrollLeft; - const currentScrollY: number = ele.scrollTop; - const scrollDeltaX: number = currentScrollX - initialScrollX; - const scrollDeltaY: number = currentScrollY - initialScrollY; - pageX = pageX - scrollDeltaX; - pageY = pageY - scrollDeltaY; - } - } - return { - left: pageX - (margin.left + propsRef.cursorAt.left), - top: pageY - (margin.top + propsRef.cursorAt.top) - }; - } - - - /** - * Retrieves or creates the helper element for the drag operation. - * - * @param {MouseEvent | TouchEvent} evt - The event triggering the drag. - * @returns {HTMLElement} - The helper element used during dragging. - */ - function getHelperElement(evt: MouseEvent & TouchEvent): HTMLElement { - let element: HTMLElement; - if (propsRef.clone) { - if (propsRef.helper) { - element = propsRef.helper({ sender: evt, element: target }); - } else { - element = createElement('div', { className: 'sf-drag-helper sf-block-touch', innerHTML: 'Draggable' }); - document.body.appendChild(element); - } - } else { - element = propsRef.element.current; - } - return element; - } - - /** - * Sets the global drop object for the current scope, managing the relationship - * between draggable and droppable elements. - * - * @param {boolean} reset - Whether to reset or set the droppable object. - * @param {HTMLElement} [drag] - The current draggable element. - * @param {HTMLElement} [helper] - The helper element used during dragging. - * @returns {void} - */ - function setGlobalDroppables(reset: boolean, drag?: HTMLElement, helper?: HTMLElement): void { - droppables[propsRef.scope] = reset ? null : { - draggable: drag, - helper: helper, - draggedElement: propsRef.element.current - }; - } - - /** - * Checks and retrieves the drop target and its associated droppable instance. - * - * @param {MouseEvent | TouchEvent} evt - The event object. - * @returns {DropObject} - Contains the drop target and the droppable instance. - */ - function checkTargetElement(evt: MouseEvent & TouchEvent): DropObject { - const dropTarget: HTMLElement = getProperTargetElement(evt); - let dropInstance: DropOption = getDropInstance(dropTarget); - if (!dropInstance && dropTarget && !isNullOrUndefined(dropTarget.parentNode)) { - const parent: Element = closest(dropTarget.parentNode, '.sf-droppable') || dropTarget.parentElement; - if (parent) { - dropInstance = getDropInstance(parent); - } - } - return { target: dropTarget as HTMLElement, instance: dropInstance }; - } - - /** - * Retrieves the drop instance associated with a DOM element. - * - * @param {Element} ele - The DOM element to find the drop instance for - * @returns {DropOption} The drop instance if found, otherwise undefined - */ - function getDropInstance(ele: Element): DropOption | undefined { - let droppables: Record; - let dropInstance: DropOption; - if (droppableContext) { - const { getAllDroppables } = droppableContext; - droppables = getAllDroppables(); - for (const id in droppables) { - if (Object.prototype.hasOwnProperty.call(droppables, id)) { - const instance: DroppableContext = droppables[`${id}`]; - if (instance.element && instance.element.current === ele) { - dropInstance = instance; - break; - } - } - } - } - else { - return undefined; - } - return dropInstance; - } - - - /** - * Checks if the dragging has started and toggles the isDragged state. - * - * @param {boolean} [change] - Optional flag to change the drag state. - * @returns {boolean} - The current drag state. - */ - function isDragStarted(change?: boolean): boolean { - if (change) { - isDraggedObject.isDragged = !isDraggedObject.isDragged; - } - return isDraggedObject.isDragged; - } - - - /** - * Processes the position values of a draggable element. If a custom - * queryPositionInfo function is provided, it will use that to process - * the position. Otherwise, it returns the original value. - * - * @param {DragPosition} value - The position values (left and top) to be processed. - * @returns {DragPosition} - The processed or original position values. - */ - function getProcessedPositionValue(value: DragPosition): DragPosition { - if (propsRef.queryPositionInfo) { - return propsRef.queryPositionInfo(value); - } - return value; - } - - /** - * Computes the drag position of an element based on specified constraints or axis limitations. - * - * @param {DragPosition | { position: string }} dragValue - The raw drag position values. - * @returns {Record} - Adjusted drag position values with applied constraints. - */ - function getDragPosition(dragValue: DragPosition & { position?: string }): Record { - const temp: Record = { ...dragValue }; - if (propsRef.axis) { - if (propsRef.axis === 'x') { - delete temp.top; - } else if (propsRef.axis === 'y') { - delete temp.left; - } - } - return temp; - } - - /** - * Adjusts the scroll position of a parent element to ensure the draggable element - * remains visible during scrolling. - * - * @param {Element} nodeEle - The element intended to be scrolled. - * @param {number} draEleTop - The top position of the draggable element. - * @returns {void} - */ - function getScrollPosition(nodeEle: HTMLElement, draEleTop: number): void { - if (nodeEle === document.scrollingElement) { - if ((nodeEle.clientHeight + nodeEle.scrollTop - helperElement.clientHeight) < draEleTop - && nodeEle.getBoundingClientRect().height + parentClientRect.top > draEleTop) { - nodeEle.scrollTop += helperElement.clientHeight; - } else if (nodeEle.scrollTop > draEleTop - helperElement.clientHeight) { - nodeEle.scrollTop -= helperElement.clientHeight; - } - } else if (nodeEle) { - const docScrollTop: number = document.scrollingElement.scrollTop; - const helperClientHeight: number = helperElement.clientHeight; - if ((nodeEle.clientHeight + nodeEle.getBoundingClientRect().top - helperClientHeight + docScrollTop) < draEleTop) { - nodeEle.scrollTop += helperElement.clientHeight; - } else if (nodeEle.getBoundingClientRect().top > (draEleTop - helperClientHeight - docScrollTop)) { - nodeEle.scrollTop -= helperElement.clientHeight; - } - } - } - - /** - * Checks and returns the appropriate current state element. - * Determines whether to use the current state's target or revert to a specified element. - * - * @param {HTMLElement} ele - The current element. - * @param {HTMLElement} [oldEle] - The previous element, if any. - * @returns {HTMLElement} - The element considered to be in the current state. - */ - function currentStateCheck(ele: HTMLElement, oldEle?: HTMLElement): HTMLElement { - let elem: HTMLElement; - if (!isNullOrUndefined(currentStateTarget) && currentStateTarget !== ele) { - elem = currentStateTarget; - } else { - elem = !isNullOrUndefined(oldEle) ? oldEle : ele; - } - return elem; - } - - /** - * Retrieves the underlying HTML element from a possibly forwarded ref or custom element. - * - * @param {RefObject} elementRef - The ref object containing the element. - * @returns {HTMLElement} The actual HTML element - */ - function getActualElement( - elementRef: React.RefObject - ): HTMLElement | { element?: HTMLElement | undefined; } { - if (elementRef.current) { - if (!(elementRef.current instanceof HTMLElement) && - elementRef.current.element && - elementRef.current.element instanceof HTMLElement) { - return elementRef.current.element; - } - } - return elementRef.current; - } - - - useLayoutEffect(() => { - if (!propsRef.element.current) { - return undefined; - } - propsRef.element.current = getActualElement(propsRef.element) as HTMLElement; - addClass([propsRef.element.current], ['sf-lib', 'sf-draggable']); - bind(); - return () => { - propsRef.destroy(); - }; - }); - - return propsRef; -} - -/** - * Retrieves the default position coordinates. - * - * @returns {PositionCoordinates} - The default position coordinates with left, top, bottom, and right set to 0. - */ -useDraggable.getDefaultPosition = (): PositionCoordinates => { - return extend({}, defaultPosition); -}; diff --git a/components/base/src/droppable.tsx b/components/base/src/droppable.tsx deleted file mode 100644 index 5e542e1..0000000 --- a/components/base/src/droppable.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { RefObject, useId, useLayoutEffect } from 'react'; -import { Browser } from './browser'; -import { addClass, isVisible, matches } from './dom'; -import { compareElementParent } from './util'; -import { EventHandler } from './event-handler'; -import { DropInfo } from './draggable'; -import { Coordinates } from './drag-util'; -import { DragDropContextProps, useDragDropContext } from './dragdrop'; - -/** - * Droppable arguments in drop callback. - * - * @private - */ -export interface DropData { - /** - * Specifies that current element can be dropped. - */ - canDrop?: boolean; - /** - * Specifies target to drop. - */ - target?: HTMLElement; -} - -export interface DropEvents { - dropTarget?: HTMLElement; -} - -/** - * Interface for drop event args - */ -export interface DropEventArgs { - /** - * Specifies the original mouse or touch event arguments. - */ - event?: MouseEvent & TouchEvent; - /** - * Specifies the target element. - */ - target?: HTMLElement; - /** - * Specifies the dropped element. - */ - droppedElement?: HTMLElement; - /** - * Specifies the dragData. - */ - dragData?: DropInfo; -} - -/** - * Main interface for public properties in Droppable. - */ -export interface IDroppableProps { - /** - * Defines the selector for draggable element to be accepted by the droppable. - */ - accept?: string; - /** - * Defines the scope value to group sets of draggable and droppable items. - * A draggable with the same scope value will only be accepted by the droppable. - */ - scope?: string; - /** - * Specifies the callback function, which will be triggered while drag element is dropped in droppable. - * - * @event drop - */ - drop?: (args: DropEventArgs) => void; - /** - * Specifies the callback function, which will be triggered while drag element is moved over droppable element. - * - * @event over - */ - over?: Function; - /** - * Specifies the callback function, which will be triggered while drag element is moved out of droppable element. - * - * @event out - */ - out?: Function; -} - -/** - * Main interface for protected methods in Droppable. - */ -export interface IDroppable extends IDroppableProps { - /** - * Data associated with the current drag operation. - * - * @private - */ - dragData?: { [key: string]: DropInfo }; - /** - * Method for handling interactions when dragged item is over the droppable area. - * - * @private - * @param event - Mouse or touch event arguments. - * @param element - The target element over which the drag is happening. - */ - intOver?: (event: MouseEvent & TouchEvent, element?: Element) => void; - /** - * Method for handling interactions when dragged item is out of the droppable area. - * - * @private - * @param event - Mouse or touch event arguments. - * @param element - The target element from which the drag is moving out. - */ - intOut?: (event: MouseEvent & TouchEvent, element?: Element) => void; - /** - * Method to clean up and remove event handlers on the component destruction. - * - * @private - */ - intDrop?: (event: MouseEvent & TouchEvent, element?: Element) => void; -} - -/** - * Creates a droppable instance with the specified element and properties. - * - * @private - * @param {RefObject} [element] - Reference to the HTML element to make droppable. - * @param {IDroppable} [props] - Configuration properties for the droppable instance. - * @returns {IDroppable} The configured droppable instance. - */ -export function useDroppable(element?: RefObject, props?: IDroppable): IDroppable { - const droppableId: string = useId(); - const droppableContext: DragDropContextProps = useDragDropContext(); - const { registerDroppable, unregisterDroppable } = droppableContext || {}; - const propsRef: IDroppable = { - accept: '', - scope: 'default', - dragData: {}, - drop: null, - over: null, - out: null, - ...props - }; - /** Represents whether the mouse is over the droppable area */ - let mouseOverRef: boolean = false; - /** Indicates if drag stop has been called */ - let dragStopCalledRef: boolean = true; - - /** - * Method to add drop events. - * - * @returns {void} - */ - function addEvent(): void { - EventHandler.add(element.current, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDrop); - } - - /** - * Handles interactions when a dragged item is over the droppable area. - * - * @param {MouseEvent | TouchEvent} event - Mouse or touch event arguments. - * @param {Element} [element] - The target element over which the drag is happening. - * @returns {void} - */ - propsRef.intOver = (event: MouseEvent & TouchEvent, element?: Element): void => { - if (!mouseOverRef) { - const drag: DropInfo = propsRef.dragData[propsRef.scope]; - if (propsRef.over) { - propsRef.over({ event, target: element, dragData: drag }); - } - mouseOverRef = true; - } - }; - - /** - * Method for handling interactions when dragged item is out of the droppable area. - * - * @param {MouseEvent | TouchEvent} event - Mouse or touch event arguments. - * @param {Element} [element] - The target element from which the drag is moving out. - * @returns {void} - */ - propsRef.intOut = (event: MouseEvent & TouchEvent, element?: Element): void => { - if (mouseOverRef) { - if (propsRef.out) { - propsRef.out({ event, target: element }); - } - mouseOverRef = false; - } - }; - - /** - * Method to handle drop event. - * - * @param {MouseEvent | TouchEvent} evt - Mouse or touch event arguments. - * @param {HTMLElement} [element] - The target element where the drop is happening. - * @returns {void} - */ - propsRef.intDrop = (evt: MouseEvent & TouchEvent, element?: HTMLElement): void => { - if (!dragStopCalledRef) { - return; - } else { - dragStopCalledRef = false; - } - let accept: boolean = true; - const drag: DropInfo = propsRef.dragData[propsRef.scope]; - const isDrag: boolean = drag ? (drag.helper && isVisible(drag.helper)) : false; - let area: DropData; - if (isDrag) { - area = isDropArea(evt, drag.helper, element); - if (propsRef.accept) { - accept = matches(drag.helper, propsRef.accept); - } - } - if (isDrag && propsRef.drop && area.canDrop && accept) { - propsRef.drop({ event: evt, target: area.target, droppedElement: drag.helper, dragData: drag }); - } - mouseOverRef = false; - }; - - /** - * Method to check if the drop area is valid. - * - * @param {MouseEvent | TouchEvent} evt - Mouse or touch event arguments. - * @param {HTMLElement} helper - The helper element involved in the drag operation. - * @param {HTMLElement} [element] - The element to check for drop validity. - * @returns {DropData} - The result indicating if the area is a valid drop target and the target itself. - */ - function isDropArea(evt: MouseEvent & TouchEvent, helper: HTMLElement, element?: HTMLElement): DropData { - const area: DropData = { canDrop: true, target: element || (evt.target as HTMLElement) }; - const isTouch: boolean = evt.type === 'touchend'; - if (isTouch || area.target === helper) { - helper.style.display = 'none'; - const coord: Coordinates = isTouch ? (evt.changedTouches[0]) : evt; - const ele: Element = document.elementFromPoint(coord.clientX, coord.clientY); - area.canDrop = false; - area.canDrop = compareElementParent(ele, element); - if (area.canDrop) { - area.target = ele as HTMLElement; - } - helper.style.display = ''; - } - return area; - } - - /** - * Method to clean up and remove event handlers on the component destruction. - * - * @returns {void} - */ - function removeEvent(): void { - EventHandler.remove(element.current, Browser.isSafari() ? 'touchend' : Browser.touchEndEvent, propsRef.intDrop); - } - - useLayoutEffect(() => { - addClass([element.current], ['sf-lib', 'sf-droppable']); - addEvent(); - if (registerDroppable) { - registerDroppable(droppableId, { - ...propsRef, - element: element - }); - } - return () => { - if (unregisterDroppable) { - unregisterDroppable(droppableId); - } - removeEvent(); - }; - }, []); - - return propsRef; -} diff --git a/components/base/src/enums.tsx b/components/base/src/enums.tsx new file mode 100644 index 0000000..134fb46 --- /dev/null +++ b/components/base/src/enums.tsx @@ -0,0 +1,162 @@ +/** + * Defines a color categories for the components. Each color category is used to visually differentiate components based on context. + */ +export enum Color { + /** + * Indicates the color for success, often used to convey the completion of an action. + */ + Success = 'Success', + /** + * Indicates the color for informational content, assisting in the communication of status or updates. + */ + Info = 'Info', + /** + * Indicates the color for warnings or cautions, drawing attention to potential issues. + */ + Warning = 'Warning', + /** + * Indicates the color for error or danger, signaling urgent problem requiring attention. + */ + Error = 'Error', + /** + * Indicates the primary color for highlighting the main role or action. + */ + Primary = 'Primary', + /** + * Indicates secondary colors for support actions with a subtle tone. + */ + Secondary = 'Secondary' +} + +/** + * Specifies positioning options for the components. Determines how the element should be displayed relative to a reference context. + */ +export enum Position { + /** + * Represents the positioning left of the reference context. + */ + Left = 'Left', + /** + * Represents the positioning right of the reference context. + */ + Right = 'Right', + /** + * Represents the positioning above the reference context. + */ + Top = 'Top', + /** + * Represents the positioning below the reference context. + */ + Bottom = 'Bottom' +} + +/** + * Defines visual variants for UI components. Each variant modifies the appearance to convey distinct user preferences. + */ +export enum Variant { + /** + * Indicates a solid background to emphasize content with contrasting text. + */ + Filled = 'Filled', + /** + * Indicates a boundary outline to highlight content with colored text, excluding a background. + */ + Outlined = 'Outlined', + /** + * Indicates a minimalist style with colored text and a subtle or absent background. + */ + Standard = 'Standard' +} + +/** + * Defines size levels for UI components. + */ +export enum Size { + /** + * Represents a larger size for the component, used for greater emphasis. + */ + Large = 'Large', + /** + * Represents a medium size for the component, offering a balanced appearance. + */ + Medium = 'Medium', + /** + * Represents a smaller size for the component, suitable for less prominent display. + */ + Small = 'Small' +} + +/** + * Defines layout orientation for components. Each orientation specifies the directional arrangement for layout and component behavior. + */ +export enum Orientation { + /** + * Arranges elements horizontally from left to right. + */ + Horizontal = 'Horizontal', + /** + * Arranges elements vertically from top to bottom. + */ + Vertical = 'Vertical' +} + +/** + * Defines severity levels for components. Each severity level conveys appropriate importance and urgency to users. + */ +export enum Severity { + /** + * Indicates an information that is a critical issue requiring immediate user attention. + */ + Error = 'Error', + /** + * Indicates an information that is general or non-urgent in nature. + */ + Info = 'Info', + /** + * Indicates an information that reflects a neutral state with no specific urgency. + */ + Normal = 'Normal', + /** + * Indicates an information that confirms a successful operation. + */ + Success = 'Success', + /** + * Indicates an information that highlights a potential issue or caution. + */ + Warning = 'Warning' +} + +/** + * Specifies the available directions for resize handles when a component is resizable. + * Each value represents a different edge or corner of the component that can be dragged to resize it. + */ +export type ResizeDirections = 'South' | 'North' | 'East' | 'West' | 'NorthEast' | 'NorthWest' | 'SouthEast' | 'SouthWest' | 'All'; + +/** + * Specifies the horizontal alignment options for component positioning. + */ +export type HorizontalAlignment = 'Left' | 'Center' | 'Right'; + +/** + * Specifies the vertical alignment options for component positioning. + */ +export type VerticalAlignment = 'Top' | 'Center' | 'Bottom'; + +/** + * Defines the label position of the component. + * ```props + * After :- When the label is positioned After, it appears to the right of the component. + * Before :- When the label is positioned Before, it appears to the left of the component. + * ``` + */ +export type LabelPlacement = 'After' | 'Before' | 'Bottom'; + +/** + * Specifies the behavior modes for a floating or persistent label in a form field. + * + * - `'Never'`: The label never floats; it remains in its original place regardless of input focus or content. + * - `'Always'`: The label is always displayed in a floating position, even when the input is empty or unfocused. + * - `'Auto'`: The label floats above the input only when the field is focused or contains a value, and returns to its default position otherwise. + * + */ +export type LabelMode= 'Never' | 'Always' | 'Auto'; diff --git a/components/base/src/event-handler.tsx b/components/base/src/event-handler.tsx deleted file mode 100644 index ca48c99..0000000 --- a/components/base/src/event-handler.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { debounce, extend } from './util'; -import { Browser } from './browser'; - -/** - * Interface for EventHandler. - */ -interface IEventHandler { - add: ( - element: Element | HTMLElement | Document, - eventName: string, - listener: Function, - bindTo?: Object, - intDebounce?: number - ) => Function | null; - clearEvents: (element: Element) => void; - remove: ( - element: Element | HTMLElement | Document, - eventName: string, - listener: Function - ) => void; - trigger: (element: HTMLElement, eventName: string, eventProp?: Object) => void; -} - -/** - * Custom hook to handle events on HTML elements. - */ -export const EventHandler: IEventHandler = (() => { - /** - * Adds or retrieves event data from an element. - * - * @param {Element | HTMLElement | Document} element - The target element to retrieve or add event data. - * @returns {EventOptions[]} - The list of event options associated with the element. - */ - function addOrGetEventData(element: Element | HTMLElement | Document): EventOptions[] { - if (!element) { - return null; - } - if ('__eventList' in element) { - return (element as EventData).__eventList.events || []; - } else { - (element as EventData).__eventList = {}; - return (element as EventData).__eventList.events = []; - } - } - - /** - * Adds an event listener to the specified DOM element. - * - * @param {Element | HTMLElement | Document} element - Target HTML DOM element. - * @param {string} eventName - A string that specifies the name of the event. - * @param {Function} listener - Specifies the function to run when the event occurs. - * @param {Object} [bindTo] - An object that binds 'this' variable in the event handler. - * @param {number} [intDebounce] - Specifies at what interval the given event listener should be triggered. - * @returns {Function} - The final event listener function with optional debounce and binding applied. - */ - function add( - element: Element | HTMLElement | Document, - eventName: string, - listener: Function, - bindTo?: Object, - intDebounce?: number - ): Function { - if (!element) { - return null; - } - const eventData: EventOptions[] = addOrGetEventData(element); - let debounceListener: Function = intDebounce ? debounce(listener, intDebounce) : listener; - if (bindTo) { debounceListener = debounceListener.bind(bindTo); } - const event: string[] = eventName.split(' '); - for (let i: number = 0; i < event.length; i++) { - eventData.push({ - name: event[parseInt(i.toString(), 10)], - listener: listener, - debounce: debounceListener - }); - const options: {passive: boolean} = Browser.isIE ? null : { passive: false }; - element.addEventListener(event[parseInt(i.toString(), 10)], debounceListener as EventListener, options); - } - return debounceListener; - } - - /** - * Removes an event listener from the specified DOM element. - * - * @param {Element | HTMLElement | Document} element - Specifies the target HTML element to remove the event. - * @param {string} eventName - A string that specifies the name of the event to remove. - * @param {Function} listener - Specifies the function to remove. - * @returns {void} - */ - function remove( - element: Element | HTMLElement | Document, - eventName: string, - listener: Function - ): void { - if (!element) { - return null; - } - const eventData: EventOptions[] = addOrGetEventData(element); - const event: string[] = eventName.split(' '); - for (let j: number = 0; j < event.length; j++) { - let index: number = -1; - let debounceListener: Function | null = null; - if (eventData && eventData.length !== 0) { - eventData.some((x: EventOptions, i: number) => { - if (x.name === event[parseInt(j.toString(), 10)] && x.listener === listener) { - index = i; - debounceListener = x.debounce || null; - return true; - } - return false; - }); - } - if (index !== -1) { - eventData.splice(index, 1); - } - if (debounceListener) { - element.removeEventListener(event[parseInt(j.toString(), 10)], debounceListener as EventListener); - } - } - } - - /** - * Clears all the event listeners that have been previously attached to the element. - * - * @param {Element} element - Specifies the target HTML element to clear the events. - * @returns {void} - */ - function clearEvents(element: Element): void { - if (!element) { - return null; - } - const eventData: EventOptions[] = addOrGetEventData(element); - const copyData: EventOptions[] = extend([], undefined, eventData) as EventOptions[]; - for (let i: number = 0; i < copyData.length; i++) { - const parseValue: EventOptions = copyData[parseInt(i.toString(), 10)]; - element.removeEventListener(parseValue.name, parseValue.debounce as EventListener); - eventData.shift(); - } - } - - /** - * Triggers a specific event on the given HTML element. - * - * @param {HTMLElement} element - Specifies the target HTML element to trigger the event. - * @param {string} eventName - Specifies the event to trigger for the specified element. - * @param {Object} [eventProp] - Additional parameters to pass on to the event properties. - * @returns {void} - */ - function trigger(element: HTMLElement, eventName: string, eventProp?: Object): void { - if (!element) { - return null; - } - const eventData: EventOptions[] = addOrGetEventData(element); - for (const event of eventData) { - if (event.name === eventName) { - event.debounce(eventProp); - } - } - } - - return { - add, - clearEvents, - remove, - trigger - }; -})(); - -/** - * Interface for EventData extending Element for custom event storage. - */ -interface EventData extends Element { - __eventList: EventList; -} - -/** - * Interface for a list of events associated with an element. - */ -interface EventList { - events?: EventOptions[]; -} - -/** - * Interface for event options to store event details. - */ -interface EventOptions { - name: string; - listener: Function; - debounce?: Function; -} - -/** - * Common Event argument for all base Essential JavaScript 2 Events. - * - * @private - */ -export interface BaseEventArgs { - /** - * Specifies name of the event. - */ - name?: string; -} diff --git a/components/base/src/fetch.tsx b/components/base/src/fetch.tsx deleted file mode 100644 index 63f23ea..0000000 --- a/components/base/src/fetch.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { isNullOrUndefined as isNOU } from './util'; - -/** - * Interface for the Fetch properties and methods. - */ -export interface IFetch { - /** - * Specifies the URL to which the request is to be sent. - * - * @default null - */ - url?: string; - /** - * Specifies which request method is to be used, such as GET, POST, etc. - * - * @default GET - */ - type?: string; - /** - * Specifies the content type of the request, which is used to indicate the original media type of the resource. - * - * @default null - */ - contentType?: string; - /** - * Specifies the data that needs to be added to the request. - * - * @default null - */ - data?: string | Object; - /** - * A boolean value indicating whether to reject the promise or not. - * - * @private - * @default true - */ - emitError?: boolean; - /** - * Specifies the request object that represents a resource request. - * - * @default null - */ - fetchRequest?: Request; - /** - * Specifies the callback function to be triggered before sending the request to the server. - * This can be used to modify the fetchRequest object before it is sent. - * - * @event beforeSend - */ - beforeSend?: ((args: BeforeSendFetchEventArgs) => void) | null; - /** - * Specifies the callback function to be triggered after the response is received. - * This callback will be triggered even if the request is failed. - * - * @event onLoad - */ - onLoad?: ((response: Response) => void) | null; - /** - * Specifies the callback function to be triggered after the request is successful. - * The callback will contain the server response as a parameter. - * - * @event onSuccess - */ - onSuccess?: ((data: string | Object, instance: IFetch) => void) | null; - /** - * Specifies the callback function to be triggered after the request is failed. - * - * @event onFailure - */ - onFailure?: ((error: Error) => void) | null; - - /** - * Sends the fetch request. - * - * @param {string | Object} [data] - Optional data to be sent with the request. - * @returns {Promise} - A promise that resolves to the fetch response. - */ - send?: (data?: string | Object) => Promise; -} - - -/** - * The Fetch function provides a way to make asynchronous network requests, typically to retrieve resources from a server. - * - * @param {string | Fetch} [props] - The URL string or Fetch object containing request details. - * @param {string} [type] - The HTTP method type (e.g., 'GET', 'POST'). - * @param {string} [contentType] - The content type of the request. - * @returns {Fetch} A Fetch object for making the request. - * - * @example - * - * var fetchApi = Fetch('index.html', 'GET'); - * fetchApi.send() - * .then((value) => { - * console.log(value); - * }).catch((error) => { - * console.log(error); - * }); - */ -export function Fetch(props?: string | IFetch, type?: string, contentType?: string): IFetch { - let url: string | undefined; - let fetchProps: IFetch; - if (typeof props === 'string') { - url = props; - fetchProps = { - url, - type: type ? type.toUpperCase() : 'GET', - contentType: contentType || 'application/json; charset=utf-8' - }; - } else { - ({ url, type, contentType, ...fetchProps } = props); - fetchProps.url = url; - fetchProps.type = type ? type.toUpperCase() : 'GET'; - fetchProps.contentType = contentType || 'application/json; charset=utf-8'; - } - const propsRef: IFetch = { - emitError: true, - ...fetchProps - }; - let fetchResponse: Promise | null = null; - - propsRef.send = async (data?: string | Object): Promise => { - const contentTypes: Object = { - 'application/json': 'json', - 'multipart/form-data': 'formData', - 'application/octet-stream': 'blob', - 'application/x-www-form-urlencoded': 'formData' - }; - try { - if (isNOU(propsRef.fetchRequest) && propsRef.type === 'GET') { - propsRef.fetchRequest = new Request(propsRef.url, { method: propsRef.type }); - } else if (isNOU(propsRef.fetchRequest)) { - propsRef.data = data && !isNOU(data) ? data : propsRef.data; - propsRef.fetchRequest = new Request(propsRef.url, { - method: propsRef.type, - headers: { 'Content-Type': propsRef.contentType }, - body: propsRef.data as BodyInit - }); - } - const eventArgs: BeforeSendFetchEventArgs = { cancel: false, fetchRequest: propsRef.fetchRequest }; - triggerEvent(propsRef.beforeSend, eventArgs); - if (eventArgs.cancel) { return null; } - fetchResponse = fetch(propsRef.fetchRequest); - return fetchResponse.then((response: Response) => { - triggerEvent(propsRef.onLoad, response); - if (!response.ok) { - throw response; - } - let responseType: string = 'text'; - for (const key of Object.keys(contentTypes)) { - if (response.headers.get('Content-Type') && (response.headers.get('Content-Type') as string).indexOf(key) !== -1) { - responseType = contentTypes[key as string]; - } - } - return response[responseType as string](); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }).then((data: any) => { - triggerEvent(propsRef.onSuccess, data, propsRef); - return data; - }).catch((error: Error) => { - let returnVal: Object = {}; - if (propsRef.emitError) { - triggerEvent(propsRef.onFailure, error); - returnVal = Promise.reject(error); - } - return returnVal; - }); - } catch (error) { - return error; - } - }; - - /** - * Triggers the callback function with provided data and instance. - * - * @param {Function | null} callback - The callback function to be triggered - * @param {string | Object} [data] - Optional data to pass to the callback - * @param {IFetch} [instance] - Optional FetchProps instance - * @returns {void} - */ - function triggerEvent(callback: Function | null, data?: string | Object, instance?: IFetch): void { - if (!isNOU(callback) && typeof callback === 'function') { - callback(data, instance); - } - } - - return propsRef; -} - -/** - * Provides information about the beforeSend event. - */ -export interface BeforeSendFetchEventArgs { - /** - * A boolean value indicating whether to cancel the fetch request or not. - */ - cancel?: boolean; - /** - * Returns the request object that represents a resource request. - */ - fetchRequest: Request; -} diff --git a/components/base/src/hijri-parser.tsx b/components/base/src/hijri-parser.tsx deleted file mode 100644 index a3e538f..0000000 --- a/components/base/src/hijri-parser.tsx +++ /dev/null @@ -1,226 +0,0 @@ -/** - * Hijri parser custom hook - */ - - -// HijriParser namespace equivalent -interface HijriDate { - year: number; - month: number; - date: number; -} - -interface IHijriParser { - getHijriDate: (gDate: Date) => HijriDate; - toGregorian: (year: number, month: number, day: number) => Date -} -/** - * Custom hook for Hijri date parsing. - */ -export const HijriParser: IHijriParser = ((): IHijriParser => { - - const dateCorrection: number[] = [28607, 28636, 28665, 28695, 28724, 28754, 28783, 28813, 28843, 28872, 28901, 28931, 28960, 28990, - 29019, 29049, 29078, 29108, 29137, 29167, 29196, 29226, 29255, 29285, 29315, 29345, 29375, 29404, 29434, 29463, 29492, 29522, - 29551, 29580, 29610, 29640, 29669, 29699, 29729, 29759, 29788, 29818, 29847, 29876, 29906, 29935, 29964, 29994, 30023, 30053, - 30082, 30112, 30141, 30171, 30200, 30230, 30259, 30289, 30318, 30348, 30378, 30408, 30437, 30467, 30496, 30526, 30555, 30585, - 30614, 30644, 30673, 30703, 30732, 30762, 30791, 30821, 30850, 30880, 30909, 30939, 30968, 30998, 31027, 31057, 31086, 31116, - 31145, 31175, 31204, 31234, 31263, 31293, 31322, 31352, 31381, 31411, 31441, 31471, 31500, 31530, 31559, 31589, 31618, 31648, - 31676, 31706, 31736, 31766, 31795, 31825, 31854, 31884, 31913, 31943, 31972, 32002, 32031, 32061, 32090, 32120, 32150, 32180, - 32209, 32239, 32268, 32298, 32327, 32357, 32386, 32416, 32445, 32475, 32504, 32534, 32563, 32593, 32622, 32652, 32681, 32711, - 32740, 32770, 32799, 32829, 32858, 32888, 32917, 32947, 32976, 33006, 33035, 33065, 33094, 33124, 33153, 33183, 33213, 33243, - 33272, 33302, 33331, 33361, 33390, 33420, 33450, 33479, 33509, 33539, 33568, 33598, 33627, 33657, 33686, 33716, 33745, 33775, - 33804, 33834, 33863, 33893, 33922, 33952, 33981, 34011, 34040, 34069, 34099, 34128, 34158, 34187, 34217, 34247, 34277, 34306, - 34336, 34365, 34395, 34424, 34454, 34483, 34512, 34542, 34571, 34601, 34631, 34660, 34690, 34719, 34749, 34778, 34808, 34837, - 34867, 34896, 34926, 34955, 34985, 35015, 35044, 35074, 35103, 35133, 35162, 35192, 35222, 35251, 35280, 35310, 35340, 35370, - 35399, 35429, 35458, 35488, 35517, 35547, 35576, 35605, 35635, 35665, 35694, 35723, 35753, 35782, 35811, 35841, 35871, 35901, - 35930, 35960, 35989, 36019, 36048, 36078, 36107, 36136, 36166, 36195, 36225, 36254, 36284, 36314, 36343, 36373, 36403, 36433, - 36462, 36492, 36521, 36551, 36580, 36610, 36639, 36669, 36698, 36728, 36757, 36786, 36816, 36845, 36875, 36904, 36934, 36963, - 36993, 37022, 37052, 37081, 37111, 37141, 37170, 37200, 37229, 37259, 37288, 37318, 37347, 37377, 37406, 37436, 37465, 37495, - 37524, 37554, 37584, 37613, 37643, 37672, 37701, 37731, 37760, 37790, 37819, 37849, 37878, 37908, 37938, 37967, 37997, 38027, - 38056, 38085, 38115, 38144, 38174, 38203, 38233, 38262, 38292, 38322, 38351, 38381, 38410, 38440, 38469, 38499, 38528, 38558, - 38587, 38617, 38646, 38676, 38705, 38735, 38764, 38794, 38823, 38853, 38882, 38912, 38941, 38971, 39001, 39030, 39059, 39089, - 39118, 39148, 39178, 39208, 39237, 39267, 39297, 39326, 39355, 39385, 39414, 39444, 39473, 39503, 39532, 39562, 39592, 39621, - 39650, 39680, 39709, 39739, 39768, 39798, 39827, 39857, 39886, 39916, 39946, 39975, 40005, 40035, 40064, 40094, 40123, 40153, - 40182, 40212, 40241, 40271, 40300, 40330, 40359, 40389, 40418, 40448, 40477, 40507, 40536, 40566, 40595, 40625, 40655, 40685, - 40714, 40744, 40773, 40803, 40832, 40862, 40892, 40921, 40951, 40980, 41009, 41039, 41068, 41098, 41127, 41157, 41186, 41216, - 41245, 41275, 41304, 41334, 41364, 41393, 41422, 41452, 41481, 41511, 41540, 41570, 41599, 41629, 41658, 41688, 41718, 41748, - 41777, 41807, 41836, 41865, 41894, 41924, 41953, 41983, 42012, 42042, 42072, 42102, 42131, 42161, 42190, 42220, 42249, 42279, - 42308, 42337, 42367, 42397, 42426, 42456, 42485, 42515, 42545, 42574, 42604, 42633, 42662, 42692, 42721, 42751, 42780, 42810, - 42839, 42869, 42899, 42929, 42958, 42988, 43017, 43046, 43076, 43105, 43135, 43164, 43194, 43223, 43253, 43283, 43312, 43342, - 43371, 43401, 43430, 43460, 43489, 43519, 43548, 43578, 43607, 43637, 43666, 43696, 43726, 43755, 43785, 43814, 43844, 43873, - 43903, 43932, 43962, 43991, 44021, 44050, 44080, 44109, 44139, 44169, 44198, 44228, 44258, 44287, 44317, 44346, 44375, 44405, - 44434, 44464, 44493, 44523, 44553, 44582, 44612, 44641, 44671, 44700, 44730, 44759, 44788, 44818, 44847, 44877, 44906, 44936, - 44966, 44996, 45025, 45055, 45084, 45114, 45143, 45172, 45202, 45231, 45261, 45290, 45320, 45350, 45380, 45409, 45439, 45468, - 45498, 45527, 45556, 45586, 45615, 45644, 45674, 45704, 45733, 45763, 45793, 45823, 45852, 45882, 45911, 45940, 45970, 45999, - 46028, 46058, 46088, 46117, 46147, 46177, 46206, 46236, 46265, 46295, 46324, 46354, 46383, 46413, 46442, 46472, 46501, 46531, - 46560, 46590, 46620, 46649, 46679, 46708, 46738, 46767, 46797, 46826, 46856, 46885, 46915, 46944, 46974, 47003, 47033, 47063, - 47092, 47122, 47151, 47181, 47210, 47240, 47269, 47298, 47328, 47357, 47387, 47417, 47446, 47476, 47506, 47535, 47565, 47594, - 47624, 47653, 47682, 47712, 47741, 47771, 47800, 47830, 47860, 47890, 47919, 47949, 47978, 48008, 48037, 48066, 48096, 48125, - 48155, 48184, 48214, 48244, 48273, 48303, 48333, 48362, 48392, 48421, 48450, 48480, 48509, 48538, 48568, 48598, 48627, 48657, - 48687, 48717, 48746, 48776, 48805, 48834, 48864, 48893, 48922, 48952, 48982, 49011, 49041, 49071, 49100, 49130, 49160, 49189, - 49218, 49248, 49277, 49306, 49336, 49365, 49395, 49425, 49455, 49484, 49514, 49543, 49573, 49602, 49632, 49661, 49690, 49720, - 49749, 49779, 49809, 49838, 49868, 49898, 49927, 49957, 49986, 50016, 50045, 50075, 50104, 50133, 50163, 50192, 50222, 50252, - 50281, 50311, 50340, 50370, 50400, 50429, 50459, 50488, 50518, 50547, 50576, 50606, 50635, 50665, 50694, 50724, 50754, 50784, - 50813, 50843, 50872, 50902, 50931, 50960, 50990, 51019, 51049, 51078, 51108, 51138, 51167, 51197, 51227, 51256, 51286, 51315, - 51345, 51374, 51403, 51433, 51462, 51492, 51522, 51552, 51582, 51611, 51641, 51670, 51699, 51729, 51758, 51787, 51816, 51846, - 51876, 51906, 51936, 51965, 51995, 52025, 52054, 52083, 52113, 52142, 52171, 52200, 52230, 52260, 52290, 52319, 52349, 52379, - 52408, 52438, 52467, 52497, 52526, 52555, 52585, 52614, 52644, 52673, 52703, 52733, 52762, 52792, 52822, 52851, 52881, 52910, - 52939, 52969, 52998, 53028, 53057, 53087, 53116, 53146, 53176, 53205, 53235, 53264, 53294, 53324, 53353, 53383, 53412, 53441, - 53471, 53500, 53530, 53559, 53589, 53619, 53648, 53678, 53708, 53737, 53767, 53796, 53825, 53855, 53884, 53913, 53943, 53973, - 54003, 54032, 54062, 54092, 54121, 54151, 54180, 54209, 54239, 54268, 54297, 54327, 54357, 54387, 54416, 54446, 54476, 54505, - 54535, 54564, 54593, 54623, 54652, 54681, 54711, 54741, 54770, 54800, 54830, 54859, 54889, 54919, 54948, 54977, 55007, 55036, - 55066, 55095, 55125, 55154, 55184, 55213, 55243, 55273, 55302, 55332, 55361, 55391, 55420, 55450, 55479, 55508, 55538, 55567, - 55597, 55627, 55657, 55686, 55716, 55745, 55775, 55804, 55834, 55863, 55892, 55922, 55951, 55981, 56011, 56040, 56070, 56100, - 56129, 56159, 56188, 56218, 56247, 56276, 56306, 56335, 56365, 56394, 56424, 56454, 56483, 56513, 56543, 56572, 56601, 56631, - 56660, 56690, 56719, 56749, 56778, 56808, 56837, 56867, 56897, 56926, 56956, 56985, 57015, 57044, 57074, 57103, 57133, 57162, - 57192, 57221, 57251, 57280, 57310, 57340, 57369, 57399, 57429, 57458, 57487, 57517, 57546, 57576, 57605, 57634, 57664, 57694, - 57723, 57753, 57783, 57813, 57842, 57871, 57901, 57930, 57959, 57989, 58018, 58048, 58077, 58107, 58137, 58167, 58196, 58226, - 58255, 58285, 58314, 58343, 58373, 58402, 58432, 58461, 58491, 58521, 58551, 58580, 58610, 58639, 58669, 58698, 58727, 58757, - 58786, 58816, 58845, 58875, 58905, 58934, 58964, 58994, 59023, 59053, 59082, 59111, 59141, 59170, 59200, 59229, 59259, 59288, - 59318, 59348, 59377, 59407, 59436, 59466, 59495, 59525, 59554, 59584, 59613, 59643, 59672, 59702, 59731, 59761, 59791, 59820, - 59850, 59879, 59909, 59939, 59968, 59997, 60027, 60056, 60086, 60115, 60145, 60174, 60204, 60234, 60264, 60293, 60323, 60352, - 60381, 60411, 60440, 60469, 60499, 60528, 60558, 60588, 60618, 60648, 60677, 60707, 60736, 60765, 60795, 60824, 60853, 60883, - 60912, 60942, 60972, 61002, 61031, 61061, 61090, 61120, 61149, 61179, 61208, 61237, 61267, 61296, 61326, 61356, 61385, 61415, - 61445, 61474, 61504, 61533, 61563, 61592, 61621, 61651, 61680, 61710, 61739, 61769, 61799, 61828, 61858, 61888, 61917, 61947, - 61976, 62006, 62035, 62064, 62094, 62123, 62153, 62182, 62212, 62242, 62271, 62301, 62331, 62360, 62390, 62419, 62448, 62478, - 62507, 62537, 62566, 62596, 62625, 62655, 62685, 62715, 62744, 62774, 62803, 62832, 62862, 62891, 62921, 62950, 62980, 63009, - 63039, 63069, 63099, 63128, 63157, 63187, 63216, 63246, 63275, 63305, 63334, 63363, 63393, 63423, 63453, 63482, 63512, 63541, - 63571, 63600, 63630, 63659, 63689, 63718, 63747, 63777, 63807, 63836, 63866, 63895, 63925, 63955, 63984, 64014, 64043, 64073, - 64102, 64131, 64161, 64190, 64220, 64249, 64279, 64309, 64339, 64368, 64398, 64427, 64457, 64486, 64515, 64545, 64574, 64603, - 64633, 64663, 64692, 64722, 64752, 64782, 64811, 64841, 64870, 64899, 64929, 64958, 64987, 65017, 65047, 65076, 65106, 65136, - 65166, 65195, 65225, 65254, 65283, 65313, 65342, 65371, 65401, 65431, 65460, 65490, 65520, 65549, 65579, 65608, 65638, 65667, - 65697, 65726, 65755, 65785, 65815, 65844, 65874, 65903, 65933, 65963, 65992, 66022, 66051, 66081, 66110, 66140, 66169, 66199, - 66228, 66258, 66287, 66317, 66346, 66376, 66405, 66435, 66465, 66494, 66524, 66553, 66583, 66612, 66641, 66671, 66700, 66730, - 66760, 66789, 66819, 66849, 66878, 66908, 66937, 66967, 66996, 67025, 67055, 67084, 67114, 67143, 67173, 67203, 67233, 67262, - 67292, 67321, 67351, 67380, 67409, 67439, 67468, 67497, 67527, 67557, 67587, 67617, 67646, 67676, 67705, 67735, 67764, 67793, - 67823, 67852, 67882, 67911, 67941, 67971, 68000, 68030, 68060, 68089, 68119, 68148, 68177, 68207, 68236, 68266, 68295, 68325, - 68354, 68384, 68414, 68443, 68473, 68502, 68532, 68561, 68591, 68620, 68650, 68679, 68708, 68738, 68768, 68797, 68827, 68857, - 68886, 68916, 68946, 68975, 69004, 69034, 69063, 69092, 69122, 69152, 69181, 69211, 69240, 69270, 69300, 69330, 69359, 69388, - 69418, 69447, 69476, 69506, 69535, 69565, 69595, 69624, 69654, 69684, 69713, 69743, 69772, 69802, 69831, 69861, 69890, 69919, - 69949, 69978, 70008, 70038, 70067, 70097, 70126, 70156, 70186, 70215, 70245, 70274, 70303, 70333, 70362, 70392, 70421, 70451, - 70481, 70510, 70540, 70570, 70599, 70629, 70658, 70687, 70717, 70746, 70776, 70805, 70835, 70864, 70894, 70924, 70954, 70983, - 71013, 71042, 71071, 71101, 71130, 71159, 71189, 71218, 71248, 71278, 71308, 71337, 71367, 71397, 71426, 71455, 71485, 71514, - 71543, 71573, 71602, 71632, 71662, 71691, 71721, 71751, 71781, 71810, 71839, 71869, 71898, 71927, 71957, 71986, 72016, 72046, - 72075, 72105, 72135, 72164, 72194, 72223, 72253, 72282, 72311, 72341, 72370, 72400, 72429, 72459, 72489, 72518, 72548, 72577, - 72607, 72637, 72666, 72695, 72725, 72754, 72784, 72813, 72843, 72872, 72902, 72931, 72961, 72991, 73020, 73050, 73080, 73109, - 73139, 73168, 73197, 73227, 73256, 73286, 73315, 73345, 73375, 73404, 73434, 73464, 73493, 73523, 73552, 73581, 73611, 73640, - 73669, 73699, 73729, 73758, 73788, 73818, 73848, 73877, 73907, 73936, 73965, 73995, 74024, 74053, 74083, 74113, 74142, 74172, - 74202, 74231, 74261, 74291, 74320, 74349, 74379, 74408, 74437, 74467, 74497, 74526, 74556, 74586, 74615, 74645, 74675, 74704, - 74733, 74763, 74792, 74822, 74851, 74881, 74910, 74940, 74969, 74999, 75029, 75058, 75088, 75117, 75147, 75176, 75206, 75235, - 75264, 75294, 75323, 75353, 75383, 75412, 75442, 75472, 75501, 75531, 75560, 75590, 75619, 75648, 75678, 75707, 75737, 75766, - 75796, 75826, 75856, 75885, 75915, 75944, 75974, 76003, 76032, 76062, 76091, 76121, 76150, 76180, 76210, 76239, 76269, 76299, - 76328, 76358, 76387, 76416, 76446, 76475, 76505, 76534, 76564, 76593, 76623, 76653, 76682, 76712, 76741, 76771, 76801, 76830, - 76859, 76889, 76918, 76948, 76977, 77007, 77036, 77066, 77096, 77125, 77155, 77185, 77214, 77243, 77273, 77302, 77332, 77361, - 77390, 77420, 77450, 77479, 77509, 77539, 77569, 77598, 77627, 77657, 77686, 77715, 77745, 77774, 77804, 77833, 77863, 77893, - 77923, 77952, 77982, 78011, 78041, 78070, 78099, 78129, 78158, 78188, 78217, 78247, 78277, 78307, 78336, 78366, 78395, 78425, - 78454, 78483, 78513, 78542, 78572, 78601, 78631, 78661, 78690, 78720, 78750, 78779, 78808, 78838, 78867, 78897, 78926, 78956, - 78985, 79015, 79044, 79074, 79104, 79133, 79163, 79192, 79222, 79251, 79281, 79310, 79340, 79369, 79399, 79428, 79458, 79487, - 79517, 79546, 79576, 79606, 79635, 79665, 79695, 79724, 79753, 79783, 79812, 79841, 79871, 79900, 79930, 79960, 79990 - ]; - - /** - * Converts a Gregorian date to a Hijri date. - * - * @param {Date} gDate - The Gregorian date to convert. - * @returns {HijriDate} - The converted Hijri date. - */ - function getHijriDate(gDate: Date): HijriDate { - let day: number = gDate.getDate(); - let month: number = gDate.getMonth(); - let year: number = gDate.getFullYear(); - let tMonth: number = month + 1; - let tYear: number = year; - if (tMonth < 3) { - tYear -= 1; - tMonth += 12; - } - let yPrefix: number = Math.floor(tYear / 100); - let julilanOffset: number = yPrefix - Math.floor(yPrefix / 4) - 2; - const julianNumber: number = Math.floor(365.25 * (tYear + 4716)) + Math.floor(30.6001 * (tMonth + 1)) + day - julilanOffset - 1524; - yPrefix = Math.floor((julianNumber - 1867216.25) / 36524.25); - julilanOffset = yPrefix - Math.floor(yPrefix / 4) + 1; - const b: number = julianNumber + julilanOffset + 1524; - let c: number = Math.floor((b - 122.1) / 365.25); - const d: number = Math.floor(365.25 * c); - const tempMonth: number = Math.floor((b - d) / 30.6001); - day = (b - d) - Math.floor(30.6001 * tempMonth); - month = Math.floor((b - d) / 20.6001); - if (month > 13) { - c += 1; - month -= 12; - } - month -= 1; - year = c - 4716; - const modifiedJulianDate: number = julianNumber - 2400000; - - const iyear: number = 10631 / 30; - let z: number = julianNumber - 1948084; - const cyc: number = Math.floor(z / 10631.); - z = z - 10631 * cyc; - const j: number = Math.floor((z - 0.1335) / iyear); - const iy: number = 30 * cyc + j; - z = z - Math.floor(j * iyear + 0.1335); - let im: number = Math.floor((z + 28.5001) / 29.5); - if (im === 13) { - im = 12; - } - const tempDay: number = z - Math.floor(29.5001 * im - 29); - let i: number = 0; - for (; i < dateCorrection.length; i++) { - if (dateCorrection[parseInt(i.toString(), 10)] > modifiedJulianDate) { - break; - } - } - const iln: number = i + 16260; - const ii: number = Math.floor((iln - 1) / 12); - let hYear: number = ii + 1; - let hmonth: number = iln - 12 * ii; - let hDate: number = modifiedJulianDate - dateCorrection[i - 1] + 1; - if ((hDate + '').length > 2) { - hDate = tempDay; - hmonth = im; - hYear = iy; - } - return { year: hYear, month: hmonth, date: hDate }; - } - - /** - * Converts a Hijri date to a Gregorian date. - * - * @param {number} year - The Hijri year. - * @param {number} month - The Hijri month. - * @param {number} day - The Hijri day. - * @returns {Date} - The converted Gregorian date. - */ - function toGregorian(year: number, month: number, day: number): Date { - const iy: number = year; - const im: number = month; - const id: number = day; - const ii: number = iy - 1; - const iln: number = (ii * 12) + 1 + (im - 1); - const i: number = iln - 16260; - const mcjdn: number = id + dateCorrection[i - 1] - 1; - const julianDate: number = mcjdn + 2400000; - const z: number = Math.floor(julianDate + 0.5); - let a: number = Math.floor((z - 1867216.25) / 36524.25); - a = z + 1 + a - Math.floor(a / 4); - const b: number = a + 1524; - const c: number = Math.floor((b - 122.1) / 365.25); - const d: number = Math.floor(365.25 * c); - const e: number = Math.floor((b - d) / 30.6001); - const gDay: number = b - d - Math.floor(e * 30.6001); - let gMonth: number = e - (e > 13.5 ? 13 : 1); - const gYear: number = c - (gMonth > 2.5 ? 4716 : 4715); - - if (gYear <= 0) { - gMonth--; - } - - return new Date(`${gYear}/${gMonth}/${gDay}`); - } - - return { - getHijriDate, - toGregorian - }; -})(); diff --git a/components/base/src/index.ts b/components/base/src/index.ts deleted file mode 100644 index 05d324e..0000000 --- a/components/base/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Base modules - */ -export * from './animation'; -export * from './base'; -export * from './browser'; -export * from './component'; -export * from './dom'; -export * from './drag-util'; -export * from './dragdrop'; -export * from './draggable'; -export * from './droppable'; -export * from './event-handler'; -export * from './fetch'; -export * from './hijri-parser'; -export * from './internationalization'; -export * from './l10n'; -export * from './observer'; -export * from './ripple'; -export * from './sanitize-helper'; -export * from './touch'; -export * from './util'; -export * from './validate-lic'; -export * from './provider'; -export * from './svg-icon'; diff --git a/components/base/src/internationalization.tsx b/components/base/src/internationalization.tsx deleted file mode 100644 index 7db4e6c..0000000 --- a/components/base/src/internationalization.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import { DateFormat } from './intl/date-formatter'; -import { NumberFormat } from './intl/number-formatter'; -import { DateParser } from './intl/date-parser'; -import { NumberParser } from './intl/number-parser'; -import { IntlBase } from './intl/intl-base'; -import { extend, getValue } from './util'; - -/** - * Interface for DateFormatOptions - */ -export interface DateFormatOptions { - /** - * Specifies the skeleton for date formatting. - */ - skeleton?: string; - /** - * Specifies the type of date formatting either date, dateTime or time. - */ - type?: string; - /** - * Specifies custom date formatting to be used. - */ - format?: string; - /** - * Specifies the calendar mode other than gregorian - */ - calendar?: string; - /** - * Enable server side date formating. - */ - isServerRendered?: boolean; -} - -/** - * Interface for numberFormatOptions - */ -export interface NumberFormatOptions { - /** - * Specifies minimum fraction digits in formatted value. - */ - minimumFractionDigits?: number; - /** - * Specifies maximum fraction digits in formatted value. - */ - maximumFractionDigits?: number; - /** - * Specifies minimum significant digits in formatted value. - */ - minimumSignificantDigits?: number; - /** - * Specifies maximum significant digits in formatted value. - */ - maximumSignificantDigits?: number; - /** - * Specifies whether to use grouping or not in formatted value, - */ - useGrouping?: boolean; - /** - * Specifies whether to ignore currency symbol in formatted value, - */ - ignoreCurrency?: boolean; - /** - * Specifies the skeleton for perform formatting. - */ - skeleton?: string; - /** - * Specifies the currency code to be used for formatting. - */ - currency?: string | null; - /** - * Specifies minimum integer digits in formatted value. - */ - minimumIntegerDigits?: number; - /** - * Specifies custom number format for formatting. - */ - format?: string; - /** - * Species which currency symbol to consider. - */ - altSymbol?: string; -} - - -/** - * Specifies the CLDR data loaded for internationalization functionalities. - * - * @private - */ -export const cldrData: Object = {}; - -/** - * Specifies the default culture value to be considered. - * - * @private - */ -export let defaultCulture: string = 'en-US'; - -/** - * Specifies default currency code to be considered - * - * @private - */ -export let defaultCurrencyCode: string = 'USD'; - -const mapper: string[] = ['numericObject', 'dateObject']; - -/** - * Gets a date formatter function for specified culture and format options - * - * @param {string} culture - The culture code (e.g. 'en-US') - * @param {DateFormatOptions} props - Date formatting options - * @returns {Function} Formatter function that accepts Date objects - */ -export function getDateFormat(culture: string, props?: DateFormatOptions): Function { - return DateFormat.dateFormat(culture, props || { type: 'date', skeleton: 'short' }, cldrData); -} - -/** - * Gets a number formatter function for specified culture and format options - * - * @param {string} culture - The culture code (e.g. 'en-US') - * @param {NumberFormatOptions} props - Number formatting options - * @returns {Function} Formatter function that accepts numeric values - */ -export function getNumberFormat(culture: string, props?: NumberFormatOptions): Function { - if (props && !props.currency) { - props.currency = defaultCurrencyCode; - } - return NumberFormat.numberFormatter(culture, props || {}, cldrData); -} - -/** - * Returns the parser function for given props. - * - * @param {string} culture - The culture code (e.g. 'en-US') - * @param {DateFormatOptions} props - Specifies the format props in which the parser function will return. - * @returns {Function} The date parser function. - */ -export function getDateParser(culture: string, props?: DateFormatOptions): Function { - return DateParser.dateParser(culture, props || { skeleton: 'short', type: 'date' }, cldrData); -} - -/** - * Returns the parser function for given props. - * - * @param {string} culture - The culture code (e.g. 'en-US') - * @param {NumberFormatOptions} props - Specifies the format props in which the parser function will return. - * @returns {Function} The number parser function. - */ -export function getNumberParser(culture: string, props?: NumberFormatOptions): Function { - return NumberParser.numberParser(culture, props || { format: 'N' }, cldrData); -} - -/** - * Returns the formatted string based on format props. - * - * @param {string} culture - The culture code (e.g. 'en-US') - * @param {number} value - Specifies the number to format. - * @param {NumberFormatOptions} option - Specifies the format props in which the number will be formatted. - * @returns {string} The formatted number string. - */ -export function formatNumber(culture: string, value: number, option?: NumberFormatOptions): string { - return getNumberFormat(culture, option)(value) || value; -} - -/** - * Returns the formatted date string based on format props. - * - * @param {string} culture - The culture code (e.g. 'en-US'). - * @param {Date} value - Specifies the number to format. - * @param {DateFormatOptions} option - Specifies the format props in which the number will be formatted. - * @returns {string} The formatted date string. - */ -export function formatDate(culture: string, value: Date, option?: DateFormatOptions): string { - return getDateFormat(culture, option)(value); -} - -/** - * Returns the date object for given date string and props. - * - * @param {string} culture - The culture code (e.g. 'en-US'). - * @param {string} value - Specifies the string to parse. - * @param {DateFormatOptions} option - Specifies the parse props in which the date string will be parsed. - * @returns {Date} The parsed Date object. - */ -export function parseDate(culture: string, value: string, option?: DateFormatOptions): Date { - return getDateParser(culture, option)(value); -} - -/** - * Returns the number object from the given string value and props. - * - * @param {string} culture - The culture code (e.g. 'en-US'). - * @param {string} value - Specifies the string to parse. - * @param {NumberFormatOptions} option - Specifies the parse props in which the string number will be parsed. - * @returns {number} The parsed number. - */ -export function parseNumber(culture: string, value: string, option?: NumberFormatOptions): number { - return getNumberParser(culture, option)(value); -} - -/** - * Returns Native Date Time Pattern - * - * @param {string} culture - The culture code (e.g. 'en-US'). - * @param {DateFormatOptions} option - Specifies the parse props for resultant date time pattern. - * @param {boolean} isExcelFormat - Specifies format value to be converted to excel pattern. - * @returns {string} The native date time pattern. - * @private - */ -export function getDatePattern(culture: string, option: DateFormatOptions, isExcelFormat?: boolean): string { - return IntlBase.getActualDateTimeFormat(culture, option, cldrData, isExcelFormat); -} - -/** - * Returns Native Number Pattern - * - * @param {string} culture - The culture code (e.g. 'en-US'). - * @param {NumberFormatOptions} option - Specifies the parse props for resultant number pattern. - * @param {boolean} isExcel - Specifies whether to return Excel format. - * @returns {string} The native number pattern. - * @private - */ -export function getNumberPattern(culture: string, option: NumberFormatOptions, isExcel?: boolean): string { - return IntlBase.getActualNumberFormat(culture, option, cldrData, isExcel); -} - -/** - * Returns the First Day of the Week - * - * @param {string} culture - The culture code (e.g. 'en-US'). - * @returns {number} The first day of the week. - */ -export function getFirstDayOfWeek(culture: string): number { - return IntlBase.getWeekData(culture, cldrData); -} -/** - * Set the default culture to all components - * - * @private - * @param {string} cultureName - Specifies the culture name to be set as default culture. - * @returns {void} - */ -export function setCulture(cultureName: string): void { - defaultCulture = cultureName; -} - -/** - * Set the default currency code to all components - * - * @private - * @param {string} currencyCode - Specifies the currency code to be set as default currency. - * @returns {void} - */ -export function setCurrencyCode(currencyCode: string): void { - defaultCurrencyCode = currencyCode; -} - -/** - * Load the CLDR data into context - * - * @param {Object[]} data - Specifies the CLDR data's to be used for formatting and parser. - * @returns {void} - */ -export function loadCldr(...data: Object[]): void { - for (const obj of data) { - extend(cldrData, obj, {}, true); - } -} - - -/** - * To get the numeric CLDR object for given culture - * - * @private - * @param {string} locale - Specifies the locale for which numericObject to be returned. - * @param {string} type - Specifies the type, by default it's decimal. - * @returns {Object} Returns the numeric CLDR object containing number formatting patterns and symbols - */ -export function getNumericObject(locale: string, type?: string): Object { - const numObject: Object = IntlBase.getDependables(cldrData, locale, '', true)[mapper[0]]; - const dateObject: Object = IntlBase.getDependables(cldrData, locale, '')[mapper[1]]; - const numSystem: string = getValue('defaultNumberingSystem', numObject); - const symbPattern: Record = getValue('symbols-numberSystem-' + numSystem, numObject); - const pattern: string = IntlBase.getSymbolPattern(type || 'decimal', numSystem, numObject, false); - return extend(symbPattern, IntlBase.getFormatData(pattern, true, '', true), { 'dateSeparator': IntlBase.getDateSeparator(dateObject) }); -} - -/** - * To get the numeric CLDR number base object for given culture - * - * @private - * @param {string} locale - Specifies the locale for which numericObject to be returned. - * @param {string} currency - Specifies the currency for which numericObject to be returned. - * @returns {string} Returns the currency symbol for the specified locale and currency - */ -export function getNumberDependable(locale: string, currency: string): string { - const numObject: Object = IntlBase.getDependables(cldrData, locale, '', true); - return IntlBase.getCurrencySymbol(numObject['numericObject'], currency); -} - -/** - * To get the default date CLDR object. - * - * @private - * @param {string} mode - Specify the mode, optional. - * @returns {Object} Returns the default date CLDR object containing date formatting patterns - */ -export function getDefaultDateObject(mode?: string): Object { - return IntlBase.getDependables(cldrData, '', mode, false)[mapper[1]]; -} diff --git a/components/base/src/intl/date-formatter.tsx b/components/base/src/intl/date-formatter.tsx deleted file mode 100644 index 6260caa..0000000 --- a/components/base/src/intl/date-formatter.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import { DateFormatOptions } from '../internationalization'; -import { ParserBase as parser, NumberMapper } from './parser-base'; -import { IntlBase as base, TimeZoneOptions, Dependables, DateObject } from './intl-base'; -import { isUndefined, throwError, getValue } from '../util'; -import { HijriParser } from '../hijri-parser'; -import { isNullOrUndefined } from '../util'; -const abbreviateRegexGlobal: RegExp = /\/MMMMM|MMMM|MMM|a|LLLL|LLL|EEEEE|EEEE|E|K|cccc|ccc|WW|W|G+|z+/gi; -const standalone: string = 'stand-alone'; -const weekdayKey: string[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; -export const basicPatterns: string[] = ['short', 'medium', 'long', 'full']; -/** - * @interface FormatOptions - * Interface for Date Format Options Modules. - * - * @private - */ -export interface FormatOptions { - month?: Object; - weekday?: Object; - pattern?: string; - designator?: Object; - timeZone?: TimeZoneOptions; - era?: Object; - hour12?: boolean; - numMapper?: NumberMapper; - dateSeperator?: string; - isIslamic?: boolean; - weekOfYear?: string; -} -const timeSetter: Object = { - m: 'getMinutes', - h: 'getHours', - H: 'getHours', - s: 'getSeconds', - d: 'getDate', - f: 'getMilliseconds' -}; -export const datePartMatcher: { [key: string]: Object } = { - 'M': 'month', - 'd': 'day', - 'E': 'weekday', - 'c': 'weekday', - 'y': 'year', - 'm': 'minute', - 'h': 'hour', - 'H': 'hour', - 's': 'second', - 'L': 'month', - 'a': 'designator', - 'z': 'timeZone', - 'Z': 'timeZone', - 'G': 'era', - 'f': 'milliseconds' -}; - -const timeSeparator: string = 'timeSeparator'; - -export interface IDateFormat { - /** - * Returns the formatter function for a given skeleton. - * - * @private - * @param {string} culture Specifies the culture name for formatting. - * @param {DateFormatOptions} option Specifies the format in which the date will format. - * @param {Object} cldr Specifies the global cldr data collection. - * @returns {Function} Formatter function - */ - dateFormat: (culture: string, option: DateFormatOptions, cldr: Object) => Function; - /** - * Returns the value of the Time Zone. - * - * @private - * @param {number} tVal Time Zone offset value. - * @param {string} pattern Time Zone pattern. - * @returns {string} Time Zone formatted string. - */ - getTimeZoneValue: (tVal: number, pattern: string) => string; -} - -/** - * @hook useDateFormat - * Date Format is a framework that provides support for date formatting. - * - * @returns {Object} An object containing methods related to date formatting. - */ -export const DateFormat: IDateFormat = (() => { - - /** - * Returns the formatter function for a given skeleton. - * - * @param {string} culture Specifies the culture name for formatting. - * @param {DateFormatOptions} option Specifies the format in which the date will format. - * @param {Object} cldr Specifies the global cldr data collection. - * @returns {Function} Formatter function - */ - function dateFormat(culture: string, option: DateFormatOptions, cldr: Object): Function { - const dependable: Dependables = base.getDependables(cldr, culture, option.calendar); - const dateObject: Object = dependable.dateObject; - const formatOptions: FormatOptions = { isIslamic: base.islamicRegex.test(option.calendar) }; - - const resPattern: string = option.format || - base.getResultantPattern(option.skeleton, dateObject, option.type, false); - - formatOptions.dateSeperator = base.getDateSeparator(dateObject); - - if (isUndefined(resPattern)) { - throwError('Format options or type given must be invalid'); - } else { - formatOptions.pattern = resPattern; - formatOptions.numMapper = parser.getNumberMapper(dependable.parserObject, parser.getNumberingSystem(cldr)); - const patternMatch: string[] = resPattern.match(abbreviateRegexGlobal) || []; - for (const str of patternMatch) { - const len: number = str.length; - let char: string = str[0]; - if (char === 'K') { - char = 'h'; - } - - switch (char) { - case 'E': - case 'c': - formatOptions.weekday = (dependable.dateObject)[`${base.days}`][`${standalone}`][(base).monthIndex[`${len}`]]; - break; - case 'M': - case 'L': - formatOptions.month = (dependable.dateObject)[`${base.month}`][`${standalone}`][(base.monthIndex)[`${len}`]]; - break; - case 'a': - formatOptions.designator = getValue('dayPeriods.format.wide', dateObject); - break; - case 'G': { - const eText: string = (len <= 3) ? 'eraAbbr' : (len === 4) ? 'eraNames' : 'eraNarrow'; - formatOptions.era = getValue('eras.' + eText, dependable.dateObject); - break; - } - case 'z': - formatOptions.timeZone = getValue('dates.timeZoneNames', dependable.parserObject); - break; - } - } - } - - return (value: Date): string => { - if (isNaN(value.getDate())) { - return null; - } - return intDateFormatter(value, formatOptions); - }; - } - - /** - * Formats the date according to the specified options. - * - * @param {Date} value The date to format. - * @param {FormatOptions} options The formatting options. - * @returns {string} The formatted date string. - */ - function intDateFormatter(value: Date, options: FormatOptions): string { - const pattern: string | null = options.pattern; - let ret: string = ''; - const matches: string[] | null = pattern.match(base.dateParseRegex); - const dObject: DateObject = getCurrentDateValue(value, options.isIslamic); - - if (matches) { - for (const match of matches) { - const length: number = match.length; - let char: string = match[0]; - if (char === 'K') { - char = 'h'; - } - - let curval: number; - let curvalstr: string = ''; - let isNumber: boolean; - let processNumber: boolean; - let curstr: string = ''; - - switch (char) { - case 'M': - case 'L': - curval = dObject.month; - if (length > 2) { - ret += options.month[`${curval}`]; - } else { - isNumber = true; - } - break; - case 'E': - case 'c': - ret += options.weekday[`${weekdayKey[value.getDay()]}`]; - break; - case 'H': - case 'h': - case 'm': - case 's': - case 'd': - case 'f': - isNumber = true; - if (char === 'd') { - curval = dObject.date; - } else if (char === 'f') { - isNumber = false; - processNumber = true; - curvalstr = (value)[`${(timeSetter)[`${char}`]}`]().toString(); - curvalstr = curvalstr.substring(0, length); - const curlength: number = curvalstr.length; - if (length !== curlength) { - if (length > 3) { - continue; - } - for (let i: number = 0; i < length - curlength; i++) { - curvalstr = '0' + curvalstr.toString(); - } - } - curstr += curvalstr; - } else { - curval = (value)[`${(timeSetter)[`${char}`]}`](); - } - if (char === 'h') { - curval = curval % 12 || 12; - } - break; - case 'y': - processNumber = true; - curstr += dObject.year; - if (length === 2) { - curstr = curstr.substr(curstr.length - 2); - } - break; - case 'a': { - const desig: string = value.getHours() < 12 ? 'am' : 'pm'; - ret += options.designator[`${desig}`]; - break; - } - case 'G': { - const dec: number = value.getFullYear() < 0 ? 0 : 1; - let retu: string = options.era[`${dec}`]; - if (isNullOrUndefined(retu)) { - retu = options.era[dec ? 0 : 1]; - } - ret += retu || ''; - break; - } - case '\'': - ret += (match === '\'\'') ? '\'' : match.replace(/'/g, ''); - break; - case 'z': { - const timezone: number = value.getTimezoneOffset(); - let pattern: string = (length < 4) ? '+H;-H' : options.timeZone.hourFormat; - pattern = pattern.replace(/:/g, options.numMapper.timeSeparator); - if (timezone === 0) { - ret += options.timeZone.gmtZeroFormat; - } else { - processNumber = true; - curstr = getTimeZoneValue(timezone, pattern); - } - curstr = options.timeZone.gmtFormat.replace(/\{0\}/, curstr); - break; - } - case ':': - ret += (options).numMapper.numberSymbols[`${timeSeparator}`]; - break; - case '/': - ret += options.dateSeperator; - break; - case 'W': - isNumber = true; - curval = base.getWeekOfYear(value); - break; - default: - ret += match; - } - if (isNumber) { - processNumber = true; - curstr = checkTwodigitNumber(curval, length); - } - if (processNumber) { - ret += parser.convertValueParts(curstr, base.latnParseRegex, options.numMapper.mapper); - } - } - } - return ret; - } - - /** - * Returns the current date values, adjusted for Islamic calendar if needed. - * - * @param {Date} value The date object. - * @param {boolean} [isIslamic] Whether the date is Islamic. - * @returns {DateObject} The current date values. - */ - function getCurrentDateValue(value: Date, isIslamic?: boolean): DateObject { - if (isIslamic) { - return HijriParser.getHijriDate(value); - } - return { year: value.getFullYear(), month: value.getMonth() + 1, date: value.getDate() }; - } - - /** - * Checks and formats the number to two digits. - * - * @param {number} val The number - * @param {number} len The desired length of the number. - * @returns {string} The formatted two-digit number string. - */ - function checkTwodigitNumber(val: number, len: number): string { - const ret: string = val + ''; - if (len === 2 && ret.length !== 2) { - return '0' + ret; - } - return ret; - } - - /** - * Returns the value of the Time Zone. - * - * @param {number} tVal Time Zone offset value. - * @param {string} pattern Time Zone pattern. - * @returns {string} Time Zone formatted string. - */ - function getTimeZoneValue(tVal: number, pattern: string): string { - const splt: string[] = pattern.split(';'); - const curPattern: string = splt[tVal > 0 ? 1 : 0]; - const no: number = Math.abs(tVal); - - return curPattern.replace(/HH?|mm/g, (str: string): string => { - const len: number = str.length; - const isHour: boolean = str.indexOf('H') !== -1; - return checkTwodigitNumber(Math.floor(isHour ? (no / 60) : (no % 60)), len); - }); - } - - return { dateFormat, getTimeZoneValue }; -})(); diff --git a/components/base/src/intl/date-parser.tsx b/components/base/src/intl/date-parser.tsx deleted file mode 100644 index fafbba9..0000000 --- a/components/base/src/intl/date-parser.tsx +++ /dev/null @@ -1,499 +0,0 @@ -import { DateFormatOptions } from '../internationalization'; -import { IntlBase as base, TimeZoneOptions, Dependables, DateObject } from './intl-base'; -import { ParserBase as parser, NumericOptions, NumberMapper } from './parser-base'; -import { isUndefined, throwError, getValue, isNullOrUndefined } from '../util'; -import { datePartMatcher } from './date-formatter'; -import { HijriParser } from '../hijri-parser'; - -const standalone: string = 'stand-alone'; -const latnRegex: RegExp = /^[0-9]*$/; -const timeSetter: Record = { - minute: 'setMinutes', - hour: 'setHours', - second: 'setSeconds', - day: 'setDate', - month: 'setMonth', - milliseconds: 'setMilliseconds' -}; - -/** - * Interface for date parsing options - */ -interface ParseOptions { - month?: Object; - weekday?: string[]; - pattern?: string; - designator?: Object; - timeZone?: TimeZoneOptions; - era?: Object; - hour12?: boolean; - parserRegex?: RegExp; - evalposition?: { [key: string]: ValuePosition }; - isIslamic?: boolean; - culture?: string; -} - -/** - * Interface for the date options - */ -interface DateParts { - month?: number; - day?: number; - year?: number; - hour?: number; - minute?: number; - second?: number; - designator?: string; - timeZone?: number; - hour12?: boolean; -} - -const month: string = 'months'; - -/** - * Interface for value position - */ -interface ValuePosition { - isNumber: boolean; - pos: number; - hourOnly?: boolean; -} - -export interface IDateParser { - /** - * Returns the parser function for given skeleton. - * - * @private - * @param {string} culture - Specifies the culture name to be which formatting. - * @param {DateFormatOptions} option - Specific the format in which string date will be parsed. - * @param {Object} cldr - Specifies the global cldr data collection. - * @returns {Function} ? - */ - dateParser: (culture: string, option: DateFormatOptions, cldr: Object) => Function; - /** - * Returns date object for provided date options. - * - * @private - * @param {DateParts} options ? - * @param {Date} value ? - * @returns {Date} ? - */ - getDateObject: (options: DateParts, value?: Date) => Date; - /** - * Returns date parsing options for provided value along with parse and numeric options. - * - * @private - * @param {string} value ? - * @param {ParseOptions} parseOptions ? - * @param {NumericOptions} num ? - * @returns {DateParts} ? - */ - internalDateParse: (value: string, parseOptions: ParseOptions, num: NumericOptions) => DateParts; - /** - * Returns parsed number for provided Numeric string and Numeric Options. - * - * @private - * @param {string} value ? - * @param {NumericOptions} option ? - * @returns {number} ? - */ - internalNumberParser: (value: string, option: NumericOptions) => number; - /** - * Returns parsed time zone RegExp for provided hour format and time zone. - * - * @private - * @param {string} hourFormat ? - * @param {TimeZoneOptions} tZone ? - * @param {string} nRegex ? - * @returns {string} ? - */ - parseTimeZoneRegx: (hourFormat: string, tZone: TimeZoneOptions, nRegex: string) => string; - /** - * Returns zone based value. - * - * @private - * @param {boolean} flag ? - * @param {string} val1 ? - * @param {string} val2 ? - * @param {NumericOptions} num ? - * @returns {number} ? - */ - getZoneValue: (flag: boolean, val1: string, val2: string, num: NumericOptions) => number; -} - -/** - * Custom function for date parsing. - */ -export const DateParser: IDateParser = (() => { -/** - * Returns the parser function for given skeleton. - * - * @param {string} culture - Specifies the culture name for formatting. - * @param {DateFormatOptions} option - Specifies the format in which string date will be parsed. - * @param {Object} cldr - Specifies the global cldr data collection. - * @returns {Function} - Returns a function that can parse dates. - */ - function dateParser(culture: string, option: DateFormatOptions, cldr: Object): Function { - const dependable: Dependables = base.getDependables(cldr, culture, option.calendar); - const numOptions: NumericOptions = parser.getCurrentNumericOptions( - dependable.parserObject, - parser.getNumberingSystem(cldr), - false - ); - let parseOptions: ParseOptions = {}; - const resPattern: string = option.format || - base.getResultantPattern(option.skeleton, dependable.dateObject, option.type, false); - let regexString: string = ''; - let hourOnly: boolean; - - if (isUndefined(resPattern)) { - throwError('Format options or type given must be invalid'); - } else { - parseOptions = { - isIslamic: base.islamicRegex.test(option.calendar), - pattern: resPattern, - evalposition: {}, - culture: culture - }; - const patternMatch: string[] = resPattern.match(base.dateParseRegex) || []; - const length: number = patternMatch.length; - let gmtCorrection: number = 0; - let zCorrectTemp: number = 0; - let isgmtTraversed: boolean = false; - const nRegx: string = numOptions.numericRegex; - const numMapper: NumberMapper = parser.getNumberMapper(dependable.parserObject, parser.getNumberingSystem(cldr)); - - for (let i: number = 0; i < length; i++) { - const str: string = patternMatch[parseInt(i.toString(), 10)]; - const len: number = str.length; - const char: string = (str[0] === 'K') ? 'h' : str[0]; - let isNumber: boolean; - let canUpdate: boolean; - const charKey: keyof typeof datePartMatcher = datePartMatcher[`${char}`] as string; - const optional: string = (len === 2) ? '' : '?'; - - if (isgmtTraversed) { - gmtCorrection = zCorrectTemp; - isgmtTraversed = false; - } - - switch (char) { - case 'E': - case 'c': { - const weekData: Object = (dependable.dateObject)[`${base.days}`][`${standalone}`][(base).monthIndex[`${len}`]]; - const weekObject: Object = parser.reverseObject(weekData); - regexString += '(' + Object.keys(weekObject).join('|') + ')'; - break; - } - case 'M': - case 'L': - case 'd': - case 'm': - case 's': - case 'h': - case 'H': - case 'f': - canUpdate = true; - if ((char === 'M' || char === 'L') && len > 2) { - const monthData: Object = (dependable).dateObject[`${month}`][`${standalone}`][(base).monthIndex[`${len}`]]; - (parseOptions)[`${charKey}`] = parser.reverseObject(monthData); - regexString += '(' + Object.keys((parseOptions)[`${charKey}`]).join('|') + ')'; - } else if (char === 'f') { - if (len > 3) { - continue; - } - isNumber = true; - regexString += '(' + nRegx + nRegx + '?' + nRegx + '?' + ')'; - } else { - isNumber = true; - regexString += '(' + nRegx + nRegx + optional + ')'; - } - if (char === 'h') { - parseOptions.hour12 = true; - } - break; - case 'W': { - const opt: string = len === 1 ? '?' : ''; - regexString += '(' + nRegx + opt + nRegx + ')'; - break; - } - case 'y': - canUpdate = isNumber = true; - if (len === 2) { - regexString += '(' + nRegx + nRegx + ')'; - } else { - regexString += '(' + nRegx + '{' + len + ',})'; - } - break; - case 'a': { - canUpdate = true; - const periodValue: Object = getValue('dayPeriods.format.wide', dependable.dateObject); - (parseOptions)[`${charKey}`] = parser.reverseObject(periodValue); - regexString += '(' + Object.keys((parseOptions)[`${charKey}`]).join('|') + ')'; - break; - } - case 'G': { - canUpdate = true; - const eText: string = (len <= 3) ? 'eraAbbr' : (len === 4) ? 'eraNames' : 'eraNarrow'; - (parseOptions)[`${charKey}`] = parser.reverseObject(getValue('eras.' + eText, dependable.dateObject)); - regexString += '(' + Object.keys((parseOptions)[`${charKey}`]).join('|') + '?)'; - break; - } - case 'z': { - const tval: number = new Date().getTimezoneOffset(); - canUpdate = (tval !== 0); - (parseOptions)[`${charKey}`] = getValue('dates.timeZoneNames', dependable.parserObject); - const tzone: TimeZoneOptions = (parseOptions)[`${charKey}`]; - hourOnly = (len < 4); - let hpattern: string = hourOnly ? '+H;-H' : tzone.hourFormat; - hpattern = hpattern.replace(/:/g, numMapper.timeSeparator); - regexString += '(' + parseTimeZoneRegx(hpattern, tzone, nRegx) + ')?'; - isgmtTraversed = true; - zCorrectTemp = hourOnly ? 6 : 12; - break; - } - case '\'': { - const iString: string = str.replace(/'/g, ''); - regexString += '(' + iString + ')?'; - break; - } - default: - regexString += '([\\D])'; - break; - } - - if (canUpdate) { - parseOptions.evalposition[`${charKey}`] = { isNumber: isNumber, pos: i + 1 + gmtCorrection, hourOnly: hourOnly }; - } - - if (i === length - 1 && !isNullOrUndefined(regexString)) { - const regExp: RegExpConstructor = RegExp; - parseOptions.parserRegex = new regExp('^' + regexString + '$', 'i'); - } - } - } - - return (value: string): Date => { - const parsedDateParts: DateParts = internalDateParse(value, parseOptions, numOptions); - if (isNullOrUndefined(parsedDateParts) || !Object.keys(parsedDateParts).length) { - return null; - } - if (parseOptions.isIslamic) { - let dobj: DateObject = {}; - let tYear: number | undefined = parsedDateParts.year; - const tDate: number | undefined = parsedDateParts.day; - const tMonth: number | undefined = parsedDateParts.month; - const ystrig: string = tYear ? (tYear + '') : ''; - const is2DigitYear: boolean = (ystrig.length === 2); - if (!tYear || !tMonth || !tDate || is2DigitYear) { - dobj = HijriParser.getHijriDate(new Date()); - } - if (is2DigitYear) { - tYear = parseInt((dobj.year + '').slice(0, 2) + ystrig, 10); - } - const dateObject: Date = HijriParser.toGregorian( - tYear || dobj.year, tMonth || dobj.month, tDate || dobj.date); - parsedDateParts.year = dateObject.getFullYear(); - parsedDateParts.month = dateObject.getMonth() + 1; - parsedDateParts.day = dateObject.getDate(); - } - return getDateObject(parsedDateParts); - }; - } - - /** - * Returns date object for provided date options. - * - * @param {DateParts} options - Specifies the date parts consisting of year, month, day, etc. - * @param {Date} [value] - Specifies the base date value to copy time parts. - * @returns {Date} - Returns the constructed date object. - */ - function getDateObject(options: DateParts, value?: Date): Date { - const res: Date = value || new Date(); - res.setMilliseconds(0); - const tKeys: string[] = ['hour', 'minute', 'second', 'milliseconds', 'month', 'day']; - let y: number | undefined = options.year; - const desig: string | undefined = options.designator; - const tzone: number | undefined = options.timeZone; - - if (y && !isUndefined(y)) { - const len: number = (y + '').length; - if (len <= 2) { - const century: number = Math.floor(res.getFullYear() / 100) * 100; - y += century; - } - res.setFullYear(y); - } - - for (const key of tKeys) { - let tValue: number = (options)[`${key}`]; - if (isUndefined(tValue) && key === 'day') { - res.setDate(1); - } - if (!isUndefined(tValue)) { - if (key === 'month') { - tValue -= 1; - if (tValue < 0 || tValue > 11) { - return new Date('invalid'); - } - const pDate: number = res.getDate(); - res.setDate(1); - (res)[(timeSetter)[`${key}`]](tValue); - const lDate: number = new Date(res.getFullYear(), tValue + 1, 0).getDate(); - res.setDate(pDate < lDate ? pDate : lDate); - } else { - if (key === 'day') { - const lastDay: number = new Date(res.getFullYear(), res.getMonth() + 1, 0).getDate(); - if ((tValue < 1 || tValue > lastDay)) { - return null; - } - } - (res)[`${(timeSetter)[`${key}`]}`](tValue); - } - } - } - - if (!isUndefined(desig)) { - const hour: number = res.getHours(); - if (desig === 'pm') { - res.setHours(hour + (hour === 12 ? 0 : 12)); - } else if (hour === 12) { - res.setHours(0); - } - } - - if (tzone && !isUndefined(tzone)) { - const tzValue: number = tzone - res.getTimezoneOffset(); - if (tzValue !== 0) { - res.setMinutes(res.getMinutes() + tzValue); - } - } - - return res; - } - - /** - * Returns date parsing options for provided value along with parse and numeric options. - * - * @param {string} value - Specifies the string value to be parsed. - * @param {ParseOptions} parseOptions - Specifies the parsing options. - * @param {NumericOptions} num - Specifies the numeric options. - * @returns {DateParts} - Returns the parsed date parts. - */ - function internalDateParse(value: string, parseOptions: ParseOptions, num: NumericOptions): DateParts { - const matches: string[] = value.match(parseOptions.parserRegex); - const retOptions: DateParts = { 'hour': 0, 'minute': 0, 'second': 0 }; - - if (isNullOrUndefined(matches)) { - return null; - } else { - const props: string[] = Object.keys(parseOptions.evalposition); - for (const prop of props) { - const curObject: ValuePosition = parseOptions.evalposition[`${prop}`]; - let matchString: string = matches[curObject.pos]; - if (curObject.isNumber) { - (retOptions)[`${prop}`] = internalNumberParser(matchString, num); - } else { - if (prop === 'timeZone' && !isUndefined(matchString)) { - const pos: number = curObject.pos; - let val: number; - const tmatch: string = matches[pos + 1]; - const flag: boolean = !isUndefined(tmatch); - if (curObject.hourOnly) { - val = getZoneValue(flag, tmatch, matches[pos + 4], num) * 60; - } else { - val = getZoneValue(flag, tmatch, matches[pos + 7], num) * 60; - val += getZoneValue(flag, matches[pos + 4], matches[pos + 10], num); - } - - if (!isNullOrUndefined(val)) { - retOptions[`${prop}`] = val; - } - } else { - const cultureOptions: string[] = ['en-US', 'en-MH', 'en-MP']; - matchString = ((prop === 'month') && (!(parseOptions).isIslamic) && ((parseOptions).culture === 'en' || (parseOptions).culture === 'en-GB' || (parseOptions).culture === 'en-US')) - ? matchString[0].toUpperCase() + matchString.substring(1).toLowerCase() : matchString; - matchString = ((prop !== 'month') && (prop === 'designator') && parseOptions.culture && (parseOptions).culture.indexOf('en-') !== -1 && cultureOptions.indexOf(parseOptions.culture) === -1) - ? matchString.toLowerCase() : matchString; - (retOptions)[`${prop}`] = (parseOptions)[`${prop}`][`${matchString}`]; - } - } - } - if (parseOptions.hour12) { - retOptions.hour12 = true; - } - } - return retOptions; - } - - /** - * Returns parsed number for provided Numeric string and Numeric Options. - * - * @param {string} value - Specifies the numeric string value to be parsed. - * @param {NumericOptions} option - Specifies the numeric options. - * @returns {number} - Returns the parsed numeric value. - */ - function internalNumberParser(value: string, option: NumericOptions): number { - value = parser.convertValueParts(value, option.numberParseRegex, option.numericPair); - if (latnRegex.test(value)) { - return +value; - } - return null; - } - - /** - * Returns parsed time zone RegExp for provided hour format and time zone. - * - * @param {string} hourFormat - Specifies the format of the hour. - * @param {TimeZoneOptions} tZone - Specifies the time zone options. - * @param {string} nRegex - Specifies the numeric regex. - * @returns {string} - Returns the timezone regular expression string. - */ - function parseTimeZoneRegx(hourFormat: string, tZone: TimeZoneOptions, nRegex: string): string { - const pattern: string = tZone.gmtFormat; - let ret: string; - const cRegex: string = '(' + nRegex + ')' + '(' + nRegex + ')'; - - ret = hourFormat.replace('+', '\\+'); - if (hourFormat.indexOf('HH') !== -1) { - ret = ret.replace(/HH|mm/g, '(' + cRegex + ')'); - } else { - ret = ret.replace(/H|m/g, '(' + cRegex + '?)'); - } - const splitStr: string[] = (ret.split(';').map((str: string): string => { - return pattern.replace('{0}', str); - })); - ret = splitStr.join('|') + '|' + tZone.gmtZeroFormat; - return ret; - } - - /** - * Returns zone based value. - * - * @param {boolean} flag - Specifies whether the value needs to be negated. - * @param {string} val1 - Specifies the first value to be parsed. - * @param {string} val2 - Specifies the second value to be parsed. - * @param {NumericOptions} num - Specifies the numeric options. - * @returns {number} - Returns the computed zone value. - */ - function getZoneValue(flag: boolean, val1: string, val2: string, num: NumericOptions): number { - const ival: string = flag ? val1 : val2; - if (!ival) { - return 0; - } - const value: number = internalNumberParser(ival, num); - if (value && flag) { - return -value; - } - return value; - } - - return { - dateParser, - getDateObject, - internalDateParse, - internalNumberParser, - parseTimeZoneRegx, - getZoneValue - }; -})(); diff --git a/components/base/src/intl/index.ts b/components/base/src/intl/index.ts deleted file mode 100644 index ae01671..0000000 --- a/components/base/src/intl/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Internationalization - */ -export * from './date-formatter'; -export * from './number-formatter'; -export * from './intl-base'; -export * from './date-parser'; -export * from './number-parser'; diff --git a/components/base/src/intl/intl-base.tsx b/components/base/src/intl/intl-base.tsx deleted file mode 100644 index ac42bb7..0000000 --- a/components/base/src/intl/intl-base.tsx +++ /dev/null @@ -1,1320 +0,0 @@ -import { NumberFormatOptions, DateFormatOptions, defaultCurrencyCode } from '../internationalization'; -import { NumericParts } from './number-parser'; -import { getValue, isNullOrUndefined, extend } from '../util'; -import { ParserBase as parser } from './parser-base'; -import { DateFormat, FormatOptions } from './date-formatter'; -import { NumberFormat, FormatParts, CommonOptions } from './number-formatter'; -import { isUndefined } from '../util'; - -/** - * Interface for NumericSkeleton structure - */ -export interface NumericSkeleton { - type?: string; - isAccount?: boolean; - fractionDigits?: number; -} - -/** - * Interface for GenericFormatOptions structure - */ -export interface GenericFormatOptions { - nData?: NegativeData; - pData?: NegativeData; - zeroData?: NegativeData; -} - -/** - * Interface for GroupSize structure - */ -export interface GroupSize { - primary?: number; - secondary?: number; -} - -/** - * Interface for NegativeData structure - * - * @private - */ -export interface NegativeData extends FormatParts { - nlead?: string; - nend?: string; - groupPattern?: string; - minimumFraction?: number; - maximumFraction?: number; -} - -/** - * Interface for Dependables structure - */ -export interface Dependables { - parserObject?: Object; - dateObject?: Object; - numericObject?: Object; -} - -/** - * Interface for TimeZoneOptions structure - */ -export interface TimeZoneOptions { - hourFormat?: string; - gmtFormat?: string; - gmtZeroFormat?: string; -} - -/** - * Interface for DateObject structure - */ -export interface DateObject { - year?: number; - month?: number; - date?: number; -} - -/** - * Interface defining the exported properties and methods in the IntlBase namespace. - */ -export interface IIntlBase { - negativeDataRegex?: RegExp; - customRegex?: RegExp; - latnParseRegex?: RegExp; - fractionRegex?: RegExp; - defaultCurrency?: string; - mapper?: string[]; - dateConverterMapper?: RegExp; - islamicRegex?: RegExp; - formatRegex?: RegExp; - currencyFormatRegex?: RegExp; - curWithoutNumberRegex?: RegExp; - dateParseRegex?: RegExp; - basicPatterns?: string[]; - defaultObject?: Object; - monthIndex?: Object; - month?: string; - days?: string; - patternMatcher?: { [key: string]: string }; - /** - * Computes the resultant date pattern. - * - * @private - * @param skeleton - Pattern skeleton. - * @param dateObject - Date-related object mapping. - * @param type - Format type. - * @param isIslamic - Islamic mode flag. - * @returns {string} The resultant pattern. - */ - getResultantPattern?( - skeleton: string, - dateObject: Object, - type: string, - isIslamic?: boolean, - ): string; - - /** - * Retrieves dependables based on the CLDR data. - * - * @private - * @param cldr - CLDR object data. - * @param culture - Culture code. - * @param mode - Calendar mode. - * @param isNumber - Flag indicating number operations. - * @returns {Dependables} Dependable objects. - */ - getDependables?( - cldr: Object, - culture: string, - mode: string, - isNumber?: boolean - ): Dependables; - - /** - * Fetches the symbol pattern for given parameters. - * - * @private - * @param type - Format type. - * @param numSystem - Number system. - * @param obj - Formatting object. - * @param isAccount - Account mode flag. - * @returns {string} Symbol pattern. - */ - getSymbolPattern?( - type: string, - numSystem: string, - obj: Object, - isAccount: boolean - ): string; - - /** - * Determines the proper numeric skeleton. - * - * @private - * @param skeleton - Skeleton string. - * @returns {NumericSkeleton} Numeric skeleton details. - */ - getProperNumericSkeleton?(skeleton: string): NumericSkeleton; - - /** - * Fetches format data for numbers. - * - * @private - * @param pattern - Number pattern. - * @param needFraction - Fraction requirement flag. - * @param cSymbol - Currency symbol. - * @param fractionOnly - Fraction data flag. - * @returns {NegativeData} Number format details. - */ - getFormatData?( - pattern: string, - needFraction: boolean, - cSymbol: string, - fractionOnly?: boolean - ): NegativeData; - - /** - * Changes the currency symbol in a given string. - * - * @private - * @param val - The value containing the currency symbol. - * @param sym - The new currency symbol. - * @returns {string} The value with the currency symbol replaced. - */ - changeCurrencySymbol?(val: string, sym: string): string; - - /** - * Retrieves the currency symbol based on the currency code. - * - * @private - * @param numericObject - The numeric object containing currency details. - * @param currencyCode - The currency code. - * @param altSymbol - Alternate symbol if applicable. - * @param ignoreCurrency - Flag to ignore currency in lookup. - * @returns {string} The currency symbol. - */ - getCurrencySymbol?( - numericObject: Object, - currencyCode: string, - altSymbol?: string, - ignoreCurrency?: boolean - ): string; - - /** - * Returns custom number format options. - * - * @private - * @param format - The custom format string. - * @param dOptions - Common options for number formatting. - * @param obj - Dependable object. - * @returns {GenericFormatOptions} Custom number format options. - */ - customFormat?( - format: string, - dOptions: CommonOptions, - obj: Dependables - ): GenericFormatOptions; - - /** - * Defines custom number format details. - * - * @private - * @param format - Format string. - * @param dOptions - Common options. - * @param numObject - Numeric object. - * @returns {NegativeData} Custom number formats. - */ - customNumberFormat?( - format: string, - dOptions?: CommonOptions, - numObject?: Object - ): NegativeData; - - /** - * Determines if a format is currency or percent type. - * - * @private - * @param parts - The parts of the format string. - * @param actual - The actual identifier. - * @param symbol - The symbol used. - * @returns {NegativeData} Formatting options. - */ - isCurrencyPercent?( - parts: string[], - actual: string, - symbol: string - ): NegativeData; - - /** - * Retrieves date separator for a given date object. - * - * @private - * @param dateObj - The date configuration object. - * @returns {string} Date separator. - */ - getDateSeparator?(dateObj: Object): string; - - /** - * Obtains the native date time format based on given options. - * - * @private - * @param culture - Cultural context. - * @param options - Date format settings. - * @param cldr - CLDR object. - * @param isExcelFormat - Flag for formatting in Excel. - * @returns {string} Compiled date time pattern. - */ - getActualDateTimeFormat?( - culture: string, - options: DateFormatOptions, - cldr?: Object, - isExcelFormat?: boolean - ): string; - - /** - * Processes symbols within a format string. - * - * @private - * @param actual - Original pattern string. - * @param option - Formatting options. - * @returns {string} Processed pattern. - */ - processSymbol?(actual: string, option: CommonOptions): string; - - /** - * Returns number format pattern for a culture. - * - * @private - * @param culture - Cultural context. - * @param options - Number format configurations. - * @param cldr - CLDR object. - * @param isExcel - Excel integration flag. - * @returns {string} Numeric pattern. - */ - getActualNumberFormat?( - culture: string, - options: NumberFormatOptions, - cldr?: Object, - isExcel?: boolean - ): string; - - /** - * Constructs pattern for fraction digits. - * - * @private - * @param minDigits - Minimum digits required. - * @param maxDigits - Maximum available digits. - * @returns {string} Pattern string with fraction digits. - */ - fractionDigitsPattern?( - pattern: string, - minDigits: number, - maxDigits?: number - ): string; - - /** - * Constructs pattern for integer digits. - * - * @private - * @param pattern - Base pattern. - * @param digits - Integer digit enforcement count. - * @returns {string} The pattern with integer digits. - */ - minimumIntegerPattern?(pattern: string, digits: number): string; - - /** - * Generates pattern to represent numeric grouping. - * - * @private - * @param pattern - Base numeric pattern. - * @returns {string} Group-patterned string. - */ - groupingPattern?(pattern: string): string; - - /** - * Returns first day of the week for the given culture. - * - * @private - * @param culture - Cultural context. - * @param cldr - CLDR data. - * @returns {number} The index number of the first day. - */ - getWeekData?(culture: string, cldr?: Object): number; - - /** - * Calculates the week number of the year for a given date. - * - * @private - * @param date - The date to calculate the week number for. - * @returns {number} The week number in the year. - */ - getWeekOfYear?(date: Date): number; -} - -/** - * Collection of methods and constants related to internationalization, date and number formatting. - */ -export const IntlBase: IIntlBase = (() => { - const regExp: RegExpConstructor = RegExp; - const props: IIntlBase = { - // eslint-disable-next-line security/detect-unsafe-regex - negativeDataRegex: /^(('[^']+'|''|[^*#@0,.E])*)(\*.)?((([#,]*[0,]*0+)(\.0*[0-9]*#*)?)|([#,]*@+#*))(E\+?0+)?(('[^']+'|''|[^*#@0,.E])*)$/, - // eslint-disable-next-line security/detect-unsafe-regex - customRegex: /^(('[^']+'|''|[^*#@0,.])*)(\*.)?((([0#,]*[0,]*[0#]*[0# ]*)(\.[0#]*)?)|([#,]*@+#*))(E\+?0+)?(('[^']+'|''|[^*#@0,.E])*)$/, - latnParseRegex: /0|1|2|3|4|5|6|7|8|9/g, - defaultCurrency: '$', - dateConverterMapper: /dddd|ddd/ig, - islamicRegex: /^islamic/, - formatRegex: new regExp('(^[ncpae]{1})([0-1]?[0-9]|20)?$', 'i'), - currencyFormatRegex: new regExp('(^[ca]{1})([0-1]?[0-9]|20)?$', 'i'), - curWithoutNumberRegex: /(c|a)$/ig, - dateParseRegex: /([a-z])\1*|'([^']|'')+'|''|./gi, - basicPatterns: ['short', 'medium', 'long', 'full'] - }; - const fractionRegex: RegExp = /[0-9]/g; - const mapper: string[] = ['infinity', 'nan', 'group', 'decimal']; - const patternRegex: RegExp = /G|M|L|H|c|'| a|yy|y|EEEE|E/g; - const patternMatch: object = { - 'G': '', - 'M': 'm', - 'L': 'm', - 'H': 'h', - 'c': 'd', - '\'': '"', - ' a': ' AM/PM', - 'yy': 'yy', - 'y': 'yyyy', - 'EEEE': 'dddd', - 'E': 'ddd' - }; - const defaultFirstDay: string = 'sun'; - const firstDayMapper: object = { - 'sun': 0, - 'mon': 1, - 'tue': 2, - 'wed': 3, - 'thu': 4, - 'fri': 5, - 'sat': 6 - }; - const typeMapper: object = { - '$': 'isCurrency', - '%': 'isPercent', - '-': 'isNegative', - 0: 'nlead', - 1: 'nend' - }; - props.defaultObject = { - 'dates': { - 'calendars': { - 'gregorian': { - 'months': { - 'stand-alone': { - 'abbreviated': { - '1': 'Jan', - '2': 'Feb', - '3': 'Mar', - '4': 'Apr', - '5': 'May', - '6': 'Jun', - '7': 'Jul', - '8': 'Aug', - '9': 'Sep', - '10': 'Oct', - '11': 'Nov', - '12': 'Dec' - }, - 'narrow': { - '1': 'J', - '2': 'F', - '3': 'M', - '4': 'A', - '5': 'M', - '6': 'J', - '7': 'J', - '8': 'A', - '9': 'S', - '10': 'O', - '11': 'N', - '12': 'D' - }, - 'wide': { - '1': 'January', - '2': 'February', - '3': 'March', - '4': 'April', - '5': 'May', - '6': 'June', - '7': 'July', - '8': 'August', - '9': 'September', - '10': 'October', - '11': 'November', - '12': 'December' - } - } - }, - 'days': { - 'stand-alone': { - 'abbreviated': { - 'sun': 'Sun', - 'mon': 'Mon', - 'tue': 'Tue', - 'wed': 'Wed', - 'thu': 'Thu', - 'fri': 'Fri', - 'sat': 'Sat' - }, - 'narrow': { - 'sun': 'S', - 'mon': 'M', - 'tue': 'T', - 'wed': 'W', - 'thu': 'T', - 'fri': 'F', - 'sat': 'S' - }, - 'short': { - 'sun': 'Su', - 'mon': 'Mo', - 'tue': 'Tu', - 'wed': 'We', - 'thu': 'Th', - 'fri': 'Fr', - 'sat': 'Sa' - }, - 'wide': { - 'sun': 'Sunday', - 'mon': 'Monday', - 'tue': 'Tuesday', - 'wed': 'Wednesday', - 'thu': 'Thursday', - 'fri': 'Friday', - 'sat': 'Saturday' - } - } - }, - 'dayPeriods': { - 'format': { - 'wide': { - 'am': 'AM', - 'pm': 'PM' - } - } - }, - 'eras': { - 'eraNames': { - '0': 'Before Christ', - '0-alt-variant': 'Before Common Era', - '1': 'Anno Domini', - '1-alt-variant': 'Common Era' - }, - 'eraAbbr': { - '0': 'BC', - '0-alt-variant': 'BCE', - '1': 'AD', - '1-alt-variant': 'CE' - }, - 'eraNarrow': { - '0': 'B', - '0-alt-variant': 'BCE', - '1': 'A', - '1-alt-variant': 'CE' - } - }, - 'dateFormats': { - 'full': 'EEEE, MMMM d, y', - 'long': 'MMMM d, y', - 'medium': 'MMM d, y', - 'short': 'M/d/yy' - }, - 'timeFormats': { - 'full': 'h:mm:ss a zzzz', - 'long': 'h:mm:ss a z', - 'medium': 'h:mm:ss a', - 'short': 'h:mm a' - }, - 'dateTimeFormats': { - 'full': '{1} \'at\' {0}', - 'long': '{1} \'at\' {0}', - 'medium': '{1}, {0}', - 'short': '{1}, {0}', - 'availableFormats': { - 'd': 'd', - 'E': 'ccc', - 'Ed': 'd E', - 'Ehm': 'E h:mm a', - 'EHm': 'E HH:mm', - 'Ehms': 'E h:mm:ss a', - 'EHms': 'E HH:mm:ss', - 'Gy': 'y G', - 'GyMMM': 'MMM y G', - 'GyMMMd': 'MMM d, y G', - 'GyMMMEd': 'E, MMM d, y G', - 'h': 'h a', - 'H': 'HH', - 'hm': 'h:mm a', - 'Hm': 'HH:mm', - 'hms': 'h:mm:ss a', - 'Hms': 'HH:mm:ss', - 'hmsv': 'h:mm:ss a v', - 'Hmsv': 'HH:mm:ss v', - 'hmv': 'h:mm a v', - 'Hmv': 'HH:mm v', - 'M': 'L', - 'Md': 'M/d', - 'MEd': 'E, M/d', - 'MMM': 'LLL', - 'MMMd': 'MMM d', - 'MMMEd': 'E, MMM d', - 'MMMMd': 'MMMM d', - 'ms': 'mm:ss', - 'y': 'y', - 'yM': 'M/y', - 'yMd': 'M/d/y', - 'yMEd': 'E, M/d/y', - 'yMMM': 'MMM y', - 'yMMMd': 'MMM d, y', - 'yMMMEd': 'E, MMM d, y', - 'yMMMM': 'MMMM y' - } - } - }, - 'islamic': { - 'months': { - 'stand-alone': { - 'abbreviated': { - '1': 'Muh.', - '2': 'Saf.', - '3': 'Rab. I', - '4': 'Rab. II', - '5': 'Jum. I', - '6': 'Jum. II', - '7': 'Raj.', - '8': 'Sha.', - '9': 'Ram.', - '10': 'Shaw.', - '11': 'Dhuʻl-Q.', - '12': 'Dhuʻl-H.' - }, - 'narrow': { - '1': '1', - '2': '2', - '3': '3', - '4': '4', - '5': '5', - '6': '6', - '7': '7', - '8': '8', - '9': '9', - '10': '10', - '11': '11', - '12': '12' - }, - 'wide': { - '1': 'Muharram', - '2': 'Safar', - '3': 'Rabiʻ I', - '4': 'Rabiʻ II', - '5': 'Jumada I', - '6': 'Jumada II', - '7': 'Rajab', - '8': 'Shaʻban', - '9': 'Ramadan', - '10': 'Shawwal', - '11': 'Dhuʻl-Qiʻdah', - '12': 'Dhuʻl-Hijjah' - } - } - }, - 'days': { - 'stand-alone': { - 'abbreviated': { - 'sun': 'Sun', - 'mon': 'Mon', - 'tue': 'Tue', - 'wed': 'Wed', - 'thu': 'Thu', - 'fri': 'Fri', - 'sat': 'Sat' - }, - 'narrow': { - 'sun': 'S', - 'mon': 'M', - 'tue': 'T', - 'wed': 'W', - 'thu': 'T', - 'fri': 'F', - 'sat': 'S' - }, - 'short': { - 'sun': 'Su', - 'mon': 'Mo', - 'tue': 'Tu', - 'wed': 'We', - 'thu': 'Th', - 'fri': 'Fr', - 'sat': 'Sa' - }, - 'wide': { - 'sun': 'Sunday', - 'mon': 'Monday', - 'tue': 'Tuesday', - 'wed': 'Wednesday', - 'thu': 'Thursday', - 'fri': 'Friday', - 'sat': 'Saturday' - } - } - }, - 'dayPeriods': { - 'format': { - 'wide': { - 'am': 'AM', - 'pm': 'PM' - } - } - }, - 'eras': { - 'eraNames': { - '0': 'AH' - }, - 'eraAbbr': { - '0': 'AH' - }, - 'eraNarrow': { - '0': 'AH' - } - }, - 'dateFormats': { - 'full': 'EEEE, MMMM d, y G', - 'long': 'MMMM d, y G', - 'medium': 'MMM d, y G', - 'short': 'M/d/y GGGGG' - }, - 'timeFormats': { - 'full': 'h:mm:ss a zzzz', - 'long': 'h:mm:ss a z', - 'medium': 'h:mm:ss a', - 'short': 'h:mm a' - }, - 'dateTimeFormats': { - 'full': '{1} \'at\' {0}', - 'long': '{1} \'at\' {0}', - 'medium': '{1}, {0}', - 'short': '{1}, {0}', - 'availableFormats': { - 'd': 'd', - 'E': 'ccc', - 'Ed': 'd E', - 'Ehm': 'E h:mm a', - 'EHm': 'E HH:mm', - 'Ehms': 'E h:mm:ss a', - 'EHms': 'E HH:mm:ss', - 'Gy': 'y G', - 'GyMMM': 'MMM y G', - 'GyMMMd': 'MMM d, y G', - 'GyMMMEd': 'E, MMM d, y G', - 'h': 'h a', - 'H': 'HH', - 'hm': 'h:mm a', - 'Hm': 'HH:mm', - 'hms': 'h:mm:ss a', - 'Hms': 'HH:mm:ss', - 'M': 'L', - 'Md': 'M/d', - 'MEd': 'E, M/d', - 'MMM': 'LLL', - 'MMMd': 'MMM d', - 'MMMEd': 'E, MMM d', - 'MMMMd': 'MMMM d', - 'ms': 'mm:ss', - 'y': 'y G', - 'yyyy': 'y G', - 'yyyyM': 'M/y GGGGG', - 'yyyyMd': 'M/d/y GGGGG', - 'yyyyMEd': 'E, M/d/y GGGGG', - 'yyyyMMM': 'MMM y G', - 'yyyyMMMd': 'MMM d, y G', - 'yyyyMMMEd': 'E, MMM d, y G', - 'yyyyMMMM': 'MMMM y G', - 'yyyyQQQ': 'QQQ y G', - 'yyyyQQQQ': 'QQQQ y G' - } - } - } - }, - 'timeZoneNames': { - 'hourFormat': '+HH:mm;-HH:mm', - 'gmtFormat': 'GMT{0}', - 'gmtZeroFormat': 'GMT' - } - }, - 'numbers': { - 'currencies': { - 'USD': { - 'displayName': 'US Dollar', - 'symbol': '$', - 'symbol-alt-narrow': '$' - }, - 'EUR': { - 'displayName': 'Euro', - 'symbol': '€', - 'symbol-alt-narrow': '€' - }, - 'GBP': { - 'displayName': 'British Pound', - 'symbol-alt-narrow': '£' - } - }, - 'defaultNumberingSystem': 'latn', - 'minimumGroupingDigits': '1', - 'symbols-numberSystem-latn': { - 'decimal': '.', - 'group': ',', - 'list': ';', - 'percentSign': '%', - 'plusSign': '+', - 'minusSign': '-', - 'exponential': 'E', - 'superscriptingExponent': '×', - 'perMille': '‰', - 'infinity': '∞', - 'nan': 'NaN', - 'timeSeparator': ':' - }, - 'decimalFormats-numberSystem-latn': { - 'standard': '#,##0.###' - }, - 'percentFormats-numberSystem-latn': { - 'standard': '#,##0%' - }, - 'currencyFormats-numberSystem-latn': { - 'standard': '¤#,##0.00', - 'accounting': '¤#,##0.00;(¤#,##0.00)' - }, - 'scientificFormats-numberSystem-latn': { - 'standard': '#E0' - } - } - }; - props.monthIndex = { - 3: 'abbreviated', - 4: 'wide', - 5: 'narrow', - 1: 'abbreviated' - }; - props.month = 'months'; - props.days = 'days'; - props.patternMatcher = { - C: 'currency', - P: 'percent', - N: 'decimal', - A: 'currency', - E: 'scientific' - }; - - /** - * Returns the resultant pattern based on the skeleton, dateObject, and the type provided. - * - * @param {string} skeleton ? - * @param {Object} dateObject ? - * @param {string} type ? - * @returns {string} Resultant pattern. - */ - props.getResultantPattern = ( - skeleton: string, dateObject: Object, type: string): string => { - let resPattern: string; - const iType: string = type || 'date'; - if (props.basicPatterns.indexOf(skeleton) !== -1) { - resPattern = getValue(iType + 'Formats.' + skeleton, dateObject); - if (iType === 'dateTime') { - const dPattern: string = getValue('dateFormats.' + skeleton, dateObject); - const tPattern: string = getValue('timeFormats.' + skeleton, dateObject); - resPattern = resPattern.replace('{1}', dPattern).replace('{0}', tPattern); - } - } else { - resPattern = getValue('dateTimeFormats.availableFormats.' + skeleton, dateObject); - } - if (isUndefined(resPattern) && skeleton === 'yMd') { - resPattern = 'M/d/y'; - } - return resPattern; - }; - - /** - * Returns the dependable object for provided cldr data and culture. - * - * @param {Object} cldr ? - * @param {string} culture ? - * @param {string} mode ? - * @param {boolean} isNumber ? - * @returns {Dependables} Dependable object. - */ - props.getDependables = (cldr: Object, culture: string, mode: string, isNumber?: boolean): Dependables => { - const ret: Dependables = {}; - const calendartype: string = mode || 'gregorian'; - ret.parserObject = parser.getMainObject(cldr, culture) || (IntlBase.defaultObject); - if (isNumber) { - ret.numericObject = getValue('numbers', ret.parserObject); - } else { - const dateString: string = ('dates.calendars.' + calendartype); - ret.dateObject = getValue(dateString, ret.parserObject); - } - return ret; - }; - - /** - * Returns the symbol pattern for provided parameters. - * - * @param {string} type ? - * @param {string} numSystem ? - * @param {Object} obj ? - * @param {boolean} isAccount ? - * @returns {string} Symbol pattern. - */ - props.getSymbolPattern = (type: string, numSystem: string, obj: Object, isAccount: boolean): string => { - return getValue( - type + 'Formats-numberSystem-' + numSystem + (isAccount ? '.accounting' : '.standard'), - obj - ) || (isAccount ? getValue( - type + 'Formats-numberSystem-' + numSystem + '.standard', - obj - ) : ''); - }; - - /** - * Returns the proper numeric skeleton. - * - * @param {string} skeleton ? - * @returns {NumericSkeleton} Numeric skeleton. - */ - props.getProperNumericSkeleton = (skeleton: string): NumericSkeleton => { - const matches: RegExpMatchArray | null = skeleton.match(props.formatRegex); - const ret: NumericSkeleton = {}; - const pattern: string = matches ? matches[1].toUpperCase() : ''; - ret.isAccount = (pattern === 'A'); - ret.type = props.patternMatcher[`${pattern}`]; - if (matches && skeleton.length > 1 && matches[2]) { - ret.fractionDigits = parseInt(matches[2], 10); - } - return ret; - }; - - /** - * Returns format data for number formatting. - * - * @param {string} pattern ? - * @param {boolean} needFraction ? - * @param {string} cSymbol ? - * @param {boolean} fractionOnly ? - * @returns {NegativeData} Format data. - */ - props.getFormatData = (pattern: string, needFraction: boolean, cSymbol: string, fractionOnly?: boolean): NegativeData => { - const nData: NegativeData = fractionOnly ? {} : { nlead: '', nend: '' }; - const match: string[] | null = pattern.match(props.customRegex); - if (match) { - if (!fractionOnly) { - nData.nlead = props.changeCurrencySymbol(match[1], cSymbol); - nData.nend = props.changeCurrencySymbol(match[10], cSymbol); - nData.groupPattern = match[4]; - } - const fraction: string = match[7]; - if (fraction && needFraction) { - const fmatch: string[] | null = fraction.match(fractionRegex); - nData.minimumFraction = fmatch ? fmatch.length : 0; - nData.maximumFraction = fraction.length - 1; - } - } - return nData; - }; - - /** - * Changes currency symbol. - * - * @param {string} val ? - * @param {string} sym ? - * @returns {string} Changed symbol. - */ - props.changeCurrencySymbol = (val: string, sym: string): string => { - if (val) { - val = val.replace(props.defaultCurrency, sym); - return (sym === '') ? val.trim() : val; - } - return ''; - }; - - /** - * Returns currency symbol based on currency code. - * - * @param {Object} numericObject ? - * @param {string} currencyCode ? - * @param {string} altSymbol ? - * @param {string} ignoreCurrency ? - * @returns {string} Currency symbol. - */ - props.getCurrencySymbol = ( - numericObject: Object, currencyCode: string, altSymbol?: string, ignoreCurrency?: boolean - ): string => { - const symbol: string = altSymbol ? ('.' + altSymbol) : '.symbol'; - const getCurrency: string = ignoreCurrency ? '$' - : getValue('currencies.' + currencyCode + symbol, numericObject) - || getValue('currencies.' + currencyCode + '.symbol-alt-narrow', numericObject) - || '$'; - return getCurrency; - }; - - /** - * Returns formatting options for custom number format. - * - * @param {string} format ? - * @param {CommonOptions} dOptions ? - * @param {Dependables} obj ? - * @returns {GenericFormatOptions} Custom format options. - */ - props.customFormat = (format: string, dOptions: CommonOptions, obj: Dependables): GenericFormatOptions => { - const options: GenericFormatOptions = {}; - const formatSplit: string[] = format.split(';'); - const data: string[] = ['pData', 'nData', 'zeroData']; - for (let i: number = 0; i < formatSplit.length; i++) { - options[data[parseInt(i.toString(), 10)]] = props.customNumberFormat(formatSplit[parseInt(i.toString(), 10)], dOptions, obj); - } - if (isNullOrUndefined(options.nData)) { - options.nData = extend({}, options.pData); - options.nData.nlead = (dOptions?.minusSymbol || '-') + options.nData.nlead; - } - return options; - }; - - /** - * Returns custom formatting options. - * - * @param {string} format ? - * @param {CommonOptions} dOptions ? - * @param {Object} numObject ? - * @returns {NegativeData} Custom number format. - */ - props.customNumberFormat = (format: string, dOptions?: CommonOptions, numObject?: Object): NegativeData => { - const cOptions: NegativeData = { type: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 }; - const pattern: string[] | null = format.match(props.customRegex); - if (isNullOrUndefined(pattern) || (pattern[5] === '' && format !== 'N/A')) { - cOptions.type = undefined; - return cOptions; - } - cOptions.nlead = pattern[1]; - cOptions.nend = pattern[10]; - let integerPart: string = pattern[6]; - const spaceCapture: boolean = integerPart.match(/ $/g) ? true : false; - const spaceGrouping: boolean = integerPart.replace(/ $/g, '').indexOf(' ') !== -1; - cOptions.useGrouping = integerPart.indexOf(',') !== -1 || spaceGrouping; - integerPart = integerPart.replace(/,/g, ''); - const fractionPart: string = pattern[7]; - if (integerPart.indexOf('0') !== -1) { - cOptions.minimumIntegerDigits = integerPart.length - integerPart.indexOf('0'); - } - if (!isNullOrUndefined(fractionPart)) { - cOptions.minimumFractionDigits = fractionPart.lastIndexOf('0'); - cOptions.maximumFractionDigits = fractionPart.lastIndexOf('#'); - if (cOptions.minimumFractionDigits === -1) { - cOptions.minimumFractionDigits = 0; - } - if (cOptions.maximumFractionDigits === -1 || cOptions.maximumFractionDigits < cOptions.minimumFractionDigits) { - cOptions.maximumFractionDigits = cOptions.minimumFractionDigits; - } - } - if (!isNullOrUndefined(dOptions)) { - dOptions.isCustomFormat = true; - extend(cOptions, props.isCurrencyPercent([cOptions.nlead, cOptions.nend], '$', dOptions.currencySymbol)); - if (!cOptions.isCurrency) { - extend(cOptions, props.isCurrencyPercent([cOptions.nlead, cOptions.nend], '%', dOptions.percentSymbol)); - } - } else { - extend(cOptions, props.isCurrencyPercent([cOptions.nlead, cOptions.nend], '%', '%')); - } - if (!isNullOrUndefined(numObject)) { - const symbolPattern: string = props.getSymbolPattern( - cOptions.type, dOptions.numberMapper.numberSystem, numObject, false); - if (cOptions.useGrouping) { - cOptions.groupSeparator = spaceGrouping ? ' ' : dOptions.numberMapper.numberSymbols[mapper[2]]; - cOptions.groupData = NumberFormat.getGroupingDetails(symbolPattern.split(';')[0]); - } - cOptions.nlead = cOptions.nlead.replace(/'/g, ''); - cOptions.nend = spaceCapture ? ' ' + cOptions.nend.replace(/'/g, '') : cOptions.nend.replace(/'/g, ''); - } - return cOptions; - }; - - /** - * Evaluates if formatting applies to currency or percent type. - * - * @param {string[]} parts ? - * @param {string} actual ? - * @param {string} symbol ? - * @returns {NegativeData} Information on currency or percent formatting. - */ - props.isCurrencyPercent = (parts: string[], actual: string, symbol: string): NegativeData => { - const options: NegativeData = { nlead: parts[0], nend: parts[1] }; - for (let i: number = 0; i < 2; i++) { - const part: string = parts[parseInt(i.toString(), 10)]; - const loc: number = part.indexOf(actual); - if ((loc !== -1) && ((loc < part.indexOf('\'')) || (loc > part.lastIndexOf('\'')))) { - options[typeMapper[parseInt(i.toString(), 10)]] = part.substr(0, loc) + symbol + part.substr(loc + 1); - options[typeMapper[`${actual}`]] = true; - options.type = options.isCurrency ? 'currency' : 'percent'; - break; - } - } - return options; - }; - - /** - * Returns culture-based date separator. - * - * @param {Object} dateObj ? - * @returns {string} Date separator. - */ - props.getDateSeparator = (dateObj: Object): string => { - const value: string[] = (getValue('dateFormats.short', dateObj) || '').match(/[dM]([^dM])[dM]/i); - return value ? value[1] : '/'; - }; - - /** - * Returns native date time pattern. - * - * @param {string} culture ? - * @param {DateFormatOptions} options ? - * @param {Object} cldr ? - * @param {boolean} isExcelFormat ? - * @returns {string} Actual date time pattern. - */ - props.getActualDateTimeFormat = ( - culture: string, options: DateFormatOptions, cldr?: Object, isExcelFormat?: boolean - ): string => { - const dependable: Dependables = props.getDependables(cldr, culture, options.calendar); - let actualPattern: string = options.format || props.getResultantPattern(options.skeleton, dependable.dateObject, options.type); - if (isExcelFormat) { - actualPattern = actualPattern.replace(patternRegex, (pattern: string): string => { - return patternMatch[`${pattern}`]; - }); - if (actualPattern.indexOf('z') !== -1) { - const tLength: number = actualPattern.match(/z/g).length || 0; - let timeZonePattern: string; - const formatOptions: FormatOptions = { timeZone: {} }; - formatOptions.numMapper = parser.getNumberMapper(dependable.parserObject, parser.getNumberingSystem(cldr)); - formatOptions.timeZone = getValue('dates.timeZoneNames', dependable.parserObject); - const value: Date = new Date(); - const timezone: number = value.getTimezoneOffset(); - let pattern: string = (tLength < 4) ? '+H;-H' : formatOptions.timeZone.hourFormat; - pattern = pattern.replace(/:/g, formatOptions.numMapper.timeSeparator); - if (timezone === 0) { - timeZonePattern = formatOptions.timeZone.gmtZeroFormat; - } else { - timeZonePattern = DateFormat.getTimeZoneValue(timezone, pattern); - timeZonePattern = formatOptions.timeZone.gmtFormat.replace(/\{0\}/, timeZonePattern); - } - actualPattern = actualPattern.replace(/[z]+/, '"' + timeZonePattern + '"'); - } - actualPattern = actualPattern.replace(/ $/, ''); - } - return actualPattern; - }; - - /** - * Processes symbols in format. - * - * @param {string} actual ? - * @param {CommonOptions} option ? - * @returns {string} Processed symbols. - */ - props.processSymbol = (actual: string, option: CommonOptions): string => { - if (actual.indexOf(',') !== -1) { - const split: string[] = actual.split(','); - actual = (split[0] + getValue('numberMapper.numberSymbols.group', option) + - split[1].replace('.', getValue('numberMapper.numberSymbols.decimal', option))); - } else { - actual = actual.replace('.', getValue('numberMapper.numberSymbols.decimal', option)); - } - return actual; - }; - - /** - * Returns native number pattern. - * - * @param {string} culture ? - * @param {NumberFormatOptions} options ? - * @param {Object} cldr ? - * @param {boolean} isExcel ? - * @returns {string} Actual number format. - */ - props.getActualNumberFormat = ( - culture: string, options: NumberFormatOptions, cldr?: Object, isExcel?: boolean - ): string => { - const dependable: Dependables = props.getDependables(cldr, culture, '', true); - const parseOptions: NumericParts = { custom: true }; - let minFrac: number; - const curObj: GenericFormatOptions & { hasNegativePattern?: boolean } = {}; - const curMatch: string[] | null = (options.format)?.match(props.currencyFormatRegex); - const dOptions: CommonOptions = {}; - if (curMatch) { - dOptions.numberMapper = parser.getNumberMapper(dependable.parserObject, parser.getNumberingSystem(cldr), true); - const curCode: string = props.getCurrencySymbol( - dependable.numericObject, - options.currency || defaultCurrencyCode, - options.altSymbol - ); - let symbolPattern: string = props.getSymbolPattern( - 'currency', dOptions.numberMapper.numberSystem, dependable.numericObject, (/a/i).test(options.format)); - symbolPattern = symbolPattern.replace(/\u00A4/g, curCode); - const split: string[] = symbolPattern.split(';'); - curObj.hasNegativePattern = (split.length > 1); - curObj.nData = props.getFormatData(split[1] || '-' + split[0], true, curCode); - curObj.pData = props.getFormatData(split[0], false, curCode); - if (!curMatch[2] && !options.minimumFractionDigits && !options.maximumFractionDigits) { - minFrac = props.getFormatData(symbolPattern.split(';')[0], true, '', true).minimumFraction; - } - } - let actualPattern: string; - if ((props.formatRegex.test(options.format)) || !(options.format)) { - extend(parseOptions, props.getProperNumericSkeleton(options.format || 'N')); - parseOptions.custom = false; - actualPattern = '###0'; - if (parseOptions.fractionDigits || options.minimumFractionDigits || options.maximumFractionDigits || minFrac) { - const defaultMinimum: number = 0; - if (parseOptions.fractionDigits) { - options.minimumFractionDigits = options.maximumFractionDigits = parseOptions.fractionDigits; - } - actualPattern = props.fractionDigitsPattern( - actualPattern, minFrac || parseOptions.fractionDigits || - options.minimumFractionDigits || defaultMinimum, - options.maximumFractionDigits || defaultMinimum); - } - if (options.minimumIntegerDigits) { - actualPattern = props.minimumIntegerPattern(actualPattern, options.minimumIntegerDigits); - } - if (options.useGrouping) { - actualPattern = props.groupingPattern(actualPattern); - } - if (parseOptions.type === 'currency') { - const cPattern: string = actualPattern; - actualPattern = curObj.pData.nlead + cPattern + curObj.pData.nend; - if (curObj.hasNegativePattern) { - actualPattern += ';' + curObj.nData.nlead + cPattern + curObj.nData.nend; - } - } - if (parseOptions.type === 'percent') { - actualPattern += ' %'; - } - } else { - actualPattern = options.format.replace(/'/g, '"'); - } - if (Object.keys(dOptions).length > 0) { - actualPattern = !isExcel ? props.processSymbol(actualPattern, dOptions) : actualPattern; - } - return actualPattern; - }; - - /** - * Constructs the pattern for fraction digits. - * - * @param {string} pattern ? - * @param {number} minDigits ? - * @param {number} maxDigits ? - * @returns {string} Pattern with fraction digits. - */ - props.fractionDigitsPattern = (pattern: string, minDigits: number, maxDigits?: number): string => { - pattern += '.'; - for (let i: number = 0; i < minDigits; i++) { - pattern += '0'; - } - if (minDigits < maxDigits) { - const diff: number = maxDigits - minDigits; - for (let j: number = 0; j < diff; j++) { - pattern += '#'; - } - } - return pattern; - }; - - /** - * Constructs the pattern for minimum integer digits. - * - * @param {string} pattern ? - * @param {number} digits ? - * @returns {string} Pattern with minimum integer digits. - */ - props.minimumIntegerPattern = (pattern: string, digits: number): string => { - const parts: string[] = pattern.split('.'); - let integer: string = ''; - for (let i: number = 0; i < digits; i++) { - integer += '0'; - } - return parts[1] ? (integer + '.' + parts[1]) : integer; - }; - - /** - * Constructs the pattern for grouping. - * - * @param {string} pattern ? - * @returns {string} Grouped pattern. - */ - props.groupingPattern = (pattern: string): string => { - const parts: string[] = pattern.split('.'); - let integerPart: string = parts[0]; - const no: number = 3 - integerPart.length % 3; - const hash: string = (no && no === 1) ? '#' : (no === 2 ? '##' : ''); - integerPart = hash + integerPart; - pattern = ''; - for (let i: number = integerPart.length - 1; i > 0; i -= 3) { - pattern = ',' + integerPart[i - 2] + integerPart[i - 1] + integerPart[parseInt(i.toString(), 10)] + pattern; - } - pattern = pattern.slice(1); - return parts[1] ? (pattern + '.' + parts[1]) : pattern; - }; - - /** - * Returns week data based on culture. - * - * @param {string} culture ? - * @param {Object} cldr ? - * @returns {number} Week data. - */ - props.getWeekData = (culture: string, cldr?: Object): number => { - let firstDay: string = defaultFirstDay; - const mapper: Object = getValue('supplemental.weekData.firstDay', cldr); - let iCulture: string = culture; - if ((/en-/).test(iCulture)) { - iCulture = iCulture.slice(3); - } - iCulture = iCulture.slice(0, 2).toUpperCase() + iCulture.substr(2); - if (mapper) { - firstDay = mapper[`${iCulture}`] || mapper[iCulture.slice(0, 2)] || defaultFirstDay; - } - return firstDayMapper[`${firstDay}`]; - }; - - /** - * Gets the week number of the year. - * - * @param {Date} date ? - * @returns {number} Week number. - */ - props.getWeekOfYear = (date: Date): number => { - const newYear: Date = new Date(date.getFullYear(), 0, 1); - let day: number = newYear.getDay(); - let weeknum: number; - day = (day >= 0 ? day : day + 7); - const daynum: number = Math.floor((date.getTime() - newYear.getTime() - - (date.getTimezoneOffset() - newYear.getTimezoneOffset()) * 60000) / 86400000) + 1; - if (day < 4) { - weeknum = Math.floor((daynum + day - 1) / 7) + 1; - if (weeknum > 52) { - const nYear: Date = new Date(date.getFullYear() + 1, 0, 1); - let nday: number = nYear.getDay(); - nday = nday >= 0 ? nday : nday + 7; - weeknum = nday < 4 ? 1 : 53; - } - } else { - weeknum = Math.floor((daynum + day - 1) / 7); - } - return weeknum; - }; - return props; -})(); diff --git a/components/base/src/intl/number-formatter.tsx b/components/base/src/intl/number-formatter.tsx deleted file mode 100644 index 829a896..0000000 --- a/components/base/src/intl/number-formatter.tsx +++ /dev/null @@ -1,456 +0,0 @@ -import { isUndefined, throwError, isNullOrUndefined, extend } from '../util'; -import { NumberFormatOptions, defaultCurrencyCode } from '../internationalization'; -import { IntlBase as base, NegativeData, GenericFormatOptions, NumericSkeleton, Dependables } from './intl-base'; -import { ParserBase as parser, NumberMapper } from './parser-base'; - -/** - * Interface for default formatting options. - * - * @private - */ -export interface FormatParts extends NumericSkeleton, NumberFormatOptions { - groupOne?: boolean; - isPercent?: boolean; - isCurrency?: boolean; - isNegative?: boolean; - groupData?: GroupDetails; - groupSeparator?: string; -} - -/** - * Interface for common formatting options. - */ -export interface CommonOptions { - numberMapper?: NumberMapper; - currencySymbol?: string; - percentSymbol?: string; - minusSymbol?: string; - isCustomFormat?: boolean; -} - -/** - * Interface for currency processing. - */ -export interface CurrencyOptions { - position?: string; - symbol?: string; - currencySpace?: boolean; -} - -/** - * Interface for grouping process. - */ -export interface GroupDetails { - primary?: number; - secondary?: number; -} - -/** - * Error text mappings for significant and fraction digits. - */ -const errorText: Object = { - 'ms': 'minimumSignificantDigits', - 'ls': 'maximumSignificantDigits', - 'mf': 'minimumFractionDigits', - 'lf': 'maximumFractionDigits' -}; - -/** - * Percent sign constant. - */ -const percentSign: string = 'percentSign'; - -/** - * Minus sign constant. - */ -const minusSign: string = 'minusSign'; - -/** - * Mapper array for numeric symbols. - */ -const mapper: string[] = ['infinity', 'nan', 'group', 'decimal', 'exponential']; - - - -/** - * Interface for public and protected properties/methods of NumberFormatObject. - */ -export interface INumberFormat { - /** - * Returns the formatter function for given skeleton. - * - * @private - * @param {string} culture - Specifies the culture name to be used for formatting. - * @param {NumberFormatOptions} option - Specifies the format in which number will be formatted. - * @param {Object} cldr - Specifies the global cldr data collection. - * @returns {Function} - */ - numberFormatter: (culture: string, option: NumberFormatOptions, cldr: Object) => Function; - /** - * Returns grouping details for the pattern provided. - * - * @private - * @param {string} pattern - * @returns {GroupDetails} - */ - getGroupingDetails: (pattern: string) => GroupDetails; -} - -/** - * Custom object structure for NumberFormat with all methods and properties. - */ -export const NumberFormat: INumberFormat = (() => { - - /** - * Returns the formatter function for given skeleton. - * - * @param {string} culture - Specifies the culture name to be used for formatting. - * @param {NumberFormatOptions} option - Specifies the format in which number will be formatted. - * @param {Object} cldr - Specifies the global cldr data collection. - * @returns {Function} Returns the formatter function for the given number format options. - */ - function numberFormatter(culture: string, option: NumberFormatOptions, cldr: Object): Function { - const fOptions: FormatParts = extend({}, option); - let cOptions: GenericFormatOptions = {}; - const dOptions: CommonOptions = {}; - let symbolPattern: string; - - const dependable: Dependables = base.getDependables(cldr, culture, '', true); - dOptions.numberMapper = parser.getNumberMapper(dependable.parserObject, parser.getNumberingSystem(cldr), true); - dOptions.currencySymbol = base.getCurrencySymbol( - dependable.numericObject, fOptions.currency || defaultCurrencyCode, option.altSymbol, option.ignoreCurrency); - dOptions.percentSymbol = (dOptions).numberMapper.numberSymbols[`${percentSign}`]; - dOptions.minusSymbol = (dOptions).numberMapper.numberSymbols[`${minusSign}`]; - - const symbols: Object = dOptions.numberMapper.numberSymbols; - if (option.format && !base.formatRegex.test(option.format)) { - cOptions = base.customFormat(option.format, dOptions, dependable.numericObject); - if (!isUndefined(fOptions.useGrouping) && fOptions.useGrouping) { - fOptions.useGrouping = cOptions.pData.useGrouping; - } - } else { - extend(fOptions, base.getProperNumericSkeleton(option.format || 'N')); - fOptions.isCurrency = fOptions.type === 'currency'; - fOptions.isPercent = fOptions.type === 'percent'; - symbolPattern = base.getSymbolPattern( - fOptions.type, dOptions.numberMapper.numberSystem, dependable.numericObject, fOptions.isAccount); - fOptions.groupOne = checkValueRange(fOptions.maximumSignificantDigits, fOptions.minimumSignificantDigits, true); - checkValueRange(fOptions.maximumFractionDigits, fOptions.minimumFractionDigits, false, true); - if (!isUndefined(fOptions.fractionDigits)) { - fOptions.minimumFractionDigits = fOptions.maximumFractionDigits = fOptions.fractionDigits; - } - if (isUndefined(fOptions.useGrouping)) { - fOptions.useGrouping = true; - } - if (fOptions.isCurrency) { - symbolPattern = symbolPattern.replace(/\u00A4/g, base.defaultCurrency); - } - const split: string[] = symbolPattern.split(';'); - cOptions.nData = base.getFormatData(split[1] || '-' + split[0], true, dOptions.currencySymbol); - cOptions.pData = base.getFormatData(split[0], false, dOptions.currencySymbol); - if (fOptions.useGrouping) { - fOptions.groupSeparator = symbols[mapper[2]]; - fOptions.groupData = getGroupingDetails(split[0]); - } - const minFrac: boolean = isUndefined(fOptions.minimumFractionDigits); - if (minFrac) { - fOptions.minimumFractionDigits = cOptions.nData.minimumFraction; - } - if (isUndefined(fOptions.maximumFractionDigits)) { - const mval: number = cOptions.nData.maximumFraction; - fOptions.maximumFractionDigits = isUndefined(mval) && fOptions.isPercent ? 0 : mval; - } - const mfrac: number = fOptions.minimumFractionDigits; - const lfrac: number = fOptions.maximumFractionDigits; - if (!isUndefined(mfrac) && !isUndefined(lfrac)) { - if (mfrac > lfrac) { - fOptions.maximumFractionDigits = mfrac; - } - } - } - extend(cOptions.nData, fOptions); - extend(cOptions.pData, fOptions); - return (value: number): string => { - if (isNaN(value)) { - return symbols[mapper[1]]; - } else if (!isFinite(value)) { - return symbols[mapper[0]]; - } - return intNumberFormatter(value, cOptions, dOptions, option); - }; - } - - /** - * Returns grouping details for the pattern provided. - * - * @param {string} pattern ? - * @returns {GroupDetails} Returns an object containing primary and secondary grouping details - */ - function getGroupingDetails(pattern: string): GroupDetails { - const ret: GroupDetails = {}; - const match: string[] | null = pattern.match(base.negativeDataRegex); - if (match && match[4]) { - const pattern: string = match[4]; - const p: number = pattern.lastIndexOf(','); - if (p !== -1) { - const temp: string = pattern.split('.')[0]; - ret.primary = (temp.length - p) - 1; - const s: number = pattern.lastIndexOf(',', p - 1); - if (s !== -1) { - ret.secondary = p - 1 - s; - } - } - } - return ret; - } - - /** - * Checks if the provided integer range is valid. - * - * @param {number} val1 ? - * @param {number} val2 ? - * @param {boolean} checkbothExist ? - * @param {boolean} isFraction ? - * @returns {boolean} ? - */ - function checkValueRange(val1: number, val2: number, checkbothExist: boolean, isFraction?: true): boolean { - const decide: string = isFraction ? 'f' : 's'; - let dint: number = 0; - const str1: string = (errorText)['l' + decide]; - const str2: string = (errorText)['m' + decide]; - if (!isUndefined(val1)) { - checkRange(val1, str1, isFraction); - dint++; - } - if (!isUndefined(val2)) { - checkRange(val2, str2, isFraction); - dint++; - } - if (dint === 2) { - if (val1 < val2) { - throwError(str2 + 'specified must be less than the' + str1); - } else { - return true; - } - } else if (checkbothExist && dint === 1) { - throwError('Both' + str2 + 'and' + str2 + 'must be present'); - } - return false; - } - - /** - * Validates if a value is within a specified range. - * - * @param {number} val ? - * @param {string} text ? - * @param {boolean} isFraction ? - * @returns {void} - */ - function checkRange(val: number, text: string, isFraction?: boolean): void { - const range: number[] = isFraction ? [0, 20] : [1, 21]; - if (val < range[0] || val > range[1]) { - throwError(text + 'value must be within the range' + range[0] + 'to' + range[1]); - } - } - - /** - * Formats numeric strings based on options. - * - * @param {number} value ? - * @param {GenericFormatOptions} fOptions ? - * @param {CommonOptions} dOptions ? - * @param {NumberFormatOptions} [option] ? - * @returns {string} ? - */ - function intNumberFormatter( - value: number, fOptions: GenericFormatOptions, - dOptions: CommonOptions, - option?: NumberFormatOptions - ): string { - let curData: NegativeData; - if (isUndefined(fOptions.nData.type)) { - return null; - } else { - if (value < 0) { - value = value * -1; - curData = fOptions.nData; - } else if (value === 0) { - curData = fOptions.zeroData || fOptions.pData; - } else { - curData = fOptions.pData; - } - let fValue: string = ''; - if (curData.isPercent) { - value = value * 100; - } - if (curData.groupOne) { - fValue = processSignificantDigits(value, curData.minimumSignificantDigits, curData.maximumSignificantDigits); - } else { - fValue = processFraction(value, curData.minimumFractionDigits, curData.maximumFractionDigits, option); - if (curData.minimumIntegerDigits) { - fValue = processMinimumIntegers(fValue, curData.minimumIntegerDigits); - } - if (dOptions.isCustomFormat && curData.minimumFractionDigits < curData.maximumFractionDigits - && /\d+\.\d+/.test(fValue)) { - const temp: string[] = fValue.split('.'); - let decimalPart: string = temp[1]; - const len: number = decimalPart.length; - for (let i: number = len - 1; i >= 0; i--) { - if (decimalPart[parseInt(i.toString(), 10)] === '0' && i >= curData.minimumFractionDigits) { - decimalPart = decimalPart.slice(0, i); - } else { - break; - } - } - fValue = temp[0] + '.' + decimalPart; - } - } - if (curData.type === 'scientific') { - fValue = value.toExponential(curData.maximumFractionDigits); - fValue = fValue.replace('e', dOptions.numberMapper.numberSymbols[mapper[4]]); - } - fValue = fValue.replace('.', (dOptions).numberMapper.numberSymbols[mapper[3]]); - fValue = curData.format === '#,###,,;(#,###,,)' ? customPivotFormat(parseInt(fValue, 10)) : fValue; - if (curData.useGrouping) { - fValue = groupNumbers( - fValue, curData.groupData.primary, curData.groupSeparator || ',', - (dOptions).numberMapper.numberSymbols[mapper[3]] || '.', curData.groupData.secondary); - } - fValue = parser.convertValueParts(fValue, base.latnParseRegex, dOptions.numberMapper.mapper); - if (curData.nlead === 'N/A') { - return curData.nlead; - } else { - if (fValue === '0' && option && option.format === '0') { - return fValue + curData.nend; - } - return curData.nlead + fValue + curData.nend; - } - } - } - - /** - * Processes significant digits for a value. - * - * @param {number} value ? - * @param {number} min ? - * @param {number} max ? - * @returns {string} ? - */ - function processSignificantDigits(value: number, min: number, max: number): string { - let temp: string = value + ''; - let tn: number; - const length: number = temp.length; - if (length < min) { - return value.toPrecision(min); - } else { - temp = value.toPrecision(max); - tn = +temp; - return tn + ''; - } - } - - /** - * Groups numeric strings based on separator and levels. - * - * @param {string} val ? - * @param {number} level1 ? - * @param {string} sep ? - * @param {string} decimalSymbol ? - * @param {number} level2 ? - * @returns {string} ? - */ - function groupNumbers(val: string, level1: number, sep: string, decimalSymbol: string, level2?: number): string { - let flag: boolean = !isNullOrUndefined(level2) && level2 !== 0; - const split: string[] = val.split(decimalSymbol); - const prefix: string = split[0]; - let length: number = prefix.length; - let str: string = ''; - while (length > level1) { - str = prefix.slice(length - level1, length) + (str.length ? - (sep + str) : ''); - length -= level1; - if (flag) { - level1 = level2; - flag = false; - } - } - split[0] = prefix.slice(0, length) + (str.length ? sep : '') + str; - return split.join(decimalSymbol); - } - - /** - * Processes the fraction part of the numeric value. - * - * @param {number} value ? - * @param {number} min ? - * @param {number} max ? - * @param {NumberFormatOptions} [option] ? - * @returns {string} ? - */ - function processFraction(value: number, min: number, max: number, option?: NumberFormatOptions): string { - const temp: string = (value + '').split('.')[1]; - const length: number = temp ? temp.length : 0; - if (min && length < min) { - let ret: string = ''; - if (length === 0) { - ret = value.toFixed(min); - } else { - ret += value; - for (let j: number = 0; j < min - length; j++) { - ret += '0'; - } - return ret; - } - return value.toFixed(min); - } else if (!isNullOrUndefined(max) && (length > max || max === 0)) { - return value.toFixed(max); - } - let str: string = value + ''; - if (str[0] === '0' && option && option.format === '###.00') { - str = str.slice(1); - } - return str; - } - - /** - * Processes integer part ensuring minimum digit count. - * - * @param {string} value ? - * @param {number} min ? - * @returns {string} ? - */ - function processMinimumIntegers(value: string, min: number): string { - const temp: string[] = value.split('.'); - let lead: string = temp[0]; - const len: number = lead.length; - if (len < min) { - for (let i: number = 0; i < min - len; i++) { - lead = '0' + lead; - } - temp[0] = lead; - } - return temp.join('.'); - } - - /** - * Formats for pivot tables specifically. - * - * @param {number} value ? - * @returns {string} ? - */ - function customPivotFormat(value: number): string { - if (value >= 500000) { - value /= 1000000; - const decimal: string | undefined = value.toString().split('.')[1]; - return decimal && +decimal.substring(0, 1) >= 5 - ? Math.ceil(value).toString() - : Math.floor(value).toString(); - } - return ''; - } - - return { - getGroupingDetails, - numberFormatter - }; -})(); diff --git a/components/base/src/intl/number-parser.tsx b/components/base/src/intl/number-parser.tsx deleted file mode 100644 index 102dcd6..0000000 --- a/components/base/src/intl/number-parser.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { NumberFormatOptions } from '../internationalization'; -import { extend, isNullOrUndefined } from '../util'; -import { ParserBase as parser, NumericOptions } from './parser-base'; -import { IntlBase as base, NegativeData, Dependables } from './intl-base'; - -const regExp: RegExpConstructor = RegExp; -const parseRegex: RegExp = new regExp('^([^0-9]*)' + '(([0-9,]*[0-9]+)(.[0-9]+)?)' + '([Ee][+-]?[0-9]+)?([^0-9]*)$'); -const groupRegex: RegExp = /,/g; - -const keys: string[] = ['minusSign', 'infinity']; - -/** - * Interface for Numeric Formatting Parts - */ -export interface NumericParts { - symbolRegex?: RegExp; - nData?: NegativeData; - pData?: NegativeData; - infinity?: string; - type?: string; - fractionDigits?: number; - isAccount?: boolean; - custom?: boolean; - maximumFractionDigits?: number; -} - -/** - * Interface for numeric parse options - */ -export interface NumberParseOptions { - parseRegex: string; - numbericMatcher: Object; -} - -/** - * Interface defining the properties and methods of the useNumberParser hook. - */ -export interface INumberParser { - numberParser: (culture: string, option: NumberFormatOptions, cldr: Object) => (value: string) => number; -} - -/** - * Custom function to parse numbers according to specified format and culture. - */ -export const NumberParser: INumberParser = (() => { - /** - * Parses a number string based on the given options and CLDR data. - * - * @param {string} culture - Specifies the culture name to be used for formatting. - * @param {NumberFormatOptions} option - Specifies the format options. - * @param {Object} cldr - Specifies the global CLDR data collection. - * @returns {Function} - Returns a function to parse the given string to a number. - */ - function numberParser(culture: string, option: NumberFormatOptions, cldr: Object): ((value: string) => number) { - const dependable: Dependables = base.getDependables(cldr, culture, '', true); - const parseOptions: NumericParts = { custom: true }; - if ((base.formatRegex.test(option.format)) || !(option.format)) { - extend(parseOptions, base.getProperNumericSkeleton(option.format || 'N')); - parseOptions.custom = false; - if (!parseOptions.fractionDigits) { - if (option.maximumFractionDigits) { - parseOptions.maximumFractionDigits = option.maximumFractionDigits; - } - } - } else { - extend(parseOptions, base.customFormat(option.format, null, null)); - } - const numOptions: NumericOptions = parser.getCurrentNumericOptions(dependable.parserObject, parser.getNumberingSystem(cldr), true); - parseOptions.symbolRegex = parser.getSymbolRegex(Object.keys(numOptions.symbolMatch)); - parseOptions.infinity = (numOptions).symbolNumberSystem[keys[1]]; - let symbolpattern: string; - symbolpattern = base.getSymbolPattern(parseOptions.type, numOptions.numberSystem, dependable.numericObject, parseOptions.isAccount); - if (symbolpattern) { - symbolpattern = symbolpattern.replace(/\u00A4/g, base.defaultCurrency); - const split: string[] = symbolpattern.split(';'); - parseOptions.nData = base.getFormatData(split[1] || '-' + split[0], true, ''); - parseOptions.pData = base.getFormatData(split[0], true, ''); - } - return (value: string): number => { - return getParsedNumber(value, parseOptions, numOptions); - }; - } - /** - * Returns parsed number for the provided formatting options. - * - * @param {string} value - The string value to be converted to number. - * @param {NumericParts} options - The numeric parts required for parsing. - * @param {NumericOptions} numOptions - Options for numeric conversion. - * @returns {number} - The parsed numeric value. - */ - function getParsedNumber(value: string, options: NumericParts, numOptions: NumericOptions): number { - let isNegative: boolean; - let isPercent: boolean; - let tempValue: string; - let lead: string; - let end: string; - let ret: number; - - if (value.indexOf(options.infinity) !== -1) { - return Infinity; - } else { - value = parser.convertValueParts(value, options.symbolRegex, numOptions.symbolMatch); - value = parser.convertValueParts(value, numOptions.numberParseRegex, numOptions.numericPair); - value = value.indexOf('-') !== -1 ? value.replace('-.', '-0.') : value; - if (value.indexOf('.') === 0) { - value = '0' + value; - } - const matches: string[] = value.match(parseRegex); - if (isNullOrUndefined(matches)) { - return NaN; - } - lead = matches[1]; - tempValue = matches[2]; - const exponent: string = matches[5]; - end = matches[6]; - isNegative = options.custom ? (lead === options.nData.nlead && end === options.nData.nend) : - (lead.indexOf(options.nData.nlead) !== -1 && end.indexOf(options.nData.nend) !== -1); - isPercent = isNegative ? options.nData.isPercent : options.pData.isPercent; - tempValue = tempValue.replace(groupRegex, ''); - if (exponent) { - tempValue += exponent; - } - ret = +tempValue; - if (options.type === 'percent' || isPercent) { - ret = ret / 100; - } - if (options.custom || options.fractionDigits) { - ret = parseFloat(ret.toFixed(options.custom ? - (isNegative ? options.nData.maximumFractionDigits : options.pData.maximumFractionDigits) : options.fractionDigits)); - } - if (options.maximumFractionDigits) { - ret = convertMaxFracDigits(tempValue, options, ret, isNegative); - } - if (isNegative) { - ret *= -1; - } - return ret; - } - } - - /** - * Adjusts the number according to the maximum fraction digits allowed. - * - * @param {string} value - The string value of the number before parsing. - * @param {NumericParts} options - Parsing options with numeric parts. - * @param {number} ret - The current numeric value. - * @param {boolean} isNegative - Flag if the number is negative. - * @returns {number} - The processed number with appropriate fraction digits. - */ - function convertMaxFracDigits(value: string, options: NumericParts, ret: number, isNegative: boolean): number { - const decimalSplitValue: string[] = value.split('.'); - if (decimalSplitValue[1] && decimalSplitValue[1].length > options.maximumFractionDigits) { - ret = +(ret.toFixed(options.custom ? - (isNegative ? options.nData.maximumFractionDigits : options.pData.maximumFractionDigits) : options.maximumFractionDigits)); - } - return ret; - } - - return { - numberParser - }; -})(); diff --git a/components/base/src/intl/parser-base.tsx b/components/base/src/intl/parser-base.tsx deleted file mode 100644 index 3713172..0000000 --- a/components/base/src/intl/parser-base.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import { isUndefined, getValue } from '../util'; - -/** - * Default numbering system for Latin script. - */ -const defaultNumberingSystem: { [key: string]: object } = { - 'latn': { - '_digits': '0123456789', - '_type': 'numeric' - } -}; - -/** - * Default symbols used in numbers representation. - */ -const defaultNumberSymbols: { [key: string]: string } = { - 'decimal': '.', - 'group': ',', - 'percentSign': '%', - 'plusSign': '+', - 'minusSign': '-', - 'infinity': '∞', - 'nan': 'NaN', - 'exponential': 'E' -}; - -/** - * Latin number system representation as array. - */ -const latnNumberSystem: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - -/** - * Interface for numeric Options. - */ -export interface NumericOptions { - numericPair?: Object; - numericRegex?: string; - numberParseRegex?: RegExp; - symbolNumberSystem?: Object; - symbolMatch?: Object; - numberSystem?: string; -} - -/** - * Interface for numeric object holding numbers system data. - */ -export interface NumericObject { - obj?: Object; - nSystem?: string; -} - -/** - * Interface for number mapper holding mapping of symbols and digits. - */ -export interface NumberMapper { - mapper?: Object; - timeSeparator?: string; - numberSymbols?: Object; - numberSystem?: string; -} - -/** - * Interface for the public and protected properties and methods in useParserBase custom object. - */ -export interface IParserBase { - nPair?: string; - nRegex?: string; - numberingSystems?: Object; - /** - * Returns the cldr object for the culture specified. - * - * @private - * @param {Object} obj - Specifies the object from which culture object to be acquired. - * @param {string} cName - Specifies the culture name. - * @returns {Object} - */ - getMainObject?(obj: Object, cName: string): Object; - /** - * Returns the numbering system object from given cldr data. - * - * @private - * @param {Object} obj - Specifies the object from which number system is acquired. - * @returns {Object} - */ - getNumberingSystem?(obj: Object): Object; - /** - * Returns the reverse of given object keys or keys specified. - * - * @private - * @param {Object} prop - Specifies the object to be reversed. - * @param {number[]} [keys] - Optional parameter specifies the custom keyList for reversal. - * @returns {Object} - */ - reverseObject?(prop: Object, keys?: number[]): Object; - /** - * Returns the symbol regex by skipping the escape sequence. - * - * @private - * @param {string[]} props - Specifies the array values to be skipped. - * @returns {RegExp} - */ - getSymbolRegex?(props: string[]): RegExp; - /** - * Returns default numbering system object for formatting from cldr data. - * - * @private - * @param {Object} obj - * @returns {NumericObject} - */ - getDefaultNumberingSystem?(obj: Object): NumericObject; - /** - * Returns the replaced value of matching regex and obj mapper. - * - * @private - * @param {string} value - Specifies the values to be replaced. - * @param {RegExp} regex - Specifies the regex to search. - * @param {Object} obj - Specifies the object matcher to be replace value parts. - * @returns {string} - */ - convertValueParts?(value: string, regex: RegExp, obj: Object): string; - /** - * Returns the replaced value of matching regex and obj mapper. - * - * @private - * @param {Object} curObj - * @param {Object} numberSystem - * @param {boolean} [needSymbols] - * @returns {NumericOptions} - */ - getCurrentNumericOptions?(curObj: Object, numberSystem: Object, needSymbols?: boolean): NumericOptions; - /** - * Returns number mapper object for the provided cldr data. - * - * @private - * @param {Object} curObj - * @param {Object} numberSystem - * @param {boolean} [isNumber] - * @returns {NumberMapper} - */ - getNumberMapper?(curObj: Object, numberSystem: Object, isNumber?: boolean): NumberMapper; -} - -/** - * Class for handling the Parser Base functionalities. - */ -export const ParserBase: IParserBase = (() => { - - const props: IParserBase = { - nPair: 'numericPair', - nRegex: 'numericRegex', - numberingSystems: defaultNumberingSystem - }; - - /** - * Returns the cldr object for the culture specified. - * - * @param {Object} obj - Specifies the object from which culture object to be acquired. - * @param {string} cName - Specifies the culture name. - * @returns {Object} ? - */ - props.getMainObject = (obj: Object, cName: string): Object => { - const value: string = 'main.' + cName; - return getValue(value, obj); - }; - - /** - * Returns the numbering system object from given cldr data. - * - * @param {Object} obj - Specifies the object from which number system is acquired. - * @returns {Object} ? - */ - props.getNumberingSystem = (obj: Object): Object => { - return getValue('supplemental.numberingSystems', obj) || props.numberingSystems; - }; - - /** - * Returns the reverse of given object keys or keys specified. - * - * @param {Object} prop - Specifies the object to be reversed. - * @param {number[]} [keys] - Optional parameter specifies the custom keyList for reversal. - * @returns {Object} ? - */ - props.reverseObject = (prop: Object, keys?: number[]): Object => { - const propKeys: string[] | number[] = keys || Object.keys(prop); - const res: Object = {}; - for (const key of propKeys) { - if (!Object.prototype.hasOwnProperty.call(res, (prop)[`${key}`])) { - (res)[(prop)[`${key}`]] = key; - } - } - return res; - }; - - /** - * Returns the symbol regex by skipping the escape sequence. - * - * @param {string[]} props - Specifies the array values to be skipped. - * @returns {RegExp} ? - */ - props.getSymbolRegex = (props: string[]): RegExp => { - const regexStr: string = props.map((str: string): string => { - return str.replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'); - }).join('|'); - const regExp: RegExpConstructor = RegExp; - return new regExp(regexStr, 'g'); - }; - - /** - * Returns default numbering system object for formatting from cldr data. - * - * @param {Object} obj ? - * @returns {NumericObject} ? - */ - function getDefaultNumberingSystem(obj: Object): NumericObject { - const ret: NumericObject = {}; - ret.obj = getValue('numbers', obj); - ret.nSystem = getValue('defaultNumberingSystem', ret.obj); - return ret; - } - - /** - * Returns the replaced value of matching regex and obj mapper. - * - * @param {string} value - Specifies the values to be replaced. - * @param {RegExp} regex - Specifies the regex to search. - * @param {Object} obj - Specifies the object matcher to be replace value parts. - * @returns {string} ? - */ - props.convertValueParts = (value: string, regex: RegExp, obj: Object): string => { - return value.replace(regex, (str: string): string => { - return (obj)[`${str}`]; - }); - }; - - /** - * Returns the replaced value of matching regex and obj mapper. - * - * @param {Object} curObj ? - * @param {Object} numberSystem ? - * @param {boolean} [needSymbols] ? - * @returns {NumericOptions} ? - */ - props.getCurrentNumericOptions = ( - curObj: Object, - numberSystem: Object, - needSymbols?: boolean - ): NumericOptions => { - const ret: NumericOptions = {}; - const cur: NumericObject = getDefaultNumberingSystem(curObj); - if (!isUndefined(cur.nSystem)) { - const digits: string = getValue(cur.nSystem + '._digits', numberSystem); - if (!isUndefined(digits)) { - ret.numericPair = props.reverseObject(digits, latnNumberSystem); - const regExp: RegExpConstructor = RegExp; - ret.numberParseRegex = new regExp(constructRegex(digits), 'g'); - ret.numericRegex = '[' + digits[0] + '-' + digits[9] + ']'; - if (needSymbols) { - ret.numericRegex = digits[0] + '-' + digits[9]; - ret.symbolNumberSystem = getValue('symbols-numberSystem-' + cur.nSystem, cur.obj - ); - ret.symbolMatch = getSymbolMatch(ret.symbolNumberSystem); - ret.numberSystem = cur.nSystem; - } - } - } - return ret; - }; - - /** - * Returns number mapper object for the provided cldr data. - * - * @param {Object} curObj ? - * @param {Object} numberSystem ? - * @returns {NumberMapper} ? - */ - props.getNumberMapper = ( - curObj: Object, - numberSystem: Object - ): NumberMapper => { - const ret: NumberMapper = { mapper: {} }; - const cur: NumericObject = getDefaultNumberingSystem(curObj); - if (!isUndefined(cur.nSystem)) { - ret.numberSystem = cur.nSystem; - ret.numberSymbols = getValue('symbols-numberSystem-' + cur.nSystem, cur.obj); - ret.timeSeparator = getValue('timeSeparator', ret.numberSymbols); - const digits: string = getValue(cur.nSystem + '._digits', numberSystem); - if (!isUndefined(digits)) { - for (const i of latnNumberSystem) { - ret.mapper[parseInt(i.toString(), 10)] = digits[parseInt(i.toString(), 10)]; - } - } - } - return ret; - }; - - /** - * Returns the symbol match for the provided object. - * - * @param {Object} prop ? - * @returns {Object} ? - */ - function getSymbolMatch(prop: Object): Object { - const matchKeys: string[] = Object.keys(defaultNumberSymbols); - const ret: Object = {}; - for (const key of matchKeys) { - (ret)[(prop)[`${key}`]] = (defaultNumberSymbols)[`${key}`]; - } - return ret; - } - - /** - * Constructs a regex string for the provided value. - * - * @param {string} val ? - * @returns {string} ? - */ - function constructRegex(val: string): string { - const len: number = val.length; - let ret: string = ''; - for (let i: number = 0; i < len; i++) { - ret += val[parseInt(i.toString(), 10)]; - if (i !== len - 1) { - ret += '|'; - } - } - return ret; - } - - return props; -})(); diff --git a/components/base/src/l10n.tsx b/components/base/src/l10n.tsx deleted file mode 100644 index 51449a3..0000000 --- a/components/base/src/l10n.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { extend, isNullOrUndefined } from './util'; -import { defaultCulture } from './internationalization'; - -/** - * L10n modules provides localized text for different culture. - */ -export interface IL10n { - /** - * Sets the locale text. - * - * @param {string} locale - * @returns {void} - */ - setLocale(locale: string): void; - - /** - * Returns current locale text for the property based on the culture name and control name. - * - * @param {string} prop - Specifies the property for which localize text to be returned. - * @returns {string} - */ - getConstant(prop: string): string; -} - -/** - * L10n modules provides localized text for different culture. - * - * @param {string} controlName - name of the control. - * @param {object} localeStrings - collection of locale string. - * @param {string} locale - default locale string. - * @returns {L10n} - Returns configured properties and methods for localization. - */ -export function L10n(controlName: string, localeStrings: Object, locale?: string): IL10n { - let currentLocale: object = {}; - - /** - * Sets the locale text. - * - * @param {string} locale - Specifies the locale to be set. - * @returns {void} - */ - function setLocale(locale: string): void { - const intLocale: Object = intGetControlConstant(L10n.locale, locale); - currentLocale = intLocale || localeStrings; - } - /** - * Returns current locale text for the property based on the culture name and control name. - * - * @param {string} prop - Specifies the property for which localized text to be returned. - * @returns {string} ? - */ - function getConstant(prop: string): string { - if (!isNullOrUndefined(currentLocale[`${prop}`])) { - return currentLocale[`${prop}`]; - } else { - return localeStrings[`${prop}`] || ''; - } - } - - /** - * Returns the control constant object for current object and the locale specified. - * - * @param {Object} curObject ? - * @param {string} locale ? - * @returns {Object} ? - */ - function intGetControlConstant(curObject: Object, locale: string): Object { - if (curObject[`${locale}`]) { - return curObject[`${locale}`][`${controlName}`]; - } - return null; - } - - setLocale(locale || defaultCulture); - - return { - setLocale, - getConstant - }; -} - -L10n.locale = {} as object; - -/** - * Sets the global locale for all components. - * - * @param {Object} localeObject - Specifies the localeObject to be set as global locale. - * @returns {void} - */ -L10n.load = (localeObject: Object): void => { - L10n.locale = extend(L10n.locale, localeObject, {}, true); -}; diff --git a/components/base/src/observer.tsx b/components/base/src/observer.tsx deleted file mode 100644 index ffc0ed5..0000000 --- a/components/base/src/observer.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { isNullOrUndefined, getValue } from './util'; - -/** - * Interface for the bound options used in the observer. - */ -export interface BoundOptions { - handler?: Function; - context?: Object; - event?: string; - id?: string; -} - -export interface IObserver { - isJson: (value: string) => boolean; - ranArray: string[]; - boundedEvents: { [key: string]: BoundOptions[] }; - on: (property: string, handler: Function, context?: object, id?: string) => void; - off: (property: string, handler?: Function, id?: string) => void; - notify: (property: string, argument?: Object, successHandler?: Function, errorHandler?: Function) => void | Object; - destroy: () => void; -} - -/** - * Observer is used to perform event handling based the object. - * - * @returns {IObserver} Returns an Observer instance for event handling - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const Observer: any = () => { - let ranArray: string[] = []; - const boundedEvents: { [key: string]: BoundOptions[] } = {}; - - /** - * Attach handler for given property in current context. - * - * @param {string} property - specifies the name of the event. - * @param {Function} handler - Specifies the handler function to be called while event notified. - * @param {Object} context - Specifies the context binded to the handler. - * @param {string} id - specifies the random generated id. - * @returns {void} - */ - function on(property: string, handler: Function, context?: Object, id?: string): void { - if (isNullOrUndefined(handler)) { - return; - } - const cntxt: Object = context || {}; - if (notExist(property)) { - boundedEvents[`${property}`] = [{ handler, context: cntxt, id }]; - } else if (id && !ranArray.includes(id)) { - ranArray.push(id); - boundedEvents[`${property}`].push({ handler, context: cntxt, id }); - } else if (!boundedEvents[`${property}`].some((event: BoundOptions) => event.handler === handler)) { - boundedEvents[`${property}`].push({ handler, context: cntxt }); - } - } - - /** - * To remove handlers from a event attached using on() function. - * - * @param {string} property - specifies the name of the event. - * @param {Function} handler - Optional argument specifies the handler function to be called while event notified. - * @param {string} id - specifies the random generated id. - * @returns {void} - */ - function off(property: string, handler?: Function, id?: string): void { - if (notExist(property)) { - return; - } - const curObject: BoundOptions[] = getValue(property, boundedEvents); - if (handler) { - for (let i: number = 0; i < curObject.length; i++) { - const currentEvent: BoundOptions = curObject[parseInt(i.toString(), 10)]; - if (id && currentEvent.id === id) { - curObject.splice(i, 1); - ranArray = ranArray.filter((item: string) => item !== id); - break; - } else if (handler === currentEvent.handler) { - curObject.splice(i, 1); - break; - } - } - } else { - delete boundedEvents[`${property}`]; - } - } - - /** - * To notify the handlers in the specified event. - * - * @param {string} property - Specifies the event to be notify. - * @param {Object} argument - Additional parameters to pass while calling the handler. - * @param {Function} successHandler - this function will invoke after event successfully triggered - * @param {Function} errorHandler - this function will invoke after event if it was failure to call. - * @returns {void} ? - */ - function notify(property: string, argument?: Object, successHandler?: Function, errorHandler?: Function): void { - if (notExist(property)) { - if (successHandler) { - successHandler(argument); - } - return; - } - if (argument) { - argument = { ...argument, name: property }; - } - const curObject: BoundOptions[] = getValue(property, boundedEvents).slice(0); - for (const cur of curObject) { - try { - cur.handler(argument); - } catch (error) { - if (errorHandler) { - errorHandler(error); - } - } - } - if (successHandler) { - successHandler(argument); - } - } - - /** - * Checks if a string value is valid JSON. - * - * @param {string} value - The string to check if it's valid JSON - * @returns {boolean} Returns true if the string is valid JSON, false otherwise - */ - function isJson(value: string): boolean { - try { - JSON.parse(value); - } catch (e) { - return false; - } - return true; - } - /** - * To destroy handlers in the event - * - * @returns {void} ? - */ - function destroy(): void { - Object.keys(boundedEvents).forEach((key: string) => { - delete boundedEvents[`${key}`]; - }); - ranArray = []; - } - /** - * To remove internationalization events - * - * @returns {void} ? - */ - function offIntlEvents(): void { - const eventsArr: BoundOptions[] = boundedEvents['notifyExternalChange']; - if (eventsArr) { - for (let i: number = 0; i < eventsArr.length; i++) { - const curEvent: BoundOptions = eventsArr[parseInt(i.toString(), 10)]; - const curContext: object = curEvent.context; - if (curContext && curContext['detectFunction'] && curContext['randomId'] && curContext['isReactMock']) { - off('notifyExternalChange', curEvent.handler, curContext['randomId']); - i--; - } - } - if (!boundedEvents['notifyExternalChange'].length) { - delete boundedEvents['notifyExternalChange']; - } - } - } - /** - * Returns if the property exists. - * - * @param {string} prop ? - * @returns {boolean} ? - */ - function notExist(prop: string): boolean { - return !boundedEvents[`${prop}`] || boundedEvents[`${prop}`].length === 0; - } - - return { - isJson, - on, - off, - notify, - destroy, - ranArray, - boundedEvents, - offIntlEvents - }; -}; diff --git a/components/base/src/provider.tsx b/components/base/src/provider.tsx deleted file mode 100644 index d150af9..0000000 --- a/components/base/src/provider.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { createContext, useContext, ReactNode } from 'react'; - -// Define the shape of the context -interface ProviderContextProps { - locale: string; - dir: string; - ripple: boolean; -} - -// Create context with default empty fallback -const ProviderContext: React.Context = createContext({locale: 'en-US', dir: 'ltr', ripple: false}); - -/** - * Props for the Provider context. - */ -export interface ProviderProps { - /** - * Components that will have access to the provided context value. - */ - children: ReactNode; - /** - * Specifies the locale for the component. - * - * @default 'en-US' - */ - locale?: string; - /** - * Specifies the text direction of the component. Use 'ltr' for left-to-right or 'rtl' for right-to-left. - * - * @default 'ltr' - */ - dir?: string; - /** - * Enables or disables the ripple effect for the component. - * - * @default false - */ - ripple?: boolean; -} - -// The Locale provider component -export const Provider: React.FC = (props: ProviderProps) => { - const { children, locale = 'en-US', dir = 'ltr', ripple = false } = props; - return ( - - {children} - - ); -}; - -/** - * Custom hook to consume locale context. - * - * @private - * @returns {ProviderContextProps} - The locale context value. - */ -export function useProviderContext(): ProviderContextProps { - return useContext(ProviderContext); -} diff --git a/components/base/src/ripple.tsx b/components/base/src/ripple.tsx deleted file mode 100644 index fc18e13..0000000 --- a/components/base/src/ripple.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import { useRef, useState, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react'; - -/** - * Configuration options for the ripple effect behavior. - */ -export interface RippleOptions { - /** - * The duration of the ripple animation in milliseconds. - * - * @default 350 - */ - duration?: number; - - /** - * Whether the ripple should start from the center of the element instead of the click position. - * - * @default false - */ - isCenterRipple?: boolean; - - /** - * CSS selector for elements that should not trigger a ripple effect when clicked. - * - * @default null - */ - ignore?: string | null; - - /** - * CSS selector to identify which parent element should receive the ripple when a child is clicked. - * - * @default null - */ - selector?: string | null; -} - -/** - * Interface for the ripple effect handlers and component. - */ -export interface RippleEffect { - /** - * Handler for the mouse down event to start the ripple effect. - * - * @param {MouseEvent} e - The mouse down event. - */ - rippleMouseDown: (e: React.MouseEvent) => void; - - /** - * React component that renders the actual ripple elements. - */ - Ripple: React.FC; -} - -/** - * Props for an individual ripple item. - * - * @private - */ -interface RippleProps { - /** - * The x-coordinate position of the ripple relative to its container. - */ - x: number; - - /** - * The y-coordinate position of the ripple relative to its container. - */ - y: number; - - /** - * The size of the ripple, either in pixels or percentage. - */ - size: string; - - /** - * The duration of the ripple animation in milliseconds. - */ - duration: number; - - /** - * Whether the ripple is centered within its container. - */ - isCenterRipple: boolean; - - /** - * Callback function triggered when the animation is complete. - */ - onAnimationComplete: () => void; -} - -/** - * Interface representing the data structure for a single ripple effect. - * - * @private - */ -interface RippleItem { - id: string | number; - x: number; - y: number; - size: number | string; - duration: number; - isCenterRipple: boolean; -} - -/** - * Interface representing the data structure RippleContainerRef. - * - * @private - */ -interface RippleContainerRef { - createRipple: (x: number, y: number, size: number, duration: number, isCenterRipple: boolean) => void; -} - -declare type Timeout = ReturnType; - -// Individual ripple component -export const RippleElement: React.FC = ({ - x, - y, - size, - duration, - onAnimationComplete -}: RippleProps) => { - const rippleRef: React.RefObject = useRef(null); - useEffect(() => { - const ripple: HTMLDivElement | null = rippleRef.current; - if (!ripple) { - return undefined; - } - void ripple.offsetWidth; - ripple.style.transform = 'scale(1)'; - const fadeTimer: Timeout = setTimeout(() => { - if (ripple){ - ripple.style.opacity = '0'; - } - }, duration / 2); - const cleanupTimer: Timeout = setTimeout(() => { - if (ripple) { - ripple.style.opacity = '0'; - ripple.style.visibility = 'hidden'; - } - if (onAnimationComplete){ - onAnimationComplete(); - } - }, duration); - return () => { - clearTimeout(fadeTimer); - clearTimeout(cleanupTimer); - }; - }, [duration, onAnimationComplete]); - return ( -
- ); -}; - -export const RippleContainer: React.ForwardRefExoticComponent<{ maxRipples?: number } & React.RefAttributes -> = forwardRef((props: { maxRipples?: number }, ref: React.ForwardedRef) => { - const { maxRipples = 2 } = props; - const containerRef: React.RefObject = useRef(null); - const [ripples, setRipples] = useState([]); - const nextIdRef: React.RefObject = useRef(0); - const createRipple: (x: number, y: number, size: number, duration: number, isCenterRipple: boolean) => void = - useCallback((x: number, y: number, size: number, duration: number, isCenterRipple: boolean) => { - const id: number = nextIdRef.current++; - setRipples((prevRipples: RippleItem[]) => { - if (prevRipples.length >= maxRipples) { - return [ - ...prevRipples.slice(prevRipples.length - maxRipples + 1), - { id, x, y, size, duration, isCenterRipple } - ]; - } - return [ - ...prevRipples, - { id, x, y, size, duration, isCenterRipple } - ]; - }); - }, [maxRipples]); - - const removeRipple: (id: string | number) => void = useCallback((id: string | number) => { - setRipples((prevRipples: RippleItem[]) => prevRipples.filter((r: RippleItem) => r.id !== id)); - }, []); - - useImperativeHandle(ref, () => ({ - createRipple - })); - - return ( - - {ripples.map((ripple: RippleItem) => ( - removeRipple(ripple.id)} - /> - ))} - - ); -}); - -const closestParent: (element: HTMLElement, selector: string | null) => HTMLElement | null = -(element: HTMLElement, selector: string | null): HTMLElement | null => { - if (!selector) - { - return null; - } - let el: HTMLElement = element; - while (el) { - if (el.matches && el.matches(selector)) { - return el; - } - el = el.parentElement as HTMLElement; - if (!el) - { - break; - } - } - return null; -}; - -/** - * useRippleEffect function provides wave effect when an element is clicked. - * - * @param {boolean} isEnabled - Whether the ripple effect is enabled. - * @param {RippleOptions} options - Optional configuration for the ripple effect. - * @returns {RippleEffect} - Ripple effect controller object. - */ -export function useRippleEffect(isEnabled: boolean, options?: RippleOptions): RippleEffect { - const rippleRef: React.RefObject = useRef(null); - const defaultOptions: RippleOptions = { - duration: 350, - isCenterRipple: false, - ignore: null, - selector: null, - ...options - }; - const maxRipples: number = 2; - const ignoreRipple: (target: HTMLElement) => boolean = useCallback((target: HTMLElement): boolean => { - if (!isEnabled || closestParent(target, defaultOptions.ignore || '')) { - return true; - } - return false; - }, [isEnabled, defaultOptions.ignore]); - - const createRipple: (e: React.MouseEvent) => void = useCallback((e: React.MouseEvent) => { - if (!isEnabled || !rippleRef.current) - { - return; - } - let target: HTMLElement = e.currentTarget; - if (defaultOptions.selector) { - const matchedTarget: Element | null = closestParent(target, defaultOptions.selector); - if (!matchedTarget){ - return; - } - target = matchedTarget as HTMLElement; } - - if (ignoreRipple(target)) - { - return; - } - const rect: DOMRect = target.getBoundingClientRect(); - const offsetX: number = e.pageX - window.pageXOffset; - const offsetY: number = e.pageY - window.pageYOffset; - let rippleX: number; - let rippleY: number; - let rippleSize: number; - if (defaultOptions.isCenterRipple) { - rippleX = rect.width / 2; - rippleY = rect.height / 2; - rippleSize = Math.max(rect.width, rect.height); - } else { - rippleX = offsetX - rect.left; - rippleY = offsetY - rect.top; - const sizeX: number = Math.max(Math.abs(rect.width - rippleX), rippleX) * 2; - const sizeY: number = Math.max(Math.abs(rect.height - rippleY), rippleY) * 2; - rippleSize = Math.sqrt(sizeX * sizeX + sizeY * sizeY); - } - rippleRef.current.createRipple( - rippleX, - rippleY, - rippleSize, - defaultOptions.duration || 350, - defaultOptions.isCenterRipple || false - ); - }, [isEnabled, defaultOptions, ignoreRipple]); - - const Ripple: () => React.ReactNode = useCallback(() => ( - - ), []); - - return { - rippleMouseDown: createRipple, - Ripple - }; -} diff --git a/components/base/src/sanitize-helper.tsx b/components/base/src/sanitize-helper.tsx deleted file mode 100644 index 688ea8d..0000000 --- a/components/base/src/sanitize-helper.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { detach } from './dom'; -import { isNullOrUndefined } from './util'; - -/** - * Interface for the request to sanitize HTML. - */ -interface BeforeSanitizeHtml { - /** Illustrates whether the current action needs to be prevented or not. */ - cancel?: boolean; - /** It is a callback function and executed it before our inbuilt action. It should return HTML as a string. - * - * @function - * @param {string} value - Returns the value. - * @returns {string} - */ - /** Returns the selectors object which carrying both tags and attributes selectors to block list of cross-site scripting attack. - * Also possible to modify the block list in this event. - */ - selectors?: SanitizeSelectors; -} - -interface SanitizeSelectors { - /** Returns the tags. */ - tags?: string[]; - /** Returns the attributes. */ - attributes?: SanitizeRemoveAttrs[]; -} - -interface SanitizeRemoveAttrs { - /** Defines the attribute name to sanitize */ - attribute?: string; - /** Defines the selector that sanitize the specified attributes within the selector */ - selector?: string; -} - - -interface ISanitize { - /** Array of attributes to remove during sanitization */ - removeAttrs: SanitizeRemoveAttrs[] - /** Array of HTML tags to remove during sanitization */ - removeTags: string[] - /** Element to wrap the sanitized content */ - wrapElement: Element - /** Callback function executed before sanitization begins */ - beforeSanitize: () => BeforeSanitizeHtml - /** Custom sanitization function */ - sanitize: (value: string) => string - /** Function to serialize the sanitized value */ - serializeValue: (item: BeforeSanitizeHtml, value: string) => string -} - -const removeTags: string[] = [ - 'script', - 'style', - 'iframe[src]', - 'link[href*="javascript:"]', - 'object[type="text/x-scriptlet"]', - 'object[data^="data:text/html;base64"]', - 'img[src^="data:text/html;base64"]', - '[src^="javascript:"]', - '[dynsrc^="javascript:"]', - '[lowsrc^="javascript:"]', - '[type^="application/x-shockwave-flash"]' -]; - -const removeAttrs: SanitizeRemoveAttrs[] = [ - { attribute: 'href', selector: '[href*="javascript:"]' }, - { attribute: 'href', selector: 'a[href]' }, - { attribute: 'background', selector: '[background^="javascript:"]' }, - { attribute: 'style', selector: '[style*="javascript:"]' }, - { attribute: 'style', selector: '[style*="expression("]' }, - { attribute: 'href', selector: 'a[href^="data:text/html;base64"]' } -]; - -const jsEvents: string[] = ['onchange', - 'onclick', - 'onmouseover', - 'onmouseout', - 'onkeydown', - 'onload', - 'onerror', - 'onblur', - 'onfocus', - 'onbeforeload', - 'onbeforeunload', - 'onkeyup', - 'onsubmit', - 'onafterprint', - 'onbeforeonload', - 'onbeforeprint', - 'oncanplay', - 'oncanplaythrough', - 'oncontextmenu', - 'ondblclick', - 'ondrag', - 'ondragend', - 'ondragenter', - 'ondragleave', - 'ondragover', - 'ondragstart', - 'ondrop', - 'ondurationchange', - 'onemptied', - 'onended', - 'onformchange', - 'onforminput', - 'onhaschange', - 'oninput', - 'oninvalid', - 'onkeypress', - 'onloadeddata', - 'onloadedmetadata', - 'onloadstart', - 'onmessage', - 'onmousedown', - 'onmousemove', - 'onmouseup', - 'onmousewheel', - 'onoffline', - 'onoine', - 'ononline', - 'onpagehide', - 'onpageshow', - 'onpause', - 'onplay', - 'onplaying', - 'onpopstate', - 'onprogress', - 'onratechange', - 'onreadystatechange', - 'onredo', - 'onresize', - 'onscroll', - 'onseeked', - 'onseeking', - 'onselect', - 'onstalled', - 'onstorage', - 'onsuspend', - 'ontimeupdate', - 'onundo', - 'onunload', - 'onvolumechange', - 'onwaiting', - 'onmouseenter', - 'onmouseleave', - 'onstart', - 'onpropertychange', - 'oncopy', - 'ontoggle', - 'onpointerout', - 'onpointermove', - 'onpointerleave', - 'onpointerenter', - 'onpointerrawupdate', - 'onpointerover', - 'onbeforecopy', - 'onbeforecut', - 'onbeforeinput' -]; - -/** - * Custom hook for sanitizing HTML. - * - * @private - * @returns An object with methods for sanitizing strings and working with HTML sanitation. - */ -export const SanitizeHtmlHelper: ISanitize = (() => { - const props: ISanitize = { - removeAttrs: [] as SanitizeRemoveAttrs[], - removeTags: [] as string[], - wrapElement: null as Element, - beforeSanitize: null as (() => BeforeSanitizeHtml) | null, - sanitize: null as ((value: string) => string) | null, - serializeValue: null as ((item: BeforeSanitizeHtml, value: string) => string) | null - }; - - /** - * Configures settings before sanitizing HTML. - * - * @returns {BeforeSanitizeHtml} An object containing selectors. - */ - props.beforeSanitize = (): BeforeSanitizeHtml => ({ - selectors: { - tags: removeTags, - attributes: removeAttrs - } - }); - - /** - * Sanitizes the provided HTML string. - * - * @param {string} value - The HTML string to be sanitized. - * @returns {string} The sanitized HTML string. - */ - props.sanitize = (value: string): string => { - if (isNullOrUndefined(value)) { - return value; - } - const item: BeforeSanitizeHtml = props.beforeSanitize(); - return props.serializeValue(item, value); - }; - - /** - * Serializes and sanitizes the given HTML value based on the provided item configuration. - * - * @param {BeforeSanitizeHtml} item - The item configuration for sanitization. - * @param {string} value - The HTML string to be serialized and sanitized. - * @returns {string} The sanitized HTML string. - */ - props.serializeValue = (item: BeforeSanitizeHtml, value: string): string => { - props.removeAttrs = item.selectors.attributes; - props.removeTags = item.selectors.tags; - props.wrapElement = document.createElement('div'); - props.wrapElement.innerHTML = value; - removeXssTags(); - removeJsEvents(); - removeXssAttrs(); - const sanitizedValue: string = props.wrapElement.innerHTML; - removeElement(); - props.wrapElement = null; - return sanitizedValue.replace(/&/g, '&'); - }; - - /** - * Removes potentially harmful element attributes. - * - * @returns {void} - */ - function removeElement(): void { - if (props.wrapElement) { - // Removes an element's attibute to avoid html tag validation - const nodes: HTMLCollection = props.wrapElement.children; - for (let j: number = 0; j < nodes.length; j++) { - const attribute: NamedNodeMap = nodes[parseInt(j.toString(), 10)].attributes; - for (let i: number = 0; i < attribute.length; i++) { - nodes[parseInt(j.toString(), 10)].removeAttribute(attribute[parseInt(i.toString(), 10)].localName); - } - } - } - } - - /** - * Removes potentially harmful tags to prevent XSS attacks. - * - * @returns {void} - */ - function removeXssTags(): void { - if (props.wrapElement) { - const elements: NodeListOf = props.wrapElement.querySelectorAll(props.removeTags.join(',')); - elements.forEach((element: Element) => { - detach(element); - }); - } - } - - /** - * Removes JavaScript event attributes to prevent XSS attacks. - * - * @returns {void} - */ - function removeJsEvents(): void { - if (props.wrapElement) { - const elements: NodeListOf = props.wrapElement.querySelectorAll('[' + jsEvents.join('],[') + ']'); - elements.forEach((element: Element) => { - jsEvents.forEach((attr: string) => { - if (element.hasAttribute(attr)) { - element.removeAttribute(attr); - } - }); - }); - } - } - - /** - * Removes attributes based on configured selectors to prevent XSS attacks. - * - * @returns {void} - */ - function removeXssAttrs(): void { - if (props.wrapElement) { - props.removeAttrs.forEach((item: SanitizeRemoveAttrs) => { - const elements: NodeListOf = props.wrapElement.querySelectorAll(item.selector); - elements.forEach((element: Element) => { - if (item.selector === 'a[href]') { - const attrValue: string = element.getAttribute(item.attribute || ''); - if (attrValue && attrValue.replace(/\t|\s|&/, '').includes('javascript:alert')) { - element.removeAttribute(item.attribute || ''); - } - } else { - element.removeAttribute(item.attribute || ''); - } - }); - }); - } - } - - return props; -})(); diff --git a/components/base/src/svg-icon.tsx b/components/base/src/svg-icon.tsx deleted file mode 100644 index d29dce9..0000000 --- a/components/base/src/svg-icon.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import { HTMLAttributes } from 'react'; - -export interface IconProps { - /** - * Specifies the path data for the SVG icon. - * - * @default '' - */ - d?: string; - - /** - * Specifies the path fill color of the SVG icon. - * - * @default '' - */ - fill?: string; - - /** - * Specifies the height of the SVG icon. - * - * @default '16' - */ - height?: string; - - /** - * Specifies the viewBox of the SVG icon. - * - * @default '0 0 24 24' - */ - viewBox?: string; - - /** - * Specifies the width of the SVG icon. - * - * @default '16' - */ - width?: string; -} - -type SvgProps = IconProps & HTMLAttributes; - -/** - * The SVG component displays SVG icons with a given height, width, and viewBox. - * - * @private - * @param {SvgProps} props - The props of the component. - * @returns {void} Returns the SVG element. - */ -export const SvgIcon: React.FC = ((props: SvgProps) => { - const { - height = '16', - viewBox = '0 0 24 24', - width = '16', - fill, - d = '', - ...restProps - } = props; - - return ( - - - - ); -}); - -export default SvgIcon; diff --git a/components/base/src/touch.tsx b/components/base/src/touch.tsx deleted file mode 100644 index 9fda926..0000000 --- a/components/base/src/touch.tsx +++ /dev/null @@ -1,535 +0,0 @@ -import { useLayoutEffect, RefObject, MouseEvent } from 'react'; -import { extend } from './util'; -import { Browser } from './browser'; -import { EventHandler, BaseEventArgs } from './event-handler'; -import { Base, IBase } from './base'; - -/** - * SwipeSettings is a framework module that provides support to handle swipe event like swipe up, swipe right, etc.., - */ -export interface SwipeSettings { - /** - * Property specifies minimum distance of swipe moved. - * - * @default 50 - */ - swipeThresholdDistance?: number; -} - -const swipeRegex: RegExp = /(Up|Down)/; - -/** - * Interface defining the touch handling props. - */ -export interface ITouch extends IBase { - /** - * Specifies the callback function for tap event. - */ - tap?: (args: TapEventArgs) => void; - - /** - * Specifies the callback function for tapHold event. - */ - tapHold?: (args: TapEventArgs) => void; - - /** - * Specifies the callback function for swipe event. - */ - swipe?: (args: SwipeEventArgs) => void; - - /** - * Specifies the callback function for scroll event. - */ - scroll?: (args: ScrollEventArgs) => void; - - /** - * Specifies the time delay for tap. - * - * @default 350 - */ - tapThreshold?: number; - - /** - * Specifies the time delay for tap hold. - * - * @default 750 - */ - tapHoldThreshold?: number; - - /** - * Customize the swipe event configuration. - * - * @default '{swipeThresholdDistance: 50}' - */ - swipeSettings?: SwipeSettings; -} - -/** - * Custom hook to handle touch events such as tap, double tap, swipe, etc. - * - * @private - * @param {RefObject} element Target HTML element for touch events - * @param {Touch} props props to customize touch behavior - * @returns {Touch} The Touch object - */ -export function Touch(element: RefObject, props?: ITouch): ITouch { - const baseRef: IBase = Base(); - const propsRef: ITouch = { ...props }; - //Internal Variables - let isTouchMoved: boolean = false; - let startPoint: Points = { clientX: 0, clientY: 0 }; - let startEventData: MouseEventArgs | TouchEventArgs = null; - let lastMovedPoint: Points = { clientX: 0, clientY: 0 }; - let scrollDirection: string = ''; - let hScrollLocked: boolean = false; - let vScrollLocked: boolean = false; - let defaultArgs: TapEventArgs = { originalEvent: null }; - let distanceX: number = 0; - let distanceY: number = 0; - let movedDirection: string = ''; - let tStampStart: number = 0; - let touchAction: boolean = true; - let timeOutTap: ReturnType | null = null; - let timeOutTapHold: ReturnType | null = null; - let tapCount: number = 0; - - useLayoutEffect(() => { - bind(); - return () => { - unwireEvents(); - baseRef.destroy(); - }; - }, []); - - /** - * Binds the touch event handlers to the target element. - * Calls internal methods to setup required bindings and adds necessary class for IE browser. - * - * @returns {void} - */ - function bind(): void { - if (element.current) { - wireEvents(); - if (Browser.isIE) { element.current.classList.add('sf-block-touch'); } - } - } - - /** - * Attaches event listeners for start, move, end, and cancel touch events to the target element. - * - * @returns {void} - */ - function wireEvents(): void { - EventHandler.add(element.current, Browser.touchStartEvent, startEvent, undefined); - } - - /** - * Detaches event listeners for start, move, end, and cancel touch events to the target element. - * - * @returns {void} - */ - function unwireEvents(): void { - EventHandler.remove(element.current, Browser.touchStartEvent, startEvent); - } - - /** - * Returns if the HTML element is scrollable by inspecting its overflow properties. - * - * @param {HTMLElement} element - The HTML element to be checked. - * @returns {boolean} True if the element is scrollable, false otherwise. - */ - function isScrollable(element: HTMLElement): boolean { - const eleStyle: CSSStyleDeclaration = getComputedStyle(element); - const style: string = eleStyle.overflow + eleStyle.overflowX + eleStyle.overflowY; - return (/(auto|scroll)/).test(style); - } - - /** - * Handler for the touch start event. Initializes touch action tracking and - * attaches additional event listeners for move, end, and cancel actions. - * - * @param {MouseEventArgs | TouchEventArgs} evt - The event object from the touch start action. - * @returns {void} - */ - function startEvent(evt: MouseEventArgs | TouchEventArgs): void { - if (touchAction === true) { - const point: MouseEventArgs | TouchEventArgs = updateChangeTouches(evt); - if (evt.changedTouches !== undefined) { - touchAction = false; - } - isTouchMoved = false; - movedDirection = ''; - startPoint = lastMovedPoint = { clientX: point.clientX, clientY: point.clientY }; - startEventData = point; - hScrollLocked = vScrollLocked = false; - tStampStart = Date.now(); - timeOutTapHold = setTimeout(() => { tapHoldEvent(evt); }, propsRef.tapHoldThreshold ? propsRef.tapHoldThreshold : 750); - EventHandler.add(element.current, Browser.touchMoveEvent, moveEvent, undefined); - EventHandler.add(element.current, Browser.touchEndEvent, endEvent, undefined); - EventHandler.add(element.current, Browser.touchCancelEvent, cancelEvent, undefined); - } - } - - /** - * Handler for the touch move event. Updates movement tracking and triggers the scroll event if a scroll action is detected. - * - * @param {MouseEventArgs | TouchEventArgs} evt - The event object from the touch move action. - * @returns {void} - */ - function moveEvent(evt: MouseEventArgs | TouchEventArgs): void { - const point: MouseEventArgs | TouchEventArgs = updateChangeTouches(evt); - isTouchMoved = !(point.clientX === startPoint.clientX && point.clientY === startPoint.clientY); - let eScrollArgs: ScrollEventArgs = {}; - if (isTouchMoved) { - clearTimeout(timeOutTapHold); - calcScrollPoints(evt); - const scrollArg: ScrollEventArgs = { - startEvents: startEventData, - originalEvent: evt, - startX: startPoint.clientX, - startY: startPoint.clientY, - distanceX: distanceX, - distanceY: distanceY, - scrollDirection: scrollDirection, - velocity: getVelocity(point) - }; - eScrollArgs = extend(eScrollArgs, {}, scrollArg); - baseRef.trigger('scroll', eScrollArgs, propsRef.scroll); - lastMovedPoint = { clientX: point.clientX, clientY: point.clientY }; - } - } - - /** - * Handler for the touch cancel event. Clears timeouts and triggers necessary cleanup. - * - * @param {MouseEventArgs | TouchEventArgs} evt - The event object from the touch cancel action. - * @returns {void} - */ - function cancelEvent(evt: MouseEventArgs | TouchEventArgs): void { - clearTimeout(timeOutTapHold); - clearTimeout(timeOutTap); - tapCount = 0; - swipeFn(evt); - EventHandler.remove(element.current, Browser.touchCancelEvent, cancelEvent); - } - - /** - * Triggers when a tap hold is detected. - * Invokes the tap hold callback and removes additional event listeners. - * - * @param {MouseEvent | TouchEventArgs} evt - The event object from the tap hold action. - * @returns {void} - */ - function tapHoldEvent(evt: MouseEventArgs | TouchEventArgs): void { - tapCount = 0; - touchAction = true; - EventHandler.remove(element.current, Browser.touchMoveEvent, moveEvent); - EventHandler.remove(element.current, Browser.touchEndEvent, endEvent); - const eTapArgs: TapEventArgs = { originalEvent: evt }; - baseRef.trigger('tapHold', eTapArgs, propsRef.tapHold); - EventHandler.remove(element.current, Browser.touchCancelEvent, cancelEvent); - } - - /** - * Handler for the touch end event. Determines if a tap or swipe occurred and triggers the respective callbacks. - * - * @param {MouseEventArgs | TouchEventArgs} evt - The event object from the touch end action. - * @returns {void} - */ - function endEvent(evt: MouseEventArgs | TouchEventArgs): void { - swipeFn(evt); - if (!isTouchMoved) { - if (typeof propsRef.tap === 'function') { - baseRef.trigger('tap', { originalEvent: evt, tapCount: ++tapCount }, propsRef.tap); - timeOutTap = setTimeout(() => { - tapCount = 0; - }, propsRef.tapThreshold ? propsRef.tapThreshold : 350); - } - } - modeclear(); - } - - /** - * Determines if a swipe occurred and triggers the swipe event callback. - * Computes swipe direction, distance, and velocity. - * - * @param {MouseEventArgs | TouchEventArgs} evt - The event object from the swipe action. - * @returns {void} - */ - function swipeFn(evt: MouseEventArgs | TouchEventArgs): void { - clearTimeout(timeOutTapHold); - clearTimeout(timeOutTap); - const point: MouseEventArgs | TouchEventArgs = updateChangeTouches(evt); - let diffX: number = point.clientX - startPoint.clientX; - let diffY: number = point.clientY - startPoint.clientY; - diffX = Math.floor(diffX < 0 ? -1 * diffX : diffX); - diffY = Math.floor(diffY < 0 ? -1 * diffY : diffY); - isTouchMoved = diffX > 1 || diffY > 1; - const isFirefox: boolean = (/Firefox/).test(Browser.userAgent); - if (isFirefox && point.clientX === 0 && point.clientY === 0 && evt.type === 'mouseup') { - isTouchMoved = false; - } - calcPoints(evt); - const swipeArgs: SwipeEventArgs = { - originalEvent: evt, - startEvents: startEventData, - startX: startPoint.clientX, - startY: startPoint.clientY, - distanceX: distanceX, - distanceY: distanceY, - swipeDirection: movedDirection, - velocity: getVelocity(point) - }; - if (isTouchMoved) { - const tDistance: number = propsRef.swipeSettings ? propsRef.swipeSettings.swipeThresholdDistance : 50; - const eSwipeArgs: object = extend(undefined, defaultArgs, swipeArgs); - let canTrigger: boolean = false; - const scrollBool: boolean = isScrollable(element.current); - const moved: boolean = swipeRegex.test(movedDirection); - if (tDistance && ((tDistance < distanceX && !moved) || (tDistance < distanceY && moved))) { - if (!scrollBool) { - canTrigger = true; - } else { - canTrigger = checkSwipe(element.current, moved); - } - } - if (canTrigger) { - baseRef.trigger('swipe', eSwipeArgs, propsRef.swipe); - } - } - modeclear(); - } - - /** - * Clears or resets various states and timeouts after a touch action completes. - * Ensures the touch action can be re-triggered properly. - * - * @returns {void} - */ - function modeclear(): void { - setTimeout(() => { - touchAction = true; - }, typeof propsRef.tap !== 'function' ? 0 : 20); - EventHandler.remove(element.current, Browser.touchMoveEvent, moveEvent); - EventHandler.remove(element.current, Browser.touchEndEvent, endEvent); - EventHandler.remove(element.current, Browser.touchCancelEvent, cancelEvent); - } - - /** - * Calculates the distance and direction of the touch points during a swipe. - * Sets the moved direction based on the calculated points' differences. - * - * @param {MouseEventArgs | TouchEventArgs} evt - The event object from the movement action. - * @returns {void} - */ - function calcPoints(evt: MouseEventArgs | TouchEventArgs): void { - const point: MouseEventArgs | TouchEventArgs = updateChangeTouches(evt); - defaultArgs = { originalEvent: evt }; - distanceX = Math.abs(Math.abs(point.clientX) - Math.abs(startPoint.clientX)); - distanceY = Math.abs(Math.abs(point.clientY) - Math.abs(startPoint.clientY)); - movedDirection = distanceX > distanceY - ? (point.clientX > startPoint.clientX ? 'Right' : 'Left') - : (point.clientY < startPoint.clientY ? 'Up' : 'Down'); - } - - /** - * Calculates the scrolling distance and direction when a scroll action is detected. - * Determines which direction the scroll is locked, updating the scroll direction accordingly. - * - * @param {MouseEventArgs | TouchEventArgs} evt - The event object from the scroll action. - * @returns {void} - */ - function calcScrollPoints(evt: MouseEventArgs | TouchEventArgs): void { - const point: MouseEventArgs | TouchEventArgs = updateChangeTouches(evt); - defaultArgs = { originalEvent: evt }; - distanceX = Math.abs(Math.abs(point.clientX) - Math.abs(lastMovedPoint.clientX)); - distanceY = Math.abs(Math.abs(point.clientY) - Math.abs(lastMovedPoint.clientY)); - if ((distanceX > distanceY || hScrollLocked) && !vScrollLocked) { - scrollDirection = point.clientX > lastMovedPoint.clientX ? 'Right' : 'Left'; - hScrollLocked = true; - } else { - scrollDirection = point.clientY < lastMovedPoint.clientY ? 'Up' : 'Down'; - vScrollLocked = true; - } - } - - /** - * Calculates the velocity of the swipe or scroll action based on the distance moved and time taken. - * - * @param {MouseEventArgs | TouchEventArgs} pnt - The point from which velocity is calculated. - * @returns {number} The calculated velocity of the touch action. - */ - function getVelocity(pnt: MouseEventArgs | TouchEventArgs): number { - const newX: number = pnt.clientX; - const newY: number = pnt.clientY; - const newT: number = Date.now(); - const xDist: number = newX - startPoint.clientX; - const yDist: number = newY - startPoint.clientY; - const interval: number = newT - tStampStart; - return Math.sqrt(xDist * xDist + yDist * yDist) / interval; - } - - /** - * Determines if a swipe action should be triggered based on the element's scrollable status and direction of movement. - * - * @param {HTMLElement} ele - The element to check for swipe triggering. - * @param {boolean} flag - Indicates the swipe direction (horizontal or vertical). - * @returns {boolean} True if the swipe can be triggered, false otherwise. - */ - function checkSwipe(ele: HTMLElement, flag: boolean): boolean { - const keys: string[] = ['scroll', 'offset']; - const temp: string[] = flag ? ['Height', 'Top'] : ['Width', 'Left']; - if (ele[keys[0] + temp[0]] <= ele[keys[1] + temp[0]]) { - return true; - } - return ( - ele[keys[0] + temp[1]] === 0 || - ele[keys[1] + temp[0]] + ele[keys[0] + temp[1]] >= ele[keys[0] + temp[0]] - ); - } - - /** - * Updates and returns the primary touch point from the event, used in various calculations. - * - * @param {MouseEventArgs | TouchEventArgs} evt - The event object from which to extract the touch point. - * @returns {MouseEventArgs | TouchEventArgs} The updated point from the touch event. - */ - function updateChangeTouches(evt: MouseEventArgs | TouchEventArgs): MouseEventArgs | TouchEventArgs { - return evt.changedTouches && evt.changedTouches.length !== 0 ? evt.changedTouches[0] : evt; - } - - return propsRef; -} - -interface Points { - clientX: number; - clientY: number; -} - -/** - * The argument type of `Tap` Event - */ -export interface TapEventArgs extends BaseEventArgs { - /** - * Original native event Object. - */ - originalEvent: TouchEventArgs | MouseEventArgs; - /** - * Tap Count. - */ - tapCount?: number; -} - -/** - * The argument type of `Scroll` Event - */ -export interface ScrollEventArgs extends BaseEventArgs { - /** - * Event argument for start event. - */ - startEvents?: TouchEventArgs | MouseEventArgs; - /** - * Original native event object for scroll. - */ - originalEvent?: TouchEventArgs | MouseEventArgs; - /** - * X position when scroll started. - */ - startX?: number; - /** - * Y position when scroll started. - */ - startY?: number; - /** - * The direction scroll. - */ - scrollDirection?: string; - /** - * The total traveled distance from X position - */ - distanceX?: number; - /** - * The total traveled distance from Y position - */ - distanceY?: number; - /** - * The velocity of scroll. - */ - velocity?: number; -} - -/** - * The argument type of `Swipe` Event - */ -export interface SwipeEventArgs extends BaseEventArgs { - /** - * Event argument for start event. - */ - startEvents?: TouchEventArgs | MouseEventArgs; - /** - * Original native event object for swipe. - */ - originalEvent?: TouchEventArgs | MouseEventArgs; - /** - * X position when swipe started. - */ - startX?: number; - /** - * Y position when swipe started. - */ - startY?: number; - /** - * The direction swipe. - */ - swipeDirection?: string; - /** - * The total traveled distance from X position - */ - distanceX?: number; - /** - * The total traveled distance from Y position - */ - distanceY?: number; - /** - * The velocity of swipe. - */ - velocity?: number; -} - -export interface TouchEventArgs extends MouseEvent { - /** - * A TouchList with touched points. - */ - changedTouches: MouseEventArgs[] | TouchEventArgs[]; - /** - * Cancel the default action. - */ - preventDefault(): void; - /** - * The horizontal coordinate point of client area. - */ - clientX: number; - /** - * The vertical coordinate point of client area. - */ - clientY: number; -} - -export interface MouseEventArgs extends MouseEvent { - /** - * A TouchList with touched points. - */ - changedTouches: MouseEventArgs[] | TouchEventArgs[]; - /** - * Cancel the default action. - */ - preventDefault(): void; - /** - * The horizontal coordinate point of client area. - */ - clientX: number; - /** - * The vertical coordinate point of client area. - */ - clientY: number; -} diff --git a/components/base/src/util.tsx b/components/base/src/util.tsx deleted file mode 100644 index ea8a0be..0000000 --- a/components/base/src/util.tsx +++ /dev/null @@ -1,418 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/** - * Common utility methods - * - * @private - */ -interface IKeyValue extends CSSStyleDeclaration { - [key: string]: any; -} - -declare let window: { - msCrypto: Crypto; -} & Window; -let uid: number = 0; - -/** - * Create Instance from constructor function with desired parameters. - * - * @param {Function} classFunction - Class function to which need to create instance - * @param {any[]} params - Parameters need to passed while creating instance - * @returns {any} ? - * @private - */ -export function createInstance(classFunction: Function, params: any[]): any { - const arrayParam: Object[] = params; - arrayParam.unshift(undefined); - return Function.prototype.bind.apply(classFunction, arrayParam); -} - -/** - * To run a callback function immediately after the browser has completed other operations. - * - * @param {Function} handler - callback function to be triggered. - * @returns {Function} ? - * @private - */ -export function setImmediate(handler: Function): Function { - let unbind: Function; - const num: any = new Uint16Array(5); - const intCrypto: Crypto = window.msCrypto || window.crypto; - intCrypto.getRandomValues(num); - let secret: string = 'syn' + combineArray(num); - let messageHandler: Function = (event: any): void => { - if (event.source === window && typeof event.data === 'string' && event.data.length <= 32 && event.data === secret) { - handler(); - unbind(); - } - }; - window.addEventListener('message', messageHandler as EventListener, false); - window.postMessage(secret, '*'); - return unbind = () => { - window.removeEventListener('message', messageHandler as EventListener); - handler = messageHandler = secret = undefined; - }; -} - -/** - * To get nameSpace value from the desired object. - * - * @param {string} nameSpace - String value to the get the inner object - * @param {any} obj - Object to get the inner object value. - * @returns {any} ? - * @private - */ -export function getValue(nameSpace: string, obj: any): any { - let value: any = obj; - const splits: string[] = !isNullOrUndefined(nameSpace) ? nameSpace.replace(/\[/g, '.').replace(/\]/g, '').split('.') : []; - for (let i: number = 0; i < splits.length && !isNullOrUndefined(value); i++) { - value = value[splits[parseInt(i.toString(), 10)]]; - } - return value; -} - -/** - * To set value for the nameSpace in desired object. - * - * @param {string} nameSpace - String value to the get the inner object - * @param {any} value - Value that you need to set. - * @param {any} obj - Object to get the inner object value. - * @returns {any} ? - * @private - */ -export function setValue(nameSpace: string, value: any, obj: any): any { - const keys: string[] = nameSpace.replace(/\[/g, '.').replace(/\]/g, '').split('.'); - const start: any = obj || {}; - let fromObj: any = start; - let i: number; - const length: number = keys.length; - let key: string; - - for (i = 0; i < length; i++) { - key = keys[parseInt(i.toString(), 10)]; - - if (i + 1 === length) { - fromObj[`${key}`] = value === undefined ? {} : value; - } else if (isNullOrUndefined(fromObj[`${key}`])) { - fromObj[`${key}`] = {}; - } - - fromObj = fromObj[`${key}`]; - } - - return start; -} - -/** - * Delete an item from Object - * - * @param {any} obj - Object in which we need to delete an item. - * @param {string} key - String value to the get the inner object - * @returns {void} ? - * @private - */ -export function deleteObject(obj: any, key: string): void { - delete obj[`${key}`]; -} - -/** - *@private - */ -export const containerObject: any = typeof window !== 'undefined' ? window : {}; - -/** - * Check weather the given argument is only object. - * - * @param {any} obj - Object which is need to check. - * @returns {boolean} ? - * @private - */ -export function isObject(obj: any): boolean { - const objCon: {} = {}; - return (!isNullOrUndefined(obj) && obj.constructor === objCon.constructor); -} - -/** - * To get enum value by giving the string. - * - * @param {any} enumObject - Enum object. - * @param {string} enumValue - Enum value to be searched - * @returns {any} ? - * @private - */ -export function getEnumValue(enumObject: any, enumValue: string | number): any { - return (enumObject[`${enumValue}`]); -} - -/** - * Merge the source object into destination object. - * - * @param {any} source - source object which is going to merge with destination object - * @param {any} destination - object need to be merged - * @returns {void} ? - * @private - */ -export function merge(source: Object, destination: Object): void { - if (!isNullOrUndefined(destination)) { - const temrObj: IKeyValue = source as IKeyValue; - const tempProp: IKeyValue = destination as IKeyValue; - const keys: string[] = Object.keys(destination); - const deepmerge: string = 'deepMerge'; - for (const key of keys) { - if (!isNullOrUndefined(temrObj[`${deepmerge}`]) && (temrObj[`${deepmerge}`].indexOf(key) !== -1) && - (isObject(tempProp[`${key}`]) || Array.isArray(tempProp[`${key}`]))) { - extend(temrObj[`${key}`], temrObj[`${key}`], tempProp[`${key}`], true); - } else { - temrObj[`${key}`] = tempProp[`${key}`]; - } - } - } -} - -/** - * Extend the two object with newer one. - * - * @param {any} copied - Resultant object after merged - * @param {Object} first - First object need to merge - * @param {Object} second - Second object need to merge - * @param {boolean} deep ? - * @returns {Object} ? - * @private - */ -export function extend(copied: Object, first: Object, second?: Object, deep?: boolean): Object { - const result: IKeyValue = copied && typeof copied === 'object' ? copied as IKeyValue : {} as IKeyValue; - let length: number = arguments.length; - const args: Object = [copied, first, second, deep]; - if (deep) { - length = length - 1; - } - for (let i: number = 1; i < length; i++) { - if (!args[parseInt(i.toString(), 10)]) { - continue; - } - const obj1: { [key: string]: Object } = args[parseInt(i.toString(), 10)]; - Object.keys(obj1).forEach((key: string) => { - const src: Object = result[`${key}`]; - const copy: Object = obj1[`${key}`]; - let clone: Object; - if (deep && (isObject(copy) || Array.isArray(copy))) { - if (isObject(copy)) { - clone = src ? src : {}; - if (Array.isArray(clone) && Object.prototype.hasOwnProperty.call(clone, 'isComplexArray')) { - extend(clone, {}, copy, deep); - } else { - result[`${key}`] = extend(clone, {}, copy, deep); - } - } else { - clone = src ? src : []; - result[`${key}`] = extend([], clone, copy, (clone && (clone as any).length) || (copy && (copy as any).length)); - } - } else { - result[`${key}`] = copy; - } - }); - } - return result; -} - -/** - * To check whether the object is null or undefined. - * - * @param {any} value - To check the object is null or undefined - * @returns {boolean} ? - * @private - */ -export function isNullOrUndefined(value: T): boolean { - return value === undefined || value === null; -} - -/** - * To check whether the object is undefined. - * - * @param {any} value - To check the object is undefined - * @returns {boolean} ? - * @private - */ -export function isUndefined(value: T): boolean { - return typeof value === 'undefined'; -} - -/** - * To return the generated unique name - * - * @param {string} definedName - To concatenate the unique id to provided name - * @returns {string} ? - * @private - */ -export function getUniqueID(definedName?: string): string { - return definedName ? `${definedName}_${uid++}` : `unique_${uid++}`; -} - -/** - * It limits the rate at which a function can fire. The function will fire only once every provided second instead of as quickly. - * - * @param {Function} eventFunction - Specifies the function to run when the event occurs - * @param {number} delay - A number that specifies the milliseconds for function delay call option - * @returns {Function} ? - * @private - */ -export function debounce(eventFunction: Function, delay: number): Function { - let timerId: NodeJS.Timeout; - return function (...args: any[]): void { - clearTimeout(timerId); - timerId = setTimeout(() => { - eventFunction.apply(this, args); - }, delay); - }; -} - -/** - * To convert the object to string for query url - * - * @param {Object} data ? - * @returns {string} ? - * @private - */ -export function queryParams(data: any): string { - return Object.keys(data) - .map((key: string) => `${encodeURIComponent(key)}=${encodeURIComponent(data[`${key}`])}`) - .join('&'); -} - -/** - * To check whether the given array contains object. - * - * @param {any} value - Specifies the T type array to be checked. - * @returns {boolean} ? - * @private - */ -export function isObjectArray(value: T[]): boolean { - return Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' && value[0] !== null; -} - -/** - * To check whether the child element is descendant to parent element or parent and child are same element. - * - * @param {Element} child - Specifies the child element to compare with parent. - * @param {Element} parent - Specifies the parent element. - * @returns {boolean} ? - * @private - */ -export function compareElementParent(child: Element, parent: Element): boolean { - let node: Node = child; - - while (node !== null && node !== document) { - if (node === parent) { - return true; - } - node = node.parentNode as Node; - } - return false; -} - -/** - * To throw custom error message. - * - * @param {string} message - Specifies the error message to be thrown. - * @returns {void} ? - * @private - */ -export function throwError(message: string): void { - try { - throw new Error(message); - } catch (e) { - throw new Error(e.message + '\n' + e.stack); - } -} - -/** - * This function is used to print given element - * - * @param {Element} element - Specifies the print content element. - * @param {Window} printWindow - Specifies the print window. - * @returns {Window | null} ? - * @private - */ -export function print(element: Element, printWindow?: Window | null): Window | null { - if (typeof window === 'undefined') { - return null; - } - const div: Element = document.createElement('div'); - const links: HTMLElement[] = [].slice.call(document.getElementsByTagName('head')[0].querySelectorAll('base, link, style')); - const blinks: HTMLElement[] = [].slice.call(document.getElementsByTagName('body')[0].querySelectorAll('link, style')); - if (blinks.length) { - for (let l: number = 0, len: number = blinks.length; l < len; l++) { - links.push(blinks[parseInt(l.toString(), 10)]); - } - } - let reference: string = ''; - if (!printWindow) { - printWindow = window.open('', 'print', 'height=452,width=1024,tabbar=no'); - } - if (!printWindow) { - throw new Error('Unable to open print window'); - } - div.appendChild(element.cloneNode(true) as Element); - for (let i: number = 0, len: number = links.length; i < len; i++) { - reference += links[parseInt(i.toString(), 10)].outerHTML; - } - printWindow.document.write(' ' + reference + '' + div.innerHTML + - '' + ''); - printWindow.document.close(); - printWindow.focus(); - const interval: any = setInterval( - () => { - if ((printWindow as any).ready) { - printWindow.print(); - printWindow.close(); - clearInterval(interval); - } - }, - 500 - ); - return printWindow; -} - -/** - * Function to normalize the units applied to the element. - * - * @param {number|string} value ? - * @returns {string} result - * @private - */ -export function formatUnit(value: number | string): string { - const result: string = (value as string) + ''; - if (result.match(/auto|cm|mm|in|px|pt|pc|%|em|ex|ch|rem|vw|vh|vmin|vmax/)) { - return result; - } - return result + 'px'; -} - -/** - * Function to generate the unique id. - * - * @returns {any} ? - * @private - */ -export function uniqueID(): any { - if ((typeof window) === 'undefined') { - return; - } - const num: any = new Uint16Array(5); - const intCrypto: Crypto = window.msCrypto || window.crypto; - return intCrypto.getRandomValues(num); -} - -/** - * Combines the first five elements of an Int16Array into a comma-separated string. - * - * @param {Int16Array} num ? - * @returns {string} ? - */ -function combineArray(num: Int16Array): string { - let ret: string = ''; - for (let i: number = 0; i < 5; i++) { - ret += (i ? ',' : '') + num[parseInt(i.toString(), 10)]; - } - return ret; -} diff --git a/components/base/src/validate-lic.tsx b/components/base/src/validate-lic.tsx deleted file mode 100644 index a6d0d41..0000000 --- a/components/base/src/validate-lic.tsx +++ /dev/null @@ -1,494 +0,0 @@ -import { getValue, containerObject, setValue, isNullOrUndefined } from './util'; -import { createElement } from './dom'; - -export const componentList: string[] = ['grid', 'pivotview', 'treegrid', 'spreadsheet', 'rangeNavigator', 'DocumentEditor', 'listbox', 'inplaceeditor', 'PdfViewer', 'richtexteditor', 'DashboardLayout', 'chart', 'stockChart', 'circulargauge', 'diagram', 'heatmap', 'lineargauge', 'maps', 'slider', 'smithchart', 'barcode', 'sparkline', 'treemap', 'bulletChart', 'kanban', 'daterangepicker', 'schedule', 'gantt', 'signature', 'query-builder', 'drop-down-tree', 'carousel', 'filemanager', 'uploader', 'accordion', 'tab', 'treeview']; - -const bypassKey: number[] = [115, 121, 110, 99, 102, 117, 115, 105, 111, 110, 46, - 105, 115, 76, 105, 99, 86, 97, 108, 105, 100, 97, 116, 101, 100]; -let accountURL: string; - -export type ILicenseValidator = { - isValidated: boolean, - isLicensed: boolean, - version: string, - platform: RegExp, - errors: IErrorType, - validate: () => boolean, - getDecryptedData: (key: string) => string, - getInfoFromKey: () => IValidator[] -} - -/** - * License validation module - * - * @private - * @param {string} key - License key to validate - * @returns {LicenseValidator} License validator object - * @private - */ -export function LicenseValidator(key: string = ''): ILicenseValidator { - let isValidated: boolean = false; - let isLicensed: boolean = true; - const version: string = '{syncfusionReleaseversion}'; - const platform: RegExp = /JavaScript|ASPNET|ASPNETCORE|ASPNETMVC|FileFormats|essentialstudio/i; - const errors: IErrorType = { - noLicense: 'This application was built using a trial version of Syncfusion® Essential Studio®.' + - ' To remove the license validation message permanently, a valid license key must be included.', - trailExpired: 'This application was built using a trial version of Syncfusion® Essential Studio®.' + - ' To remove the license validation message permanently, a valid license key must be included.', - versionMismatched: 'The included Syncfusion® license key is invalid.', - platformMismatched: 'The included Syncfusion® license key is invalid.', - invalidKey: 'The included Syncfusion® license key is invalid.' - }; - - /** - * To manage licensing operation. - */ - const manager: { - setKey: (key: string) => void; - getKey: () => string; - } = (() => { - let licKey: string = ''; - /** - * Sets the license key. - * - * @param {string} key - Specifies the license key. - * @returns {void} - */ - function set(key: string): void { licKey = key; } - /** - * Gets the license key. - * - * @returns {string} -Gets the license key. - */ - function get(): string { return licKey; } - return { - setKey: set, - getKey: get - }; - })(); - - /** - * To manage npx licensing operation. - */ - const npxManager: { - getKey: () => string; - } = (() => { - const npxLicKey: string = 'npxKeyReplace'; - /** - * Gets the license key. - * - * @returns {string} - Gets the license key. - */ - function get(): string { return npxLicKey; } - return { - getKey: get - }; - })(); - - manager.setKey(key); - - /** - * To validate the provided license key. - * - * @returns {boolean} ? - */ - function validate(): boolean { - const contentKey: number[] = [115, 121, 110, 99, 102, 117, 115, 105, 111, 110, 46, 108, 105, - 99, 101, 110, 115, 101, 67, 111, 110, 116, 101, 110, 116]; - const URLKey: number[] = [115, 121, 110, 99, 102, 117, 115, 105, 111, 110, 46, 99, 108, - 97, 105, 109, 65, 99, 99, 111, 117, 110, 116, 85, 82, 76]; - if (!isValidated && (containerObject && !getValue(convertToChar(bypassKey), containerObject))) { - let validateMsg: string | null = null; - let validateURL: string | null = null; - if ((manager && manager.getKey()) || (npxManager && npxManager.getKey() !== 'npxKeyReplace')) { - const result: IValidator[] = getInfoFromKey(); - if (result && result.length) { - for (const res of result) { - if (!platform.test(res.platform) || res.invalidPlatform) { - validateMsg = errors.platformMismatched; - } - else { - if (((res.minVersion >= res.lastValue) && (res.minVersion !== res.lastValue)) || - (res.lastValue < parseInt(version, 10))) { - validateMsg = errors.versionMismatched; - } - else { - if (res.lastValue == null || isNaN(res.lastValue)) { - validateMsg = errors.versionMismatched; - } - } - if (res.expiryDate) { - const expDate: Date = new Date(res.expiryDate); - const currDate: Date = new Date(); - if (expDate !== currDate && expDate < currDate) { - validateMsg = errors.trailExpired; - } else { - break; - } - } - } - } - } else { - validateMsg = errors.invalidKey; - } - } else { - const licenseContent: string = getValue(convertToChar(contentKey), containerObject); - validateURL = getValue(convertToChar(URLKey), containerObject); - if (licenseContent && licenseContent !== '') { - validateMsg = licenseContent; - } else { - validateMsg = errors.noLicense; - } - } - if (validateMsg && typeof document !== 'undefined' && !isNullOrUndefined(document)) { - accountURL = (validateURL && validateURL !== '') ? validateURL : 'https://www.syncfusion.com/account/claim-license-key?pl=SmF2YVNjcmlwdA==&vs=Mjc=&utm_source=es_license_validation_banner&utm_medium=listing&utm_campaign=license-information'; - const errorDiv: HTMLElement = createElement('div', { - innerHTML: ` - - - - - - - - - - - - ` + validateMsg + ' ' + 'Claim your free account' - }); - errorDiv.setAttribute('style', `position: fixed; - top: 10px; - left: 10px; - right: 10px; - font-size: 14px; - background: #EEF2FF; - color: #222222; - z-index: 999999999; - text-align: left; - border: 1px solid #EEEEEE; - padding: 10px 11px 10px 50px; - border-radius: 8px; - font-family: Helvetica Neue, Helvetica, Arial;`); - document.body.appendChild(errorDiv); - isLicensed = false; - } - isValidated = true; - setValue(convertToChar(bypassKey), isValidated, containerObject); - } - return isLicensed; - } - - /** - * Decrypts base64 encoded data from the provided key. - * - * @param {string} key - The base64 encoded key to decrypt - * @returns {string} The decrypted string or empty string if decryption fails - */ - function getDecryptedData(key: string): string { - try { - return atob(key); - } - catch (error) { - return ''; - } - } - - /** - * Get license information from key. - * - * @returns {IValidator} - Get license information from key. - */ - function getInfoFromKey(): IValidator[] { - try { - let licKey: string = ''; - const pkey: number[] = [5439488, 7929856, 5111808, 6488064, 4587520, 7667712, 5439488, - 6881280, 5177344, 7208960, 4194304, 4456448, 6619136, 7733248, 5242880, 7077888, - 6356992, 7602176, 4587520, 7274496, 7471104, 7143424]; - let decryptedStr: string[] = []; - const resultArray: IValidator[] = []; - let invalidPlatform: boolean = false; - let isNpxKey: boolean = false; - if (manager.getKey()) { - licKey = manager.getKey(); - } else { - isNpxKey = true; - licKey = npxManager.getKey().split('npxKeyReplace')[1]; - } - const licKeySplit: string[] = licKey.split(';'); - for (const lKey of licKeySplit) { - const decodeStr: string = getDecryptedData(lKey); - if (!decodeStr) { - continue; - } - let k: number = 0; - let buffr: string = ''; - if (!isNpxKey) { - for (let i: number = 0; i < decodeStr.length; i++, k++) { - if (k === pkey.length) { k = 0; } - const c: number = decodeStr.charCodeAt(i); - buffr += String.fromCharCode(c ^ (pkey[parseInt(k.toString(), 10)] >> 16)); - } - } else { - const charKey: string = decodeStr[decodeStr.length - 1]; - const decryptedKey: number[] = []; - for (let i: number = 0; i < decodeStr.length; i++) { - decryptedKey[parseInt(i.toString(), 10)] = decodeStr[parseInt(i.toString(), 10)].charCodeAt(0) - - charKey.charCodeAt(0); - } - for (let i: number = 0; i < decryptedKey.length; i++) { - buffr += String.fromCharCode(decryptedKey[parseInt(i.toString(), 10)]); - } - } - if (platform.test(buffr)) { - decryptedStr = buffr.split(';'); - invalidPlatform = false; - if (decryptedStr.length > 3) { - const minVersion: number = parseInt(decryptedStr[1].split('.')[0], 10); - const lastValue: number = parseInt(decryptedStr[4], 10); - resultArray.push({ - platform: decryptedStr[0], - version: decryptedStr[1], - expiryDate: decryptedStr[2], - lastValue: lastValue, - minVersion: minVersion - }); - } - } else if (buffr && buffr.split(';').length > 3) { - invalidPlatform = true; - } - } - if (invalidPlatform && !resultArray.length) { - return [{ invalidPlatform: invalidPlatform }]; - } else { - return resultArray.length ? resultArray : []; - } - } catch (error) { - return []; - } - } - - return { - isValidated, - isLicensed, - version, - platform, - errors, - validate, - getDecryptedData, - getInfoFromKey - }; -} - -let licenseValidator: ILicenseValidator = LicenseValidator(); - -/** - * Converts the given number to characters. - * - * @private - * @param {number} cArr - Specifies the license key as number. - * @returns {string} ? - */ -export function convertToChar(cArr: number[]): string { - let ret: string = ''; - for (const arr of cArr) { - ret += String.fromCharCode(arr); - } - return ret; -} - -/** - * To set license key. - * - * @param {string} key - license key - * @returns {void} - */ -export function registerLicense(key: string): void { - licenseValidator = LicenseValidator(key); -} - -/** - * Validates the license key. - * - * @private - * @param {string} [key] - Optional license key to validate - * @returns {boolean} Returns true if license is valid, false otherwise - */ -export function validateLicense(key?: string): boolean { - if (key) { - registerLicense(key); - } - return licenseValidator.validate(); -} - -/** - * Gets the version information from the license validator. - * - * @private - * @returns {string} The version string from the license validator - */ -export function getVersion(): string { - return licenseValidator.version; -} - -/** - * Method for create overlay over the sample - * - * @private - * @returns {void} - */ -export function createLicenseOverlay(): void { - const bannerTemplate: string = ` -
-
-
- -
-
Claim your FREE account and get a key in less than a minute
-
    -
  • Access to a 30-day free trial of any of our products.
  • -
  • Access to 24x5 support by developers via the support tickets, forum, feature & feedback page and chat.
  • -
  • 200+ ebooks on the latest technologies, industry trends, and research topics. -
  • -
  • Largest collection of over 7500 flat and wireframe icons for free with Syncfusion® Metro Studio.
  • -
  • Free and unlimited access to Syncfusion® technical blogs and whitepapers.
  • -
-
Syncfusion is trusted by 29,000+ businesses worldwide
- - Claim your FREE account -
have a Syncfusion® account? Sign In
-
-
`; - if (typeof document !== 'undefined' && !isNullOrUndefined(document)) { - const errorBackground: HTMLElement = createElement('div', { - innerHTML: bannerTemplate - }); - document.body.appendChild(errorBackground); - } -} - -interface IValidator { - version?: string; - expiryDate?: string; - platform?: string; - invalidPlatform?: boolean; - lastValue?: number; - minVersion?: number; -} - -interface IErrorType { - noLicense: string; - trailExpired: string; - versionMismatched: string; - platformMismatched: string; - invalidKey: string; -} diff --git a/components/base/styles/_all.scss b/components/base/styles/_all.scss deleted file mode 100644 index b07cc99..0000000 --- a/components/base/styles/_all.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'common/all.scss'; -@import 'animation/all.scss'; diff --git a/components/base/styles/_material3-dark-definition.scss b/components/base/styles/_material3-dark-definition.scss deleted file mode 100644 index 2b536d3..0000000 --- a/components/base/styles/_material3-dark-definition.scss +++ /dev/null @@ -1,15 +0,0 @@ -@use 'sass:meta'; -@import 'definition/material3-dark.scss'; - -@if not meta.variable-exists('is-roboto-loaded') { - //sass-lint:disable no-url-protocols,no-url-domains - @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,900&display=swap');// stylelint-disable-line no-invalid-position-at-import-rule -} - -$font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif !default; -$font-size: 12px !default; -$font-weight: 400 !default; -$error-font-color: $danger !default; -$warning-font-color: $warning !default; -$success-font-color: $success !default; -$information-font-color: $info !default; diff --git a/components/base/styles/_material3-definition.scss b/components/base/styles/_material3-definition.scss deleted file mode 100644 index 17b8458..0000000 --- a/components/base/styles/_material3-definition.scss +++ /dev/null @@ -1,16 +0,0 @@ -@use 'sass:meta'; -@import 'definition/material3.scss'; - -@if not meta.variable-exists('is-roboto-loaded') { - //sass-lint:disable no-url-protocols,no-url-domains - @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,900&display=swap');// stylelint-disable-line no-invalid-position-at-import-rule -} - -$is-roboto-loaded: 'true' !default; -$font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif !default; -$font-size: 12px !default; -$font-weight: 400 !default; -$error-font-color: $danger !default; -$warning-font-color: $warning !default; -$success-font-color: $success !default; -$information-font-color: $info !default; diff --git a/components/base/styles/border-radius/_radius.scss b/components/base/styles/border-radius/_radius.scss new file mode 100644 index 0000000..076f35f --- /dev/null +++ b/components/base/styles/border-radius/_radius.scss @@ -0,0 +1,25 @@ +// Export border radius variables +$border-radius-none: 0 !default; +$border-radius-xs: 0.25rem !default; +$border-radius-sm: 0.875rem !default; +$border-radius-md: 1rem !default; +$border-radius-lg: 1.25rem !default; +$border-radius-xl: 1.5rem !default; +$border-radius-2xl: 1.5625rem !default; +$border-radius-3xl: 2rem !default; +$border-radius-50: 50% !default; +$border-radius-full: 100% !default; + +// Defines the map for use in utilities below +$border-radius: ( + none: $border-radius-none, + xs: $border-radius-xs, + sm: $border-radius-sm, + md: $border-radius-md, + lg: $border-radius-lg, + xl: $border-radius-xl, + 2xl: $border-radius-2xl, + 3xl: $border-radius-3xl, + 50: $border-radius-50, + full: $border-radius-full, + ) !default; \ No newline at end of file diff --git a/components/base/styles/common/_all.scss b/components/base/styles/common/_all.scss deleted file mode 100644 index d43f3cf..0000000 --- a/components/base/styles/common/_all.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'mixin.scss'; -@import 'core.scss'; diff --git a/components/base/styles/common/_core.scss b/components/base/styles/common/_core.scss deleted file mode 100644 index a03f90c..0000000 --- a/components/base/styles/common/_core.scss +++ /dev/null @@ -1,112 +0,0 @@ -@include export-module('base-core') { - - .sf-control, - .sf-css, - .sf-error { - font-family: $font-family; - font-size: $font-size; - font-weight: $font-weight; - } - - $white: #fff; - $background: #e82824; - $warning: #ffd800; - $yellow: #ff0; - .sf-error { - @if $skin-name == 'Material3' { - color: rgba($error-font-color); - } - @else { - color: $error-font-color; - } - } - - .sf-control, - .sf-control [class ^= 'sf-'], - .sf-control [class *= ' sf-'] { - box-sizing: border-box; - } - - .sf-control:focus, - .sf-control *:focus { - outline: none; - } - - .sf-rtl { - direction: rtl; - text-align: right; - } - - .sf-overlay { - background-color: $overlay-bg-color; - height: 100%; - opacity: .5; - pointer-events: none; - touch-action: none; - width: 100%; - } - - .sf-hidden { - display: none; - } - - .sf-blazor-hidden { - visibility: hidden; - } - - .sf-disabled { - background-image: none; - cursor: default; - opacity: .35; - } - - .sf-ul { - list-style-type: none; - } - - .sf-prevent-select { - user-select: none; - } - - .sf-warning { - @if $skin-name == 'Material3' { - color: rgba($warning-font-color); - } - @else { - color: $warning-font-color; - } - } - - .sf-success { - @if $skin-name == 'Material3' { - color: rgba($success-font-color); - } - @else { - color: $success-font-color; - } - } - - .sf-information { - @if $skin-name == 'Material3' { - color: rgba($information-font-color); - } - @else { - color: $information-font-color; - } - } - - .sf-block-touch { - touch-action: pinch-zoom; - } - .sf-license { - color: $yellow; - text-decoration: none; - } - .sf-license-banner { - position: absolute; - right: 10px; - top: 27%; - cursor: pointer; - } - } - \ No newline at end of file diff --git a/components/base/styles/definition/_material3-dark.scss b/components/base/styles/definition/_material3-dark.scss deleted file mode 100644 index a049add..0000000 --- a/components/base/styles/definition/_material3-dark.scss +++ /dev/null @@ -1,660 +0,0 @@ -@use 'sass:math'; -@use 'sass:color'; -@use 'sass:meta'; -@use 'sass:list'; -@function mapcolorvariable($pallete-name){ - @return var(#{'--color-sf-'+ $pallete-name}); -} - -@function darken-color($color, $amount) { - @if is-custom-property($color) { - @return #{$color}-#{$amount}; - } - - // maybe there is a way to call the original `darken` instead?? - @return adjust-color($color, $lightness: -1 * $amount); -} - -@mixin lighten-color($color, $amount) { - filter: brightness(#{100% + $amount}) saturate(100%) hue-rotate(0deg); - background-color: $color; -} - -@function rgbaChange($hex, $alpha: 1) { - $r: str-slice($hex, 1, 2); - $g: str-slice($hex, 3, 4); - $b: str-slice($hex, 5, 6); - $rgba: rgba(hex($r), hex($g), hex($b), $alpha); - @return $rgba; -} - -:root { - --color-sf-black: 0, 0, 0; - --color-sf-white: 255, 255, 255; - --color-sf-primary: 208, 188, 255; - --color-sf-primary-container: 79, 55, 139; - --color-sf-secondary: 204, 194, 220; - --color-sf-secondary-container: 74, 68, 88; - --color-sf-tertiary: 239, 184, 200; - --color-sf-tertiary-container: 99, 59, 72; - --color-sf-surface: 28, 27, 31; - --color-sf-surface-variant: 73, 69, 79; - --color-sf-background: var(--color-sf-surface); - --color-sf-on-primary: 55, 30, 115; - --color-sf-on-primary-container: 234, 221, 255; - --color-sf-on-secondary: 51, 45, 65; - --color-sf-on-secondary-container: 232, 222, 248; - --color-sf-on-tertiary: 73, 37, 50; - --color-sf-on-tertiary-containe: 255, 216, 228; - --color-sf-on-surface: 230, 225, 229; - --color-sf-on-surface-variant: 202, 196, 208; - --color-sf-on-background: 230, 225, 229; - --color-sf-outline: 147, 143, 153; - --color-sf-outline-variant: 68, 71, 70; - --color-sf-shadow: 0, 0, 0; - --color-sf-surface-tint-color: 208, 188, 255; - --color-sf-inverse-surface: 230, 225, 229; - --color-sf-inverse-on-surface: 49, 48, 51; - --color-sf-inverse-primary: 103, 80, 164; - --color-sf-scrim: 0, 0, 0; - --color-sf-error: 242, 184, 181; - --color-sf-error-container: 140, 29, 24; - --color-sf-on-error: 96, 20, 16; - --color-sf-on-error-container: 249, 222, 220; - --color-sf-success: 83, 202, 23; - --color-sf-success-container: 22, 62, 2; - --color-sf-on-success: 13, 39, 0; - --color-sf-on-success-container: 183, 250, 150; - --color-sf-info: 71, 172, 251; - --color-sf-info-container: 0, 67, 120; - --color-sf-on-info: 0, 51, 91; - --color-sf-on-info-container: 173, 219, 255; - --color-sf-warning: 245, 180, 130; - --color-sf-warning-container: 123, 65, 0; - --color-sf-on-warning: 99, 52, 0; - --color-sf-on-warning-container: 255, 220, 193; - --color-sf-spreadsheet-gridline: 231, 224, 236; - --color-sf-shadow-focus-ring1: 0 0 0 1px #000, 0 0 0 3px #fff; - --color-sf-success-text: 0, 0, 0; - --color-sf-warning-text: 0, 0, 0; - --color-sf-info-text: 0, 0, 0; - --color-sf-danger-text: 0, 0, 0; - --color-sf-diagram-palette-background: var(--color-sf-inverse-surface); - --color-sf-content-text-color-alt2: var(--color-sf-on-secondary); -} - -$black: mapcolorvariable('black') !default; -$white: mapcolorvariable('white') !default; - -$primary : mapcolorvariable('primary') !default; -$primary-container: mapcolorvariable('primary-container') !default; -$secondary: mapcolorvariable('secondary') !default; -$secondary-container: mapcolorvariable('secondary-container') !default; -$tertiary: mapcolorvariable('tertiary') !default; -$tertiary-container: mapcolorvariable('tertiary-container') !default; -$surface: mapcolorvariable('surface') !default; -$surface-variant: mapcolorvariable('surface-variant') !default; -$background: mapcolorvariable('background') !default; -$on-primary: mapcolorvariable('on-primary') !default; -$on-primary-container: mapcolorvariable('on-primary-container') !default; -$on-secondary: mapcolorvariable('on-secondary') !default; -$on-secondary-container: mapcolorvariable('on-secondary-container') !default; -$on-tertiary: mapcolorvariable('on-tertiary') !default; -$on-tertiary-containe: mapcolorvariable('on-tertiary-containe') !default; -$on-surface: mapcolorvariable('on-surface') !default; -$on-surface-variant: mapcolorvariable('on-surface-variant') !default; -$on-background: mapcolorvariable('on-background') !default; -$outline: mapcolorvariable('outline') !default; -$outline-variant: mapcolorvariable('outline-variant') !default; -$shadow: mapcolorvariable('shadow') !default; -$surface-tint-color: mapcolorvariable('surface-tint-color') !default; -$inverse-surface: mapcolorvariable('inverse-surface') !default; -$inverse-on-surface: mapcolorvariable('inverse-on-surface') !default; -$inverse-primary: mapcolorvariable('inverse-primary') !default; -$scrim:mapcolorvariable('scrim') !default; -$error: mapcolorvariable('error') !default; -$error-container: mapcolorvariable('error-container') !default; -$on-error: mapcolorvariable('on-error') !default; -$on-error-container: mapcolorvariable('on-error-container') !default; -$success: mapcolorvariable('success') !default; -$success-container: mapcolorvariable('success-container') !default; -$on-success: mapcolorvariable('on-success') !default; -$on-success-container: mapcolorvariable('on-success-container') !default; -$info: mapcolorvariable('info') !default; -$info-container: mapcolorvariable('info-container') !default; -$on-info: mapcolorvariable('on-info') !default; -$on-info-container: mapcolorvariable('on-info-container') !default; -$warning: mapcolorvariable('warning') !default; -$warning-container: mapcolorvariable('warning-container') !default; -$on-warning: mapcolorvariable('on-warning') !default; -$on-warning-container: mapcolorvariable('on-warning-container') !default; -$success-text: mapcolorvariable('success-text') !default; -$warning-text: mapcolorvariable('warning-text') !default; -$info-text: mapcolorvariable('info-text') !default; -$danger-text: mapcolorvariable('danger-text') !default; -$spreadsheet-gridline: mapcolorvariable('spreadsheet-gridline') !default; - -$opacity0: 0 !default; -$opacity4: .04 !default; -$opacity5: .05 !default; -$opacity6: .06 !default; -$opacity8: .08 !default; -$opacity11: .11 !default; -$opacity12: .12 !default; -$opacity14: .14 !default; -$opacity16: .16 !default; - -$surface1: linear-gradient(0deg, rgba($primary, $opacity5), rgba($primary, $opacity5)), rgba($surface) !default; -$surface2: linear-gradient(0deg, rgba($primary, $opacity8), rgba($primary, $opacity8)), rgba($surface) !default; -$surface3: linear-gradient(0deg, rgba($primary, $opacity11), rgba($primary, $opacity11)), rgba($surface) !default; -$surface4: linear-gradient(0deg, rgba($primary, $opacity12), rgba($primary, $opacity12)), rgba($surface) !default; -$surface5: linear-gradient(0deg, rgba($primary, $opacity14), rgba($primary, $opacity14)), rgba($surface) !default; -$surface6: linear-gradient(0deg, rgba($primary, $opacity16), rgba($primary, $opacity16)), rgba($surface) !default; - -$level1: 0 1px 3px 1px rgba(0, 0, 0, .15), 0 1px 2px 0 rgba(0, 0, 0, .3); -$level2: 0 2px 6px 2px rgba(0, 0, 0, .15), 0 1px 2px 0 rgba(0, 0, 0, .3); -$level3: 0 1px 3px 0 rgba(0, 0, 0, .3), 0 4px 8px 3px rgba(0, 0, 0, .15); -$level4: 0 2px 3px 0 rgba(0, 0, 0, .3), 0 6px 10px 4px rgba(0, 0, 0, .15); -$level5: 0 4px 4px 0 rgba(0, 0, 0, .3), 0 8px 12px 6px rgba(0, 0, 0, .15); - -$primary: $primary !default; -$primary-text-color: $on-primary !default; -$primary-light: $primary-container !default; -$primary-lighter: $primary-light !default; -$primary-dark: $surface-tint-color !default; -$primary-darker: $on-primary-container !default; -$success: $success !default; -$transparent: transparent !default; -$info: $info !default; -$warning: $warning !default; -$danger: $error !default; -$success-light: $success-container !default; -$info-light: $info-container !default; -$warning-light: $warning-container !default; -$danger-light: $error-container !default; -$success-dark: $success !default; -$info-dark: $info !default; -$warning-dark: $warning !default; -$danger-dark:$error !default; -$success-light-alt: $success-light !default; -$info-light-alt: $info-light !default; -$warning-light-alt: $warning-light !default; -$danger-light-alt: $danger-light !default; - -$content-bg-color: $surface !default; -$content-bg-color-alt1: $surface1 !default; -$content-bg-color-alt2: $surface2 !default; -$content-bg-color-alt3: $surface3 !default; -$content-bg-color-alt4: $surface4 !default; -$content-bg-color-alt5: $surface5 !default; -$content-bg-color-alt6: $surface6 !default; -$content-bg-color-hover: rgba($on-surface, $opacity5) !default; -$content-bg-color-pressed: rgba($on-surface, $opacity8) !default; -$content-bg-color-focus: rgba($on-surface, $opacity4) !default; -$content-bg-color-selected: $primary-light !default; -$content-bg-color-dragged: $primary-light !default; -$content-bg-color-disabled: $white !default; -$flyout-bg-color: $surface3 !default; -$flyout-bg-color-hover: rgba($on-surface, $opacity5) !default; -$flyout-bg-color-pressed: rgba($on-surface, $opacity8) !default; -$flyout-bg-color-selected: rgba($primary-container, .65) !default; -$flyout-bg-color-focus: rgba($on-surface, $opacity4) !default; -$overlay-bg-color: rgba($scrim, .5) !default; -$table-bg-color-hover: rgba($on-surface, $opacity5) !default; -$table-bg-color-pressed: rgba($on-surface, $opacity8) !default; -$table-bg-color-selected: rgba($primary-container, .65) !default; - -$colorpicker-gradient-1: #f00 !default; -$colorpicker-gradient-2: #ff0 !default; -$ccolorpicker-gradient-3: #0f0 !default; -$colorpicker-gradient-4: #0ff !default; -$colorpicker-gradient-5: #00f !default; -$colorpicker-gradient-6: #f0f !default; -$colorpicker-gradient-7: #ff0004 !default; -$spreadsheet-selection-1: #673ab8 !default; -$spreadsheet-selection-2: #9c27b0 !default; -$spreadsheet-selection-3: #029688 !default; -$spreadsheet-selection-4: #4caf51 !default; -$spreadsheet-selection-5: #fe9800 !default; -$spreadsheet-selection-6: #3f52b5 !default; - -$content-text-color: $on-surface !default; -$content-text-color-alt1: $on-surface-variant !default; -$content-text-color-alt2: $on-surface-variant !default; -$content-text-color-alt3: $on-tertiary !default; -$content-text-color-inverse: $inverse-on-surface !default; -$content-text-color-hover: $content-text-color !default; -$content-text-color-pressed: $content-text-color !default; -$content-text-color-focus: $content-text-color !default; -$content-text-color-selected: $content-text-color !default; -$content-text-color-dragged: $content-text-color !default; -$content-text-color-disabled: rgba($on-surface, .38) !default; -$placeholder-text-color: $outline !default; -$flyout-text-color: $content-text-color !default; -$flyout-text-color-hover: $content-text-color !default; -$flyout-text-color-pressed: $content-text-color !default; -$flyout-text-color-selected: $content-text-color !default; -$flyout-text-color-focus: $content-text-color !default; -$flyout-text-color-disabled: rgba($on-surface, .38) !default; -$table-text-color-hover: $content-text-color !default; -$table-text-color-pressed: $content-text-color !default; -$table-text-color-selected: $content-text-color !default; - -$icon-color: $on-surface-variant !default; -$icon-color-hover: $on-surface !default; -$icon-color-pressed: $on-surface-variant !default; -$icon-color-disabled: rgba($on-surface-variant, .38) !default; - -$border-light: $outline-variant !default; -$border: $outline !default; -$border-alt: $on-surface-variant !default; -$border-dark: rgba($on-surface, .38) !default; -$border-hover: $border-light !default; -$border-pressed: $border-light !default; -$border-focus: $border-light !default; -$border-selected: $border-light !default; -$border-dragged: $border-light !default; -$border-disabled: $border-light !default; -$border-warning: $warning !default; -$border-error: $error !default; -$border-success: $success !default; -$spreadsheet-gridline: #e7e0ec !default; -$flyout-border: $border-light !default; - -$tooltip-bg-color: $inverse-surface !default; -$tooltip-border: $inverse-surface; -$tooltip-text-color: $inverse-on-surface !default; - -$shadow: 0 .8px 16px rgba($black, .15) !default; -$shadow-sm: $level1 !default; -$shadow-md: $level2 !default; -$shadow-lg: $level3 !default; -$shadow-xl: $level4 !default; -$shadow-2xl: $level5 !default; -$shadow-inner: inset 0 1px 2px rgba($black, .075) !default; -$shadow-none: 0 0 rgba($black, 0) !default; - -$shadow-focus-ring1: 0 0 0 1px #000, 0 0 0 3px #fff !default; -$shadow-focus-ring2: 0 0 0 1px rgba($black, .95) !default; -$primary-shadow-focus: 0 0 0 4px rgba($primary, .5) !default; -$secondary-shadow-focus: 0 0 0 4px rgba($secondary, .5) !default; -$success-shadow-focus: 0 0 0 4px rgba($success, .5) !default; -$danger-shadow-focus: 0 0 0 4px rgba($danger, .5) !default; -$info-shadow-focus: 0 0 0 4px rgba($info, .5) !default; -$warning-shadow-focus: 0 0 0 4px rgba($warning, .5) !default; - -$font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif !default; - -$text-xxs: 10px !default; -$text-xs: 12px !default; -$text-sm: 14px !default; -$text-base: 16px !default; -$text-lg: 18px !default; -$text-xl: 20px !default; -$text-2xl: 24px !default; -$text-3xl: 28px !default; -$text-4xl: 32px !default; -$text-5xl: 42px !default; -$text-6xl: 68px !default; -$text-7xl: 78px !default; -$text-8xl: 96px !default; -$text-9xl: 128px !default; - -$h1-font-size: 40px !default; -$h2-font-size: 32px !default; -$h3-font-size: 28px !default; -$h4-font-size: $text-2xl !default; -$h5-font-size: $text-xl !default; -$h6-font-size: $text-base !default; - -$leading-none: 1 !default; -$leading-tight: 1.25 !default; -$leading-snug: 1.375 !default; -$leading-normal: 1.5 !default; -$leading-relaxed: 1.625 !default; -$leading-loose: 2 !default; - -$font-weight-lighter: lighter !default; -$font-weight-light: 300 !default; -$font-weight-normal: 400 !default; -$font-weight-medium: 600 !default; -$font-weight-bold: 700 !default; -$font-weight-bolder: bolder !default; - -$secondary-bg-color: $content-bg-color-alt1 !default; -$secondary-border-color: linear-gradient(0deg, rgba($primary, $opacity5), rgba($primary, $opacity5)) !default; -$secondary-text-color: $on-surface !default; -$secondary-bg-color-hover: linear-gradient(0deg, rgba($secondary-text-color, $opacity8), rgba($secondary-text-color, $opacity8)), rgba($content-bg-color) !default; -$secondary-border-color-hover: linear-gradient(0deg, rgba($secondary-text-color, $opacity8), rgba($secondary-text-color, $opacity8)) !default; -$secondary-text-color-hover: $secondary-text-color !default; -$secondary-bg-color-pressed:linear-gradient(0deg, rgba($secondary-text-color, $opacity12), rgba($secondary-text-color, $opacity12)), rgba($content-bg-color) !default; -$secondary-border-color-pressed: linear-gradient(0deg, rgba($secondary-text-color, $opacity12), rgba($secondary-text-color, $opacity12)) !default; -$secondary-text-color-pressed: $secondary-text-color !default; -$secondary-bg-color-focus: $secondary-bg-color-hover !default; -$secondary-border-color-focus: $secondary-border-color-hover !default; -$secondary-text-color-focus: $secondary-text-color-hover !default; -$secondary-bg-color-disabled: rgba($secondary, .12) !default; -$secondary-border-color-disabled: rgba($secondary, 0) !default; -$secondary-text-color-disabled: rgba($on-surface, .38) !default; - -$primary-bg-color: $primary !default; -$primary-border-color: $primary !default; -$primary-text: $primary-text-color !default; -$primary-bg-color-hover: linear-gradient(0deg, rgba($primary-text, $opacity8), rgba($primary-text, $opacity8)), rgba($primary-bg-color) !default; -$primary-border-color-hover: $primary-bg-color-hover !default; -$primary-text-hover: $primary-text !default; -$primary-bg-color-pressed: linear-gradient(0deg, rgba($primary-text, $opacity12), rgba($primary-text, $opacity12)), rgba($primary-bg-color) !default; -$primary-border-color-pressed: $primary-bg-color-pressed !default; -$primary-text-pressed: $primary-text-color !default; -$primary-bg-color-focus: $primary-bg-color-hover !default; -$primary-border-color-focus: $primary-border-color-hover !default; -$primary-text-focus: $primary-text !default; -$primary-bg-color-disabled: rgba($secondary, .12) !default; -$primary-border-color-disabled: rgba($secondary, 0) !default; -$primary-text-disabled: rgba($on-surface, .38) !default; - -$success-bg-color: $success !default; -$success-border-color: $success !default; -$success-text: $black !default; -$success-bg-color-hover: linear-gradient(0deg, rgba($success-text, $opacity8), rgba($success-text, $opacity8)), rgba($success-bg-color) !default; -$success-border-color-hover: $success-bg-color-hover !default; -$success-text-hover: $success-text !default; -$success-bg-color-pressed: linear-gradient(0deg, rgba($success-text, $opacity12), rgba($success-text, $opacity12)), rgba($success-bg-color) !default; -$success-border-color-pressed: $success-bg-color-pressed !default; -$success-text-pressed: $success-text !default; -$success-bg-color-focus: $success-bg-color-hover !default; -$success-border-color-focus: $success-border-color-hover !default; -$success-text-focus: $success-text !default; -$success-bg-color-disabled: rgba($secondary, .12) !default; -$success-border-color-disabled: $success-bg-color-disabled !default; -$success-text-disabled: rgba($on-surface, .38) !default; - -$warning-bg-color: $warning !default; -$warning-border-color: $warning !default; -$warning-text: $black !default; -$warning-bg-color-hover: linear-gradient(0deg, rgba($warning-text, $opacity8), rgba($warning-text, $opacity8)), rgba($warning-bg-color) !default; -$warning-border-color-hover: $warning-bg-color-hover !default; -$warning-text-hover: $warning-text !default; -$warning-bg-color-pressed: linear-gradient(0deg, rgba($warning-text, $opacity12), rgba($warning-text, $opacity12)), rgba($warning-bg-color) !default; -$warning-border-color-pressed: $warning-bg-color-pressed !default; -$warning-text-pressed: $warning-text !default; -$warning-bg-color-focus: $warning-bg-color-hover !default; -$warning-border-color-focus: $warning-border-color-hover !default; -$warning-text-focus: $warning-text !default; -$warning-bg-color-disabled: rgba($secondary, .12) !default; -$warning-border-color-disabled: $warning-bg-color-disabled !default; -$warning-text-disabled: rgba($on-surface, .38) !default; - -$danger-bg-color: $danger !default; -$danger-border-color: $danger !default; -$danger-text: $black !default; -$danger-bg-color-hover: linear-gradient(0deg, rgba($danger-text, $opacity8), rgba($danger-text, $opacity8)), rgba($danger-bg-color) !default; -$danger-border-color-hover: $danger-bg-color-hover !default; -$danger-text-hover: $danger-text !default; -$danger-bg-color-pressed: linear-gradient(0deg, rgba($danger-text, $opacity12), rgba($danger-text, $opacity12)), rgba($danger-bg-color) !default; -$danger-border-color-pressed: $danger-bg-color-pressed !default; -$danger-text-pressed: $danger-text !default; -$danger-bg-color-focus: $danger-bg-color-hover !default; -$danger-border-color-focus: $danger-border-color-hover !default; -$danger-text-focus: $danger-text !default; -$danger-bg-color-disabled: rgba($secondary, .12) !default; -$danger-border-color-disabled: $danger-bg-color-disabled !default; -$danger-text-disabled: rgba($on-surface, .38) !default; - -$info-text: $black !default; -$info-bg-color: $info !default; -$info-border-color: $info-bg-color !default; -$info-bg-color-hover: linear-gradient(0deg, rgba($info-text, $opacity8), rgba($info-text, $opacity8)), rgba($info-bg-color) !default; -$info-border-color-hover: $info-bg-color-hover !default; -$info-text-hover: $info-text !default; -$info-bg-color-pressed: linear-gradient(0deg, rgba($info-text, $opacity12), rgba($info-text, $opacity12)), rgba($info-bg-color) !default; -$info-border-color-pressed: $info-bg-color-pressed !default; -$info-text-pressed: $info-text !default; -$info-bg-color-focus: $info-bg-color-hover !default; -$info-border-color-focus: $info-border-color-hover !default; -$info-text-focus: $info-text-hover !default; -$info-bg-color-disabled: rgba($secondary, .12) !default; -$info-border-color-disabled: $info-bg-color-disabled !default; -$info-text-disabled: rgba($on-surface, .38) !default; - -$primary-outline: $primary-bg-color !default; -$primary-outline-border: $outline !default; -$secondary-outline: $secondary-text-color !default; -$secondary-outline-border: $outline !default; -$warning-outline: $warning-bg-color !default; -$warning-outline-border: $outline !default; -$danger-outline: $danger-bg-color !default; -$danger-outline-border: $outline !default; -$success-outline: $success-bg-color !default; -$success-outline-border: $outline !default; -$info-outline: $info-bg-color !default; -$info-outline-border:$outline !default; - -$toast-text-color: $content-text-color !default; -$toast-alt-text-color: $content-text-color !default; - -$series-1: $surface-variant !default; -$series-2: $outline-variant !default; -$series-3: $outline !default; -$series-4: $on-surface-variant !default; -$series-5: #6200ee !default; -$series-6: #e77a16 !default; -$series-7: #82c100 !default; -$series-8: #7107dc !default; -$series-9: #05b3da !default; -$series-10: #828486 !default; -$series-11: #b1212d !default; -$series-12: #38be09 !default; -$skin-name: 'Material3' !default; -$theme-name: 'Material3-dark' !default; - -$diagram-palette-background: $inverse-surface !default; -$diagram-palette-hover-background: rgba($surface, .05) !default; -$diagram-palette-selection-background: rgba($surface, .12) !default; - -$shape-none:0 !default; -$shape-extra-small:4px !default; -$shape-small:8px !default; -$shape-medium:12px !default; -$shape-Large:16px !default; -$shape-extra-large:16px !default; -$shape-full:50% !default; - -$button-radius:$shape-full !default; -$button-radius-small: $shape-full !default; -$button-radius-bigger: $shape-full !default; -$input-radius: $shape-extra-small !default; -$input-radius-small: $shape-extra-small !default; -$input-radius-bigger: $shape-extra-small !default; -$model-radius: $shape-medium !default; -$model-radius-small: $shape-small !default; -$model-radius-bigger: $shape-Large !default; -$flyout-radius: $shape-extra-small !default; -$flyout-radius-bigger: $shape-extra-small !default; -$flyout-radius-small: $shape-extra-small !default; -$chkbox-radius:2px !default; -$chkbox-radius-small:2px !default; -$chkbox-radius-bigger:2px !default; -$card-radius:$shape-small !default; -$card-radius-small:$shape-extra-small !default; -$card-radius-bigger:$shape-medium !default; -$msg-radius:$shape-none !default; -$msg-radius-small: $msg-radius !default; -$msg-radius-bigger: $msg-radius !default; -$toast-radius: $shape-extra-small !default; -$toast-radius-small: $toast-radius !default; -$toast-radius-bigger: $toast-radius !default; -$chip-radius: $shape-extra-small !default; -$chip-radius-small: $shape-extra-small !default; -$chip-radius-bigger: $shape-small !default; - -$btn-secondary-border-color: linear-gradient(0deg, rgba($content-bg-color, 0), rgba($content-bg-color, 0)) !default; - -$msg-color: rgba($on-surface) !default; -$msg-bg-color: $content-bg-color-alt1 !default; -$msg-border-color: linear-gradient(0deg, rgba($primary, $opacity5), rgba($primary, $opacity5)) !default; -$msg-color-alt1: rgba($on-surface) !default; -$msg-bg-color-alt1: $transparent !default; -$msg-border-color-alt1: rgba($outline) !default; -$msg-color-alt2: rgba($inverse-on-surface) !default; -$msg-bg-color-alt2: rgba($inverse-surface) !default; -$msg-border-color-alt2: rgba($inverse-surface) !default; - -$msg-icon-color: rgba($on-surface-variant) !default; -$msg-icon-color-alt1: rgba($on-surface-variant) !default; -$msg-icon-color-alt2: rgba($inverse-on-surface) !default; - -$msg-close-icon-color: rgba($icon-color) !default; -$msg-close-icon-color-alt1: rgba($icon-color) !default; -$msg-close-icon-color-alt2: rgba($inverse-on-surface) !default; - -$msg-success-color: rgba($on-success-container) !default; -$msg-success-bg-color: rgba($success-container) !default; -$msg-success-border-color: rgba($success-container) !default; -$msg-success-color-alt1: rgba($on-success-container) !default; -$msg-success-bg-color-alt1: $transparent !default; -$msg-success-border-color-alt1: rgba($success) !default; -$msg-success-color-alt2: rgba($on-success) !default; -$msg-success-bg-color-alt2: rgba($success) !default; -$msg-success-border-color-alt2: rgba($success) !default; - -$msg-success-icon-color: rgba($success) !default; -$msg-success-icon-color-alt1: rgba($on-success-container) !default; -$msg-success-icon-color-alt2: rgba($on-success) !default; - -$msg-success-close-icon-color: rgba($icon-color) !default; -$msg-success-close-icon-color-alt1: rgba($icon-color) !default; -$msg-success-close-icon-color-alt2: rgba($on-success) !default; - -$msg-danger-color: rgba($on-error-container) !default; -$msg-danger-bg-color: rgba($error-container) !default; -$msg-danger-border-color: rgba($error-container) !default; -$msg-danger-color-alt1: rgba($on-error-container) !default; -$msg-danger-bg-color-alt1: $transparent !default; -$msg-danger-border-color-alt1: rgba($error) !default; -$msg-danger-color-alt2: rgba($on-error) !default; -$msg-danger-bg-color-alt2: rgba($error) !default; -$msg-danger-border-color-alt2: rgba($error) !default; - -$msg-danger-icon-color: rgba($error) !default; -$msg-danger-icon-color-alt1: rgba($on-error-container) !default; -$msg-danger-icon-color-alt2: rgba($on-error) !default; - -$msg-danger-close-icon-color: rgba($icon-color) !default; -$msg-danger-close-icon-color-alt1: rgba($icon-color) !default; -$msg-danger-close-icon-color-alt2: rgba($on-error) !default; - -$msg-warning-color: rgba($on-warning-container) !default; -$msg-warning-bg-color: rgba($warning-container) !default; -$msg-warning-border-color: rgba($warning-container) !default; -$msg-warning-color-alt1: rgba($on-warning-container) !default; -$msg-warning-bg-color-alt1: $transparent !default; -$msg-warning-border-color-alt1: rgba($warning) !default; -$msg-warning-color-alt2: rgba($on-warning) !default; -$msg-warning-bg-color-alt2: rgba($warning) !default; -$msg-warning-border-color-alt2: rgba($warning) !default; - -$msg-warning-icon-color: rgba($warning) !default; -$msg-warning-icon-color-alt1: rgba($on-warning-container) !default; -$msg-warning-icon-color-alt2: rgba($on-warning) !default; - -$msg-warning-close-icon-color: rgba($icon-color) !default; -$msg-warning-close-icon-color-alt1: rgba($icon-color) !default; -$msg-warning-close-icon-color-alt2: rgba($on-warning) !default; - -$msg-info-color: rgba($on-info-container) !default; -$msg-info-bg-color: rgba($info-container) !default; -$msg-info-border-color: rgba($info-container) !default; -$msg-info-color-alt1: rgba($on-info-container) !default; -$msg-info-bg-color-alt1: $transparent !default; -$msg-info-border-color-alt1: rgba($info) !default; -$msg-info-color-alt2: rgba($on-info) !default; -$msg-info-bg-color-alt2: rgba($info) !default; -$msg-info-border-color-alt2: rgba($info) !default; - -$msg-info-icon-color: rgba($info) !default; -$msg-info-icon-color-alt1: rgba($on-info-container) !default; -$msg-info-icon-color-alt2: rgba($on-info) !default; - -$msg-info-close-icon-color: rgba($icon-color) !default; -$msg-info-close-icon-color-alt1: rgba($icon-color) !default; -$msg-info-close-icon-color-alt2: rgba($on-info) !default; - -$appbar-bg-color-alt1: $content-bg-color-alt2 !default; -$appbar-color-alt1: rgba($content-text-color) !default; -$appbar-border-color-alt1: linear-gradient(0deg, rgba($primary, $opacity8), rgba($primary, $opacity8)) !default; -$appbar-hover-bg-color-alt1: rgba(0, 0, 0, .05) !default; - -$appbar-bg-color-alt2: rgba($inverse-surface) !default; -$appbar-color-alt2: rgba($inverse-on-surface) !default; -$appbar-border-color-alt2: rgba($inverse-surface) !default; -$appbar-hover-bg-color-alt2: rgba(255, 255, 255, .08) !default; - -$appbar-bottom-shadow: 0 1.6px 3.6px rgba(0, 0, 0, .13), 0 .3px .9px rgba(0, 0, 0, .1) !default; -$appbar-top-shadow: 0 -1.6px 3.6px rgba(0, 0, 0, .13), 0 -.3px .9px rgba(0, 0, 0, .1) !default; - -$rating-selected-color: $primary !default; -$rating-unrated-color: $content-bg-color-alt3 !default; -$rating-selected-disabled-color: rgba($on-surface, .24) !default; -$rating-unrated-disabled-color: rgba($on-surface, .08) !default; -$rating-selected-hover-color: darken-color(rgba($primary), 5%) !default; -$rating-unrated-hover-color: darken-color(rgba($primary), 10%) !default; -$rating-pressed-color: darken-color(rgba($primary), 10%) !default; - -$skeleton-wave-color: rgba(73, 69, 79, 1) !default; - -$splitbtn-right-border: linear-gradient(to right, rgba(190, 162, 255, 1) 25%, rgba(190, 162, 255, 1) 75%) 1 !default; -$splitbtn-right-border-rtl: linear-gradient(to left, rgba($content-bg-color, 1) 25%, rgba($border-light, 1) 75%) 1 !default; -$splitbtn-right-border-vertical: linear-gradient(to bottom, rgba($content-bg-color, 1) 25%, rgba($border-light, 1) 75%) 1 !default; - -$gantt-taskbar-color: linear-gradient(0deg, rgba($primary, .75), rgba($primary, .75)), rgba($content-bg-color) !default; - -$grey-white: #fff !default; -$base-font: #000 !default; -$grey-50: #fafafa !default; -$grey-300: #e0e0e0 !default; -$grey-400: #bdbdbd !default; -$grey-500: #9e9e9e !default; -$grey-600: #757575 !default; -$grey-700: #616161 !default; -$grey-800: #424242 !default; -$grey-900: #212121 !default; -$sd-focus-box-shadow: $secondary-shadow-focus !default; -$toooltip-text-color: #f9fafb !default; - -$range-button-font-color: $info !default; -$ddl-input-placeholder-padding: 0 0 0 8px !default; -$theme-light: $primary-light !default; -$gray-600: #6c757d !default; -$gray-300: #dee2e6 !default; -$gray-500: #adb5bd !default; -$gray-900: #212529 !default; -$primary-300: #7986cb; -$gray-400: #ced4da !default; - -$font-icon-8: 8px !default; -$font-icon-9: 9px !default; -$font-icon-10: 10px !default; -$font-icon-12: 12px !default; -$font-icon-13: 13px !default; -$font-icon-14: 14px !default; -$font-icon-15: 15px !default; -$font-icon-16: 16px !default; -$font-icon-17: 17px !default; -$font-icon-18: 18px !default; -$font-icon-20: 20px !default; -$font-icon-22: 22px !default; -$font-icon-24: 24px !default; -$font-icon-26: 26px !default; -$font-icon-28: 28px !default; -$font-icon-32: 32px !default; - -$font-size: 12px !default; -$font-weight: 400 !default; -$error-font-color: $danger !default; -$warning-font-color: $warning !default; -$success-font-color: $success !default; -$information-font-color: $info !default; - -$frozen-shadow: rgba(0, 0, 0, .25) !default; -$frozen-shadow-2: rgba(0, 0, 0, .25) !default; \ No newline at end of file diff --git a/components/base/styles/functions/_all.scss b/components/base/styles/functions/_all.scss new file mode 100644 index 0000000..ac214dd --- /dev/null +++ b/components/base/styles/functions/_all.scss @@ -0,0 +1,2 @@ +@import './calc'; +@import './core'; \ No newline at end of file diff --git a/components/base/styles/functions/_calc.scss b/components/base/styles/functions/_calc.scss new file mode 100644 index 0000000..8969b64 --- /dev/null +++ b/components/base/styles/functions/_calc.scss @@ -0,0 +1,43 @@ +@use "sass:math"; + +/// Calculate a rem value from pixel value +/// @param {Number} $pixels - The pixel value +/// @param {Number} $base - The base font size in pixels (default: 16px) +/// @return {Number} - The rem value +/// +/// @example scss - Set consistent component spacing +/// .sf-card { +/// padding: sf-rem(16px); +/// margin-bottom: sf-rem(24px); +/// } +/// +/// @example scss - Create responsive typography +/// .sf-title { +/// font-size: sf-rem(20px); +/// line-height: sf-rem(28px); +/// } +/// +@function sf-rem($pixels, $base: 16px) { + @return math.div($pixels, $base) * 1rem; +} + +/// Calculate a em value from pixel value +/// @param {Number} $pixels - The pixel value +/// @param {Number} $base - The base font size in pixels (default: 16px) +/// @return {Number} - The em value +/// +/// @example scss - Create icon sizing relative to text +/// .sf-icon { +/// width: sf-em(20px); +/// height: sf-em(20px); +/// } +/// +/// @example scss - Set component padding relative to font size +/// .sf-button { +/// padding: sf-em(8px) sf-em(16px); +/// border-radius: sf-em(4px); +/// } +/// +@function sf-em($pixels, $base: 16px) { + @return math.div($pixels, $base) * 1em; +} \ No newline at end of file diff --git a/components/base/styles/functions/_core.scss b/components/base/styles/functions/_core.scss new file mode 100644 index 0000000..5a0b447 --- /dev/null +++ b/components/base/styles/functions/_core.scss @@ -0,0 +1,6 @@ +/// Retrieves a color from the theme palette by variable name +/// @param {String} $palette-name - Name of the color in the palette +/// @return {Color} CSS variable reference to the color +@function sf-color($palette-name) { + @return var(#{'--color-sf-' + $palette-name}); +} \ No newline at end of file diff --git a/components/base/styles/material3-dark.scss b/components/base/styles/material3-dark.scss deleted file mode 100644 index 63defcb..0000000 --- a/components/base/styles/material3-dark.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'material3-dark-definition.scss'; -@import 'all.scss'; \ No newline at end of file diff --git a/components/base/styles/material3.scss b/components/base/styles/material3.scss deleted file mode 100644 index 2e9c602..0000000 --- a/components/base/styles/material3.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'material3-definition.scss'; -@import 'all.scss'; \ No newline at end of file diff --git a/components/base/styles/mixins/_all.scss b/components/base/styles/mixins/_all.scss new file mode 100644 index 0000000..382654a --- /dev/null +++ b/components/base/styles/mixins/_all.scss @@ -0,0 +1,3 @@ +@import './decorator.scss'; +@import './_radius.scss'; +@import './core.scss'; \ No newline at end of file diff --git a/components/base/styles/common/_mixin.scss b/components/base/styles/mixins/_core.scss similarity index 100% rename from components/base/styles/common/_mixin.scss rename to components/base/styles/mixins/_core.scss diff --git a/components/base/styles/mixins/_decorator.scss b/components/base/styles/mixins/_decorator.scss new file mode 100644 index 0000000..97edf4d --- /dev/null +++ b/components/base/styles/mixins/_decorator.scss @@ -0,0 +1,44 @@ +/// A flexible mixin to apply various fill styles such as text color, background, and border styles +/// +/// @param {Color} $color - Sets the text color. (optional) +/// @param {Color} $bg - Sets the background color (optional) +/// @param {Color} $bg-color - Sets the background color property specifically (optional) +/// @param {Color} $border-color - Sets the border color (optional) +/// @param {Color} $border-image - Sets the border image (optional) +/// +/// @example scss - Simple button with primary styling +/// .sf-button--primary { +/// @include sf-fill($on-primary, $primary, $primary); +/// } +/// +/// @example scss - Card with background and gradient +/// .sf-card { +/// @include sf-fill( +/// $on-primary-container, +/// linear-gradient(0deg,rgba(var(--color-sf-primary),.05),rgba(var(--color-sf-primary),.05)),rgba(var(--color-sf-surface)), +/// null, +/// null, +/// linear-gradient(0deg,rgba(var(--color-sf-primary),.05),rgba(var(--color-sf-primary),.05)) +/// ); +/// } +@mixin sf-fill($color: null, $bg: null, $bg-color: null, $border-color: null, $border-image: null) { + @if $color { + color: $color; + } + + @if $bg { + background: $bg; + } + + @if $bg-color { + background-color: $bg-color; + } + + @if $border-color { + border-color: $border-color; + } + + @if $border-image { + border-image: $border-image; + } +} \ No newline at end of file diff --git a/components/base/styles/mixins/_radius.scss b/components/base/styles/mixins/_radius.scss new file mode 100644 index 0000000..4a551ef --- /dev/null +++ b/components/base/styles/mixins/_radius.scss @@ -0,0 +1,64 @@ +/// Border radius mixin +/// @param {Length} $radius - Border radius value +/// @param {Boolean} $important - Whether to use !important +/// +/// @example scss - Apply default border radius to a card +/// .sf-card { +/// @include sf-border-radius(); +/// } +/// +/// @example scss - Apply custom border radius to a button with !important +/// .sf-button { +/// @include sf-border-radius(8px); +/// } +@mixin sf-border-radius($radius: $border-radius-xs) { + border-radius: $radius; +} + +/// Top border radius mixin +/// @param {Length} $radius - Border radius value +/// +/// @example scss - Rounded top corners for a modal +/// .sf-modal { +/// @include sf-border-top-radius(16px); +/// } +@mixin sf-border-top-radius($radius: $border-radius-xs) { + border-top-left-radius: $radius; + border-top-right-radius: $radius; +} + +/// Bottom border radius mixin +/// @param {Length} $radius - Border radius value +/// +/// @example scss - Rounded bottom corners for a drawer +/// .sf-drawer { +/// @include sf-border-bottom-radius(10px); +/// } +@mixin sf-border-bottom-radius($radius: $border-radius-xs) { + border-bottom-left-radius: $radius; + border-bottom-right-radius: $radius; +} + +/// Left border radius mixin +/// @param {Length} $radius - Border radius value +/// +/// @example scss - Rounded left corners for a chip +/// .sf-chip { +/// @include sf-border-left-radius(6px); +/// } +@mixin sf-border-left-radius($radius: $border-radius-xs) { + border-top-left-radius: $radius; + border-bottom-left-radius: $radius; +} + +/// Right border radius mixin +/// @param {Length} $radius - Border radius value +/// +/// @example scss - Rounded right corners for a chip +/// .sf-chip { +/// @include sf-border-right-radius(6px); +/// } +@mixin sf-border-right-radius($radius: $border-radius-xs) { + border-top-right-radius: $radius; + border-bottom-right-radius: $radius; +} \ No newline at end of file diff --git a/components/base/styles/styles/_all.scss b/components/base/styles/styles/_all.scss new file mode 100644 index 0000000..0bcb9f8 --- /dev/null +++ b/components/base/styles/styles/_all.scss @@ -0,0 +1,5 @@ +@import './animation'; +@import './base'; +@import './layout'; +@import './resize'; +@import './theme'; \ No newline at end of file diff --git a/components/base/styles/animation/_all.scss b/components/base/styles/styles/_animation.scss similarity index 89% rename from components/base/styles/animation/_all.scss rename to components/base/styles/styles/_animation.scss index 9845ebf..644ee05 100644 --- a/components/base/styles/animation/_all.scss +++ b/components/base/styles/styles/_animation.scss @@ -463,43 +463,4 @@ transform-style: preserve-3d; } } - - #{&} .sf-ripple-wrapper - { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - overflow: hidden; - border-radius: inherit; - pointer-events: none; - } - - $ripple-background: rgba(0, 0, 0, .1); - $ripple-background-m3: linear-gradient(90deg, rgba(28, 27, 31, .08) 0%, rgba(28, 27, 31, .1) 5%, rgba(28, 27, 31, .1) 50%, rgba(28, 27, 31, .1) 50%, rgba(28, 27, 31, .1) 95%, rgba(28, 27, 31, .08) 100%); - - #{&} .sf-ripple-element - { - @if $skin-name =='Material3' { - background: $ripple-background-m3; - } - - @else { - background-color: $ripple-background; - } - border-radius: 0; - overflow: hidden; - pointer-events: none; - position: absolute; - transform: scale(0); - - @if $skin-name =='Material3' { - transition: opacity .3s transform 50ms cubic-bezier(.2, 0, 0, 1); - } - - @else { - transition: opacity, transform 0ms cubic-bezier(0, .1, .2, 1); - } - } } diff --git a/components/base/styles/styles/_base.scss b/components/base/styles/styles/_base.scss new file mode 100644 index 0000000..3420433 --- /dev/null +++ b/components/base/styles/styles/_base.scss @@ -0,0 +1,77 @@ +/// +/// @example scss - Basic component structure +///
Component content
+/// +/// @example scss - State handling with status classes +///
Warning message goes here
+/// +@include export-module('base-core') { + + // Base control styles + .sf-control { + font-family: $font-family; + font-size: $font-size; + font-weight: $font-weight; + line-height: $leading-normal; + &:focus, + *:focus { + outline: none; + } + } + + .sf-size-small { + font-size: $font-size-12; + line-height: $leading-small; + } + + .sf-size-medium { + font-size: $font-size-14; + line-height: $leading-medium; + } + + .sf-size-large { + font-size: $font-size-16; + line-height: $leading-normal + } + + .sf-size-extra-large { + font-size: $font-size-24; + line-height: $leading-small; + } + + // Box-sizing reset + .sf-control, + .sf-control [class^='sf-'], + .sf-control [class*=' sf-'] { + box-sizing: border-box; + } + + // Ripple effect utilities + .sf-ripple-wrapper { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + overflow: hidden; + border-radius: inherit; + pointer-events: none; + } + + .sf-ripple-element { + border-radius: 0; + overflow: hidden; + pointer-events: none; + position: absolute; + transform: scale(0); + transition: opacity .3s transform 50ms cubic-bezier(.2, 0, 0, 1); + } + + // License-related styles + .sf-license-banner { + position: absolute; + right: 10px; + top: 27%; + cursor: pointer; + } +} \ No newline at end of file diff --git a/components/base/styles/styles/_layout.scss b/components/base/styles/styles/_layout.scss new file mode 100644 index 0000000..9df5748 --- /dev/null +++ b/components/base/styles/styles/_layout.scss @@ -0,0 +1,179 @@ +@include export-module('base-layout') { + + // Positioning utilities + .sf-pos-absolute { position: absolute; } + .sf-pos-relative { position: relative; } + + // Display utilities + .sf-display-none { display: none; } + .sf-display-block { display: block; } + .sf-display-flex { display: flex; } + .sf-display-inline-block { display: inline-block; } + .sf-display-inline { display: inline; } + .sf-display-inline-flex { display: inline-flex; } + .sf-display-table { display: table; } + + // Content utilities + .sf-content-start, .sf-content-left { + display: flex; + justify-content: flex-start; + align-items: flex-start; + } + .sf-content-center { + display: flex; + justify-content: center; + align-items: center; + } + .sf-content-end, .sf-content-right { + display: flex; + justify-content: flex-end; + align-items: flex-end; + } + .sf-content-between { + display: flex; + justify-content: space-between; + align-items: center; + } + .sf-title { + font-size: $font-size-24; + line-height: $leading-small; + } + .sf-content { + outline: 0; + font-size: $font-size-14; + line-height: $leading-medium; + height: 100%; + white-space: normal; + width: 100%; + overflow-wrap: break-word; + } + + // Icon utility classes + .sf-icon { + display: inline-flex; + vertical-align: middle; + align-items: center; + justify-content: center; + } + + .sf-icon-size { + width: 1em; + height: 1em; + font-size: $font-size-16; + } + + // FONT SIZE VARIANTS + .sf-font-size-11 { font-size: $font-size-11; } + .sf-font-size-12 { font-size: $font-size-12; } + .sf-font-size-13 { font-size: $font-size-13; } + .sf-font-size-14 { font-size: $font-size-14; } + .sf-font-size-15 { font-size: $font-size-15; } + .sf-font-size-16 { font-size: $font-size-16; } + .sf-font-size-18 { font-size: $font-size-18; } + .sf-font-size-20 { font-size: $font-size-20; } + .sf-font-size-24 { font-size: $font-size-24; } + .sf-font-size-26 { font-size: $font-size-26; } + .sf-font-size-28 { font-size: $font-size-28; } + .sf-font-size-30 { font-size: $font-size-30; } + .sf-font-size-32 { font-size: $font-size-32; } + + // RTL support + .sf-rtl { + direction: rtl; + text-align: right; + } + + // Overlay styling + .sf-overlay { + height: 100%; + opacity: .5; + pointer-events: none; + touch-action: none; + width: 100%; + } + + // Layout interaction utilities + .sf-prevent-select { user-select: none; } + .sf-block-touch { touch-action: pinch-zoom; } + .sf-touch-none { touch-action: none; } + .sf-no-pointer { pointer-events: none; } + + // Disabled state + .sf-disabled { + background-image: none; + cursor: default; + opacity: .35; + pointer-events: none; + } + + // Read-only state + .sf-readonly { + cursor: default; + pointer-events: none; + } + + // Link styling + .sf-link { + text-decoration: none; + cursor: pointer; + color: inherit; + &:hover { + text-decoration: underline; + } + } + + // Image styling + .sf-image { display: inline-block; } + + // List utilities + .sf-list-none { list-style-type: none; } + + /* sf-overflow: Overflow handling utilities */ + .sf-overflow-hidden { + overflow: hidden; + } + + .sf-overflow-auto { + overflow: auto; + } + + .sf-overflow-visible { + overflow: visible; + } + + /* sf-cursor: Cursor style utilities */ + .sf-cursor-pointer { + cursor: pointer; + } + + .sf-cursor-default { + cursor: default; + } + + .sf-cursor-auto { + cursor: auto; + } + + .sf-cursor-not-allowed { + cursor: not-allowed; + } + + // Border radius utility classes module + @each $key, $value in $border-radius { + .sf-rounded-#{$key} { + border-radius: $value; + } + } + + // other common utility classes + .sf-align-center { + display: flex; + align-items: center; + line-height: normal; + } + .sf-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} \ No newline at end of file diff --git a/components/base/styles/styles/_resize.scss b/components/base/styles/styles/_resize.scss new file mode 100644 index 0000000..b96e2a3 --- /dev/null +++ b/components/base/styles/styles/_resize.scss @@ -0,0 +1,100 @@ +@include export-module('resize') { + .sf-resize { + position: absolute; + } + + .sf-resize-x { + resize: horizontal; + overflow: auto; + } + + .sf-resize-y { + resize: vertical; + overflow: auto; + } + + .sf-resize-xy { + resize: both; + overflow: auto; + } + + .sf-resize-none { + resize: none; + } + + // Resize handles for custom resizing + .sf-resize-handle { + position: absolute; + } + + // Cardinal directions + .sf-resize-n { + cursor: n-resize; + } + + .sf-resize-e { + cursor: e-resize; + } + + .sf-resize-s { + cursor: s-resize; + } + + .sf-resize-w { + cursor: w-resize; + } + + // Corner handles + .sf-resize-nw { + cursor: nw-resize; + } + + .sf-resize-ne { + cursor: ne-resize; + } + + .sf-resize-se { + cursor: se-resize; + } + + .sf-resize-sw { + cursor: sw-resize; + } + + // Bidirectional resize cursors + .sf-resize-ns { + cursor: ns-resize; + } + + .sf-resize-ew { + cursor: ew-resize; + } + + .sf-resize-nesw { + cursor: nesw-resize; + } + + .sf-resize-nwse { + cursor: nwse-resize; + } + + // General resize cursor + .sf-resize-all { + cursor: move; + } + + // Column resize + .sf-resize-col { + cursor: col-resize; + } + + // Row resize + .sf-resize-row { + cursor: row-resize; + } + + // Not allowed to resize + .sf-resize-not-allowed { + cursor: not-allowed; + } +} \ No newline at end of file diff --git a/components/base/styles/styles/_theme.scss b/components/base/styles/styles/_theme.scss new file mode 100644 index 0000000..baf2f39 --- /dev/null +++ b/components/base/styles/styles/_theme.scss @@ -0,0 +1,32 @@ +@include export-module('base-theme') { + // Define state colors + $states: ( + error: $error-font-color, + warning: $warning-font-color, + success: $success-font-color, + information: $information-font-color + ); + + // State classes + @each $state, $color in $states { + .sf-#{$state} { + color: rgba($color); + } + } + .sf-overlay { + background-color: $overlay-bg-color; + } + .sf-ripple-element { + background: linear-gradient(90deg, + rgba(28, 27, 31, .08) 0%, + rgba(28, 27, 31, .1) 5%, + rgba(28, 27, 31, .1) 50%, + rgba(28, 27, 31, .1) 50%, + rgba(28, 27, 31, .1) 95%, + rgba(28, 27, 31, .08) 100%); + } + .sf-license { + color: #ff0; + text-decoration: none; + } +} \ No newline at end of file diff --git a/components/base/styles/definition/_material3.scss b/components/base/styles/themes/_material3.scss similarity index 64% rename from components/base/styles/definition/_material3.scss rename to components/base/styles/themes/_material3.scss index 7576b7d..cc3719d 100644 --- a/components/base/styles/definition/_material3.scss +++ b/components/base/styles/themes/_material3.scss @@ -2,45 +2,11 @@ @use 'sass:color'; @use 'sass:meta'; @use 'sass:list'; -@function mapcolorvariable($pallete-name){ - @return var(#{'--color-sf-'+ $pallete-name}); -} - -@function darken-color($color, $amount) { - @if is-custom-property($color) { - @return #{$color}-#{$amount}; - } +@import '../mixins/all'; +@import '../functions/all'; +@import '../typography/variables'; +@import '../border-radius/radius'; - // maybe there is a way to call the original `darken` instead?? - @return adjust-color($color, $lightness: -1 * $amount); -} - -@mixin lighten-color($color, $amount) { - filter: brightness(#{100% + $amount}) saturate(100%) hue-rotate(0deg); - background-color: $color; -} - -@function rgbaChange($hex, $alpha: 1) { - $r: str-slice($hex, 1, 2); - $g: str-slice($hex, 3, 4); - $b: str-slice($hex, 5, 6); - $rgba: rgba(hex($r), hex($g), hex($b), $alpha); - @return $rgba; -} - -@function hex-to-rgba($hex, $opacity) { - $r: str-slice($hex, 1, 2); - $g: str-slice($hex, 3, 4); - $b: str-slice($hex, 5, 6); - @if str-length($hex) == 8 { - $opacity: str-slice($hex, 7, 8); - } - $r: str-to-num($r, 16); - $g: str-to-num($g, 16); - $b: str-to-num($b, 16); - $opacity: $opacity / 255; - @return rgba($r, $g, $b, $opacity); -} :root, .sf-light-mode { --color-sf-black: 0, 0, 0; @@ -53,13 +19,13 @@ --color-sf-tertiary-container: 255, 216, 228; --color-sf-surface: 255, 255, 255; --color-sf-surface-variant: 231, 224, 236; - --color-sf-background: var(--color-sf-surface); + --color-sf-background: 255, 255, 255; --color-sf-on-primary: 255, 255, 255; --color-sf-on-primary-container: 33, 0, 94; --color-sf-on-secondary: 255, 255, 255; --color-sf-on-secondary-container: 30, 25, 43; --color-sf-on-tertiary: 255, 255, 255; - --color-sf-on-tertiary-containe: 55, 11, 30; + --color-sf-on-tertiary-container: 55, 11, 30; --color-sf-on-surface: 28, 27, 31; --color-sf-on-surface-variant: 73, 69, 78; --color-sf-on-background: 28, 27, 31; @@ -87,76 +53,61 @@ --color-sf-warning-container: 254, 236, 222; --color-sf-on-warning: 255, 255, 255; --color-sf-on-warning-container: 47, 21, 0; - --color-sf-spreadsheet-gridline: var(--color-sf-surface-variant); - --color-sf-shadow-focus-ring1: 0 0 0 1px rgb(255, 255, 255), 0 0 0 3px rgb(0, 0, 0); - --color-sf-diagram-palette-background: --color-sf-white; --color-sf-success-text: 255, 255, 255; --color-sf-warning-text: 255, 255, 255; - --color-sf-danger-text: 255, 255, 255; + --color-sf-error-text: 255, 255, 255; --color-sf-info-text: 255, 255, 255; - --color-sf-content-text-color-alt2: var(--color-sf-on-secondary-container); - --color-sf-secondary-bg-color: var(--color-sf-surface); } -$black: mapcolorvariable('black') !default; -$white: mapcolorvariable('white') !default; - -$primary : mapcolorvariable('primary') !default; -$primary-container: mapcolorvariable('primary-container') !default; -$secondary: mapcolorvariable('secondary') !default; -$secondary-container: mapcolorvariable('secondary-container') !default; -$tertiary: mapcolorvariable('tertiary') !default; -$tertiary-container: mapcolorvariable('tertiary-container') !default; -$surface: mapcolorvariable('surface') !default; -$surface-variant: mapcolorvariable('surface-variant') !default; -$background: mapcolorvariable('background') !default; -$on-primary: mapcolorvariable('on-primary') !default; -$on-primary-container: mapcolorvariable('on-primary-container') !default; -$on-secondary: mapcolorvariable('on-secondary') !default; -$on-secondary-container: mapcolorvariable('on-secondary-container') !default; -$on-tertiary: mapcolorvariable('on-tertiary') !default; -$on-tertiary-containe: mapcolorvariable('on-tertiary-containe') !default; -$on-surface: mapcolorvariable('on-surface') !default; -$on-surface-variant: mapcolorvariable('on-surface-variant') !default; -$on-background: mapcolorvariable('on-background') !default; -$outline: mapcolorvariable('outline') !default; -$outline-variant: mapcolorvariable('outline-variant') !default; -$shadow: mapcolorvariable('shadow') !default; -$surface-tint-color: mapcolorvariable('surface-tint-color') !default; -$inverse-surface: mapcolorvariable('inverse-surface') !default; -$inverse-on-surface: mapcolorvariable('inverse-on-surface') !default; -$inverse-primary: mapcolorvariable('inverse-primary') !default; -$scrim:mapcolorvariable('scrim') !default; -$error: mapcolorvariable('error') !default; -$error-container: mapcolorvariable('error-container') !default; -$on-error: mapcolorvariable('on-error') !default; -$on-error-container: mapcolorvariable('on-error-container') !default; -$success: mapcolorvariable('success') !default; -$success-container: mapcolorvariable('success-container') !default; -$on-success: mapcolorvariable('on-success') !default; -$on-success-container: mapcolorvariable('on-success-container') !default; -$info: mapcolorvariable('info') !default; -$info-container: mapcolorvariable('info-container') !default; -$on-info: mapcolorvariable('on-info') !default; -$on-info-container: mapcolorvariable('on-info-container') !default; -$warning: mapcolorvariable('warning') !default; -$warning-container: mapcolorvariable('warning-container') !default; -$on-warning: mapcolorvariable('on-warning') !default; -$on-warning-container: mapcolorvariable('on-warning-container') !default; -$success-text: mapcolorvariable('success-text') !default; -$warning-text: mapcolorvariable('warning-text') !default; -$info-text: mapcolorvariable('info-text') !default; -$danger-text: mapcolorvariable('danger-text') !default; - -$opacity0: 0 !default; -$opacity4: .04 !default; -$opacity5: .05 !default; -$opacity6: .06 !default; -$opacity8: .08 !default; -$opacity11: .11 !default; -$opacity12: .12 !default; -$opacity14: .14 !default; -$opacity16: .16 !default; +$black: sf-color('black') !default; +$white: sf-color('white') !default; + +$primary : sf-color('primary') !default; +$primary-container: sf-color('primary-container') !default; +$secondary: sf-color('secondary') !default; +$secondary-container: sf-color('secondary-container') !default; +$tertiary: sf-color('tertiary') !default; +$tertiary-container: sf-color('tertiary-container') !default; +$surface: sf-color('surface') !default; +$surface-variant: sf-color('surface-variant') !default; +$background: sf-color('background') !default; +$on-primary: sf-color('on-primary') !default; +$on-primary-container: sf-color('on-primary-container') !default; +$on-secondary: sf-color('on-secondary') !default; +$on-secondary-container: sf-color('on-secondary-container') !default; +$on-tertiary: sf-color('on-tertiary') !default; +$on-tertiary-container: sf-color('on-tertiary-container') !default; +$on-surface: sf-color('on-surface') !default; +$on-surface-variant: sf-color('on-surface-variant') !default; +$on-background: sf-color('on-background') !default; +$outline: sf-color('outline') !default; +$outline-variant: sf-color('outline-variant') !default; +$shadow: sf-color('shadow') !default; +$surface-tint-color: sf-color('surface-tint-color') !default; +$inverse-surface: sf-color('inverse-surface') !default; +$inverse-on-surface: sf-color('inverse-on-surface') !default; +$inverse-primary: sf-color('inverse-primary') !default; +$scrim:sf-color('scrim') !default; +$error: sf-color('error') !default; +$error-container: sf-color('error-container') !default; +$on-error: sf-color('on-error') !default; +$on-error-container: sf-color('on-error-container') !default; +$success: sf-color('success') !default; +$success-container: sf-color('success-container') !default; +$on-success: sf-color('on-success') !default; +$on-success-container: sf-color('on-success-container') !default; +$info: sf-color('info') !default; +$info-container: sf-color('info-container') !default; +$on-info: sf-color('on-info') !default; +$on-info-container: sf-color('on-info-container') !default; +$warning: sf-color('warning') !default; +$warning-container: sf-color('warning-container') !default; +$on-warning: sf-color('on-warning') !default; +$on-warning-container: sf-color('on-warning-container') !default; +$success-text: sf-color('success-text') !default; +$warning-text: sf-color('warning-text') !default; +$info-text: sf-color('info-text') !default; +$error-text: sf-color('error-text') !default; $surface1: linear-gradient(0deg, rgba($primary, $opacity5), rgba($primary, $opacity5)), rgba($surface) !default; $surface2: linear-gradient(0deg, rgba($primary, $opacity8), rgba($primary, $opacity8)), rgba($surface) !default; @@ -171,29 +122,24 @@ $level3: 0 1px 3px 0 rgba(0, 0, 0, .3), 0 4px 8px 3px rgba(0, 0, 0, .15); $level4: 0 2px 3px 0 rgba(0, 0, 0, .3), 0 6px 10px 4px rgba(0, 0, 0, .15); $level5: 0 4px 4px 0 rgba(0, 0, 0, .3), 0 8px 12px 6px rgba(0, 0, 0, .15); -$primary: $primary !default; $primary-text-color: $on-primary !default; $primary-light: $primary-container !default; $primary-lighter: $primary-light !default; $primary-dark: $surface-tint-color !default; $primary-darker: $on-primary-container !default; -$success: $success !default; $transparent: transparent !default; -$info: $info !default; -$warning: $warning !default; -$danger: $error !default; $success-light: $success-container !default; $info-light: $info-container !default; $warning-light: $warning-container !default; -$danger-light: $error-container !default; +$error-light: $error-container !default; $success-dark: $success !default; $info-dark: $info !default; $warning-dark: $warning !default; -$danger-dark:$error !default; +$error-dark:$error !default; $success-light-alt: $success-light !default; $info-light-alt: $info-light !default; $warning-light-alt: $warning-light !default; -$danger-light-alt: $danger-light !default; +$error-light-alt: $error-light !default; $content-bg-color: $surface !default; $content-bg-color-alt1: $surface1 !default; @@ -215,22 +161,9 @@ $flyout-bg-color-selected: rgba($primary-container, .65) !default; $flyout-bg-color-focus: rgba($on-surface, $opacity4) !default; $overlay-bg-color: rgba($scrim, .5) !default; $table-bg-color-hover: rgba($on-surface, $opacity5) !default; -$table-bg-color-pressed: rgba($primary-container, .65) !default; +$table-bg-color-pressed: rgba($on-surface, $opacity8) !default; $table-bg-color-selected: rgba($primary-container, .65) !default; - -$colorpicker-gradient-1: #f00 !default; -$colorpicker-gradient-2: #ff0 !default; -$ccolorpicker-gradient-3: #0f0 !default; -$colorpicker-gradient-4: #0ff !default; -$colorpicker-gradient-5: #00f !default; -$colorpicker-gradient-6: #f0f !default; -$colorpicker-gradient-7: #ff0004 !default; -$spreadsheet-selection-1: #673ab8 !default; -$spreadsheet-selection-2: #9c27b0 !default; -$spreadsheet-selection-3: #029688 !default; -$spreadsheet-selection-4: #4caf51 !default; -$spreadsheet-selection-5: #fe9800 !default; -$spreadsheet-selection-6: #3f52b5 !default; +$table-bg-color-selected-hover: color-mix(in srgb, rgba($primary-container, .85), rgba($on-surface, 0.15)); $content-text-color: $on-surface !default; $content-text-color-alt1: $on-surface-variant !default; @@ -272,7 +205,6 @@ $border-disabled: $border-light !default; $border-warning: $warning !default; $border-error: $error !default; $border-success: $success !default; -$spreadsheet-gridline: $surface-variant !default; $flyout-border: $border-light !default; $tooltip-bg-color: $inverse-surface !default; @@ -293,48 +225,12 @@ $shadow-focus-ring2: 0 0 0 1px rgba($black, .95) !default; $primary-shadow-focus: 0 0 0 4px rgba($primary, .5) !default; $secondary-shadow-focus: 0 0 0 4px rgba($secondary, .5) !default; $success-shadow-focus: 0 0 0 4px rgba($success, .5) !default; -$danger-shadow-focus: 0 0 0 4px rgba($danger, .5) !default; +$error-shadow-focus: 0 0 0 4px rgba($error, .5) !default; $info-shadow-focus: 0 0 0 4px rgba($info, .5) !default; $warning-shadow-focus: 0 0 0 4px rgba($warning, .5) !default; $font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif !default; -$text-xxs: 10px !default; -$text-xs: 12px !default; -$text-sm: 14px !default; -$text-base: 16px !default; -$text-lg: 18px !default; -$text-xl: 20px !default; -$text-2xl: 24px !default; -$text-3xl: 28px !default; -$text-4xl: 32px !default; -$text-5xl: 42px !default; -$text-6xl: 68px !default; -$text-7xl: 78px !default; -$text-8xl: 96px !default; -$text-9xl: 128px !default; - -$h1-font-size: 40px !default; -$h2-font-size: 32px !default; -$h3-font-size: 28px !default; -$h4-font-size: $text-2xl !default; -$h5-font-size: $text-xl !default; -$h6-font-size: $text-base !default; - -$leading-none: 1 !default; -$leading-tight: 1.25 !default; -$leading-snug: 1.375 !default; -$leading-normal: 1.5 !default; -$leading-relaxed: 1.625 !default; -$leading-loose: 2 !default; - -$font-weight-lighter: lighter !default; -$font-weight-light: 300 !default; -$font-weight-normal: 400 !default; -$font-weight-medium: 600 !default; -$font-weight-bold: 700 !default; -$font-weight-bolder: bolder !default; - $secondary-bg-color: linear-gradient(0deg, rgba($content-bg-color, 1), rgba($content-bg-color, 1)), rgba($content-bg-color) !default; $secondary-border-color: linear-gradient(0deg, rgba($content-bg-color, 1), rgba($content-bg-color, 1)) !default; $secondary-text-color: $on-surface !default; @@ -399,21 +295,21 @@ $warning-bg-color-disabled: rgba($secondary, .12) !default; $warning-border-color-disabled: $warning-bg-color-disabled !default; $warning-text-disabled: rgba($on-surface, .38) !default; -$danger-bg-color: $danger !default; -$danger-border-color: $danger !default; -$danger-text: $white !default; -$danger-bg-color-hover: linear-gradient(0deg, rgba($danger-text, $opacity8), rgba($danger-text, $opacity8)), rgba($danger-bg-color) !default; -$danger-border-color-hover: $danger-bg-color-hover !default; -$danger-text-hover: $danger-text !default; -$danger-bg-color-pressed: linear-gradient(0deg, rgba($danger-text, $opacity12), rgba($danger-text, $opacity12)), rgba($danger-bg-color) !default; -$danger-border-color-pressed: $danger-bg-color-pressed !default; -$danger-text-pressed: $danger-text !default; -$danger-bg-color-focus: $danger-bg-color-hover !default; -$danger-border-color-focus: $danger-border-color-hover !default; -$danger-text-focus: $danger-text !default; -$danger-bg-color-disabled: rgba($secondary, .12) !default; -$danger-border-color-disabled: $danger-bg-color-disabled !default; -$danger-text-disabled: rgba($on-surface, .38) !default; +$error-bg-color: $error !default; +$error-border-color: $error !default; +$error-text: $white !default; +$error-bg-color-hover: linear-gradient(0deg, rgba($error-text, $opacity8), rgba($error-text, $opacity8)), rgba($error-bg-color) !default; +$error-border-color-hover: $error-bg-color-hover !default; +$error-text-hover: $error-text !default; +$error-bg-color-pressed: linear-gradient(0deg, rgba($error-text, $opacity12), rgba($error-text, $opacity12)), rgba($error-bg-color) !default; +$error-border-color-pressed: $error-bg-color-pressed !default; +$error-text-pressed: $error-text !default; +$error-bg-color-focus: $error-bg-color-hover !default; +$error-border-color-focus: $error-border-color-hover !default; +$error-text-focus: $error-text !default; +$error-bg-color-disabled: rgba($secondary, .12) !default; +$error-border-color-disabled: $error-bg-color-disabled !default; +$error-text-disabled: rgba($on-surface, .38) !default; $info-text: $white !default; $info-bg-color: $info !default; @@ -437,8 +333,8 @@ $secondary-outline: $secondary-text-color !default; $secondary-outline-border: $outline !default; $warning-outline: $warning-bg-color !default; $warning-outline-border: $outline !default; -$danger-outline: $danger-bg-color !default; -$danger-outline-border: $outline !default; +$error-outline: $error-bg-color !default; +$error-outline-border: $outline !default; $success-outline: $success-bg-color !default; $success-outline-border: $outline !default; $info-outline: $info-bg-color !default; @@ -459,12 +355,6 @@ $series-9: #05b3da !default; $series-10: #828486 !default; $series-11: #b1212d !default; $series-12: #38be09 !default; -$skin-name: 'Material3' !default; -$theme-name: 'Material3' !default; - -$diagram-palette-background: $white !default; -$diagram-palette-hover-background: $content-bg-color-hover !default; -$diagram-palette-selection-background: rgba($on-surface, .12) !default; $shape-none:0 !default; $shape-extra-small:4px !default; @@ -476,31 +366,20 @@ $shape-full:50% !default; $button-radius: $shape-full !default; $button-radius-small: $shape-full !default; -$button-radius-bigger: $shape-full !default; $input-radius: $shape-extra-small !default; $input-radius-small: $shape-extra-small !default; -$input-radius-bigger: $shape-extra-small !default; $model-radius: $shape-medium !default; $model-radius-small: $shape-small !default; -$model-radius-bigger: $shape-Large !default; $flyout-radius: $shape-extra-small !default; -$flyout-radius-bigger: $shape-extra-small !default; $flyout-radius-small: $shape-extra-small !default; -$chkbox-radius:2px !default; -$chkbox-radius-small:2px !default; -$chkbox-radius-bigger:2px !default; $card-radius:$shape-small !default; $card-radius-small:$shape-extra-small !default; -$card-radius-bigger:$shape-medium !default; $msg-radius:$shape-none !default; $msg-radius-small: $msg-radius !default; -$msg-radius-bigger: $msg-radius !default; $toast-radius: $shape-extra-small !default; $toast-radius-small: $toast-radius !default; -$toast-radius-bigger: $toast-radius !default; $chip-radius: $shape-extra-small !default; $chip-radius-small: $shape-extra-small !default; -$chip-radius-bigger: $shape-small !default; $btn-secondary-border-color: linear-gradient(0deg, rgba($content-bg-color, 0), rgba($content-bg-color, 0)) !default; @@ -540,23 +419,23 @@ $msg-success-close-icon-color: rgba($icon-color) !default; $msg-success-close-icon-color-alt1: rgba($icon-color) !default; $msg-success-close-icon-color-alt2: rgba($on-success) !default; -$msg-danger-color: rgba($on-error-container) !default; -$msg-danger-bg-color: rgba($error-container) !default; -$msg-danger-border-color: rgba($error-container) !default; -$msg-danger-color-alt1: rgba($on-error-container) !default; -$msg-danger-bg-color-alt1: $transparent !default; -$msg-danger-border-color-alt1: rgba($error) !default; -$msg-danger-color-alt2: rgba($on-error) !default; -$msg-danger-bg-color-alt2: rgba($error) !default; -$msg-danger-border-color-alt2: rgba($error) !default; +$msg-error-color: rgba($on-error-container) !default; +$msg-error-bg-color: rgba($error-container) !default; +$msg-error-border-color: rgba($error-container) !default; +$msg-error-color-alt1: rgba($on-error-container) !default; +$msg-error-bg-color-alt1: $transparent !default; +$msg-error-border-color-alt1: rgba($error) !default; +$msg-error-color-alt2: rgba($on-error) !default; +$msg-error-bg-color-alt2: rgba($error) !default; +$msg-error-border-color-alt2: rgba($error) !default; -$msg-danger-icon-color: rgba($error) !default; -$msg-danger-icon-color-alt1: rgba($on-error-container) !default; -$msg-danger-icon-color-alt2: rgba($on-error) !default; +$msg-error-icon-color: rgba($error) !default; +$msg-error-icon-color-alt1: rgba($on-error-container) !default; +$msg-error-icon-color-alt2: rgba($on-error) !default; -$msg-danger-close-icon-color: rgba($icon-color) !default; -$msg-danger-close-icon-color-alt1: rgba($icon-color) !default; -$msg-danger-close-icon-color-alt2: rgba($on-error) !default; +$msg-error-close-icon-color: rgba($icon-color) !default; +$msg-error-close-icon-color-alt1: rgba($icon-color) !default; +$msg-error-close-icon-color-alt2: rgba($on-error) !default; $msg-warning-color: rgba($on-warning-container) !default; $msg-warning-bg-color: rgba($warning-container) !default; @@ -594,34 +473,11 @@ $msg-info-close-icon-color: rgba($icon-color) !default; $msg-info-close-icon-color-alt1: rgba($icon-color) !default; $msg-info-close-icon-color-alt2: rgba($on-info) !default; -$appbar-bg-color-alt1: $content-bg-color-alt2 !default; -$appbar-color-alt1: rgba($content-text-color) !default; -$appbar-border-color-alt1: linear-gradient(0deg, rgba($primary, $opacity8), rgba($primary, $opacity8)) !default; -$appbar-hover-bg-color-alt1: rgba(0, 0, 0, .05) !default; - -$appbar-bg-color-alt2: rgba($inverse-surface) !default; -$appbar-color-alt2: rgba($inverse-on-surface) !default; -$appbar-border-color-alt2: rgba($inverse-surface) !default; -$appbar-hover-bg-color-alt2: rgba(255, 255, 255, .08) !default; - -$appbar-bottom-shadow: 0 1.6px 3.6px rgba(0, 0, 0, .13), 0 .3px .9px rgba(0, 0, 0, .1) !default; -$appbar-top-shadow: 0 -1.6px 3.6px rgba(0, 0, 0, .13), 0 -.3px .9px rgba(0, 0, 0, .1) !default; - -$rating-selected-color: $primary !default; -$rating-unrated-color: $content-bg-color-alt3 !default; -$rating-selected-disabled-color: rgba($on-surface, .24) !default; -$rating-unrated-disabled-color: rgba($on-surface, .08) !default; -$rating-selected-hover-color: darken-color(rgba($primary), 5%) !default; -$rating-unrated-hover-color: darken-color(rgba($primary), 10%) !default; -$rating-pressed-color: darken-color(rgba($primary), 10%) !default; - $skeleton-wave-color: rgba(255, 255, 255, 1) !default; -$splitbtn-right-border: linear-gradient(to right, rgba(92, 72, 147, 1) 25%, rgba(92, 72, 147, 1) 75%) 1 !default; -$splitbtn-right-border-rtl: linear-gradient(to left, rgba($content-bg-color, 1) 25%, rgba($border-light, 1) 75%) 1 !default; -$splitbtn-right-border-vertical: linear-gradient(to bottom, rgba($content-bg-color, 1) 25%, rgba($border-light, 1) 75%) 1 !default; - -$gantt-taskbar-color: linear-gradient(0deg, rgba($primary, .75), rgba($primary, .75)), rgba($content-bg-color) !default; +$split-btn-right-border: linear-gradient(to right, rgba(92, 72, 147, 1) 25%, rgba(92, 72, 147, 1) 75%) 1 !default; +$split-btn-right-border-rtl: linear-gradient(to left, rgba($content-bg-color, 1) 25%, rgba($border-light, 1) 75%) 1 !default; +$split-btn-right-border-vertical: linear-gradient(to bottom, rgba($content-bg-color, 1) 25%, rgba($border-light, 1) 75%) 1 !default; $grey-white: #fff !default; $base-font: #000 !default; @@ -634,7 +490,7 @@ $grey-700: #616161 !default; $grey-800: #424242 !default; $grey-900: #212121 !default; $sd-focus-box-shadow: $secondary-shadow-focus !default; -$toooltip-text-color: #f9fafb !default; +$tooltip-text-color: #f9fafb !default; $range-button-font-color: $info !default; $ddl-input-placeholder-padding: 0 0 0 8px !default; @@ -646,23 +502,6 @@ $gray-900: #212529 !default; $primary-300: #7986cb; $gray-400: #ced4da !default; -$font-icon-8: 8px !default; -$font-icon-9: 9px !default; -$font-icon-10: 10px !default; -$font-icon-12: 12px !default; -$font-icon-13: 13px !default; -$font-icon-14: 14px !default; -$font-icon-15: 15px !default; -$font-icon-16: 16px !default; -$font-icon-17: 17px !default; -$font-icon-18: 18px !default; -$font-icon-20: 20px !default; -$font-icon-22: 22px !default; -$font-icon-24: 24px !default; -$font-icon-26: 26px !default; -$font-icon-28: 28px !default; -$font-icon-32: 32px !default; - .sf-dark-mode { --color-sf-black: 0, 0, 0; --color-sf-white: 255, 255, 255; @@ -674,13 +513,12 @@ $font-icon-32: 32px !default; --color-sf-tertiary-container: 99, 59, 72; --color-sf-surface: 28, 27, 31; --color-sf-surface-variant: 73, 69, 79; - --color-sf-background: var(--color-sf-surface); --color-sf-on-primary: 55, 30, 115; --color-sf-on-primary-container: 234, 221, 255; --color-sf-on-secondary: 51, 45, 65; --color-sf-on-secondary-container: 232, 222, 248; --color-sf-on-tertiary: 73, 37, 50; - --color-sf-on-tertiary-containe: 255, 216, 228; + --color-sf-on-tertiary-container: 255, 216, 228; --color-sf-on-surface: 230, 225, 229; --color-sf-on-surface-variant: 202, 196, 208; --color-sf-on-background: 230, 225, 229; @@ -708,19 +546,15 @@ $font-icon-32: 32px !default; --color-sf-warning-container: 123, 65, 0; --color-sf-on-warning: 99, 52, 0; --color-sf-on-warning-container: 255, 220, 193; - --color-sf-spreadsheet-gridline: 231, 224, 236; - --color-sf-shadow-focus-ring1: 0 0 0 1px #000, 0 0 0 3px #fff; --color-sf-success-text: 0, 0, 0; --color-sf-warning-text: 0, 0, 0; --color-sf-info-text: 0, 0, 0; - --color-sf-danger-text: 0, 0, 0; - --color-sf-diagram-palette-background: var(--color-sf-inverse-surface); - --color-sf-content-text-color-alt2: var(--color-sf-on-secondary); + --color-sf-error-text: 0, 0, 0; } -$font-size: 12px !default; +$font-size: 1rem !default; $font-weight: 400 !default; -$error-font-color: $danger !default; +$error-font-color: $error !default; $warning-font-color: $warning !default; $success-font-color: $success !default; $information-font-color: $info !default; diff --git a/components/base/styles/typography/_variables.scss b/components/base/styles/typography/_variables.scss new file mode 100644 index 0000000..108a1a3 --- /dev/null +++ b/components/base/styles/typography/_variables.scss @@ -0,0 +1,84 @@ +$opacity0: 0 !default; +$opacity4: .04 !default; +$opacity5: .05 !default; +$opacity6: .06 !default; +$opacity8: .08 !default; +$opacity11: .11 !default; +$opacity12: .12 !default; +$opacity14: .14 !default; +$opacity16: .16 !default; + +// Text sizes +$text-xxs: 10px !default; +$text-xs: 12px !default; +$text-sm: 14px !default; +$text-base: 16px !default; +$text-lg: 18px !default; +$text-xl: 20px !default; +$text-2xl: 24px !default; +$text-3xl: 28px !default; +$text-4xl: 32px !default; +$text-5xl: 42px !default; +$text-6xl: 68px !default; +$text-7xl: 78px !default; +$text-8xl: 96px !default; +$text-9xl: 128px !default; + +// Font sizes +$font-size-11: 0.6875rem !default; +$font-size-12: 0.75rem !default; +$font-size-13: 0.8125rem !default; +$font-size-14: 0.875rem !default; +$font-size-15: 0.9375rem !default; +$font-size-16: 1rem !default; +$font-size-18: 1.125rem !default; +$font-size-20: 1.25rem !default; +$font-size-24: 1.5rem !default; +$font-size-26: 1.625rem !default; +$font-size-28: 1.75rem !default; +$font-size-30: 1.875rem !default; +$font-size-32: 2rem !default; + +// Heading sizes +$h1-font-size: 40px !default; +$h2-font-size: 32px !default; +$h3-font-size: 28px !default; +$h4-font-size: $text-2xl !default; +$h5-font-size: $text-xl !default; +$h6-font-size: $text-base !default; + +// Line heights +$leading-none: 1 !default; +$leading-tight: 1.25 !default; +$leading-small: 1.333 !default; +$leading-snug: 1.375 !default; +$leading-medium: 1.425 !default; +$leading-normal: 1.5 !default; +$leading-relaxed: 1.625 !default; +$leading-loose: 2 !default; + +// Font weights +$font-weight-lighter: lighter !default; +$font-weight-light: 300 !default; +$font-weight-normal: 400 !default; +$font-weight-medium: 600 !default; +$font-weight-bold: 700 !default; +$font-weight-bolder: bolder !default; + +// Font icon sizes +$font-icon-8: 8px !default; +$font-icon-9: 9px !default; +$font-icon-10: 10px !default; +$font-icon-12: 12px !default; +$font-icon-13: 13px !default; +$font-icon-14: 14px !default; +$font-icon-15: 15px !default; +$font-icon-16: 16px !default; +$font-icon-17: 17px !default; +$font-icon-18: 18px !default; +$font-icon-20: 20px !default; +$font-icon-22: 22px !default; +$font-icon-24: 24px !default; +$font-icon-26: 26px !default; +$font-icon-28: 28px !default; +$font-icon-32: 32px !default; \ No newline at end of file diff --git a/components/buttons/CHANGELOG.md b/components/buttons/CHANGELOG.md deleted file mode 100644 index 7a08d63..0000000 --- a/components/buttons/CHANGELOG.md +++ /dev/null @@ -1,108 +0,0 @@ -# Changelog - -## [Unreleased] - -## 29.2.4 (2025-05-14) - -### Button - -The Button component is designed to create highly customizable and interactive button elements with a variety of styling and functional options. It allows for tailored interactions through different configurations such as size, color, icon positioning, and toggle capability. - -Explore the demo here - -**Key features** - -- **Color Variants:** Style buttons with distinct color options like 'warning', 'success', 'danger', and 'info' to fit your application's theme. - -- **Icon Support:** Integrate SVG icons within buttons for enhanced visual cues. Configure the icon's position to be left, right, top, or bottom relative to button text. - -- **Toggle Functionality:** Use the button as a toggle to maintain and represent states within your application, enabling buttons to switch between active and inactive states upon user interaction. - -- **Size Options:** Adjust button dimensions with size variants such as 'small', 'medium' and 'bigger', allowing for flexibility in different UI contexts. - -- **Variant Styles:** Choose from various button styles like 'outlind', 'filled', and 'flat' to seamlessly integrate with your design language. - -- **Selection Management:** Include prop configurations to set initial states, making it simple to handle selection states, especially useful for toggle buttons. - -### Checkbox - -The Checkbox component offers a flexible and user-friendly way to allow users to make binary selections. It supports various states and configurations to accommodate different use cases in applications. - -Explore the demo here - -**Key features** - -- **Selection States:** The Checkbox component can be configured to be in checked, unchecked, or indeterminate states. This provides a visual cue for users to understand the current selection state. - -- **Label Support:** Display informative text alongside the checkbox to clearly convey its purpose to users. The label can be positioned either before or after the checkbox element based on UI preferences. - -- **Label Positioning:** Configure the label placement with the `labelPlacement` prop, choosing whether the label appears before or after the Checkbox. - -### Chip - -The Chip component represents information in a compact form, such as entity attribute, text, or action. It provides a versatile way to display content in a contained, interactive element. - -Explore the demo here - -**Key features** - -- **Variants:** Display chips with different visual styles using either 'filled' or 'outlined' variants to match your design requirements. - -- **Colors:** Customize the appearance with predefined color options including primary, info, danger, success, and warning. - -- **Icons and Avatars:** Enhance visual representation with leading icons, trailing icons, or avatars to provide additional context. - -### ChipList - -The ChipList component displays a collection of chips that can be used to represent multiple items in a compact form. It provides a flexible way to manage and interact with a group of chip elements. - -Explore the demo here - -**Key features** - -- **Selection Modes:** Supports three selection types - 'single', 'multiple', and 'none' to control how users can select chips. - -- **Data Binding:** Easily populate the ChipList with an array of strings, numbers, or custom chip configurations. - -- **Customizable Chips:** Each chip can be individually styled with avatars, leading icons, trailing icons, and different variants. - -- **Removable Chips:** Configure chips to be removable with built-in delete event handling. - -- **Controlled & Uncontrolled Modes:** Supports both controlled and uncontrolled component patterns for selection and deletion. - -### Floating Action Button - -The Floating Action Button (FAB) component provides a prominent primary action within an application interface, positioned for high visibility and customizable with various styling options. - -Explore the demo here - -**Key features** - -- **Color Variants:** Customizable color options such as 'warning', 'success', 'danger', and 'info' are available to help the FAB blend seamlessly with your application's color scheme. - -- **Icon Customization:** Integrate SVG icons within buttons for enhanced visual appeal. Control icon placement relative to text with configurable options for positioning. - -- **Visibility Control:** Easily manage the visibility of the FAB using the `isVisible` prop, deciding if it should be displayed based on application logic. - -- **Positioning:** The FAB can be positioned flexibly with options like top-left, top-right, bottom-left, and bottom-right to fit different design requirements. - -- **Size Options:** Modify the size of the FAB with options for 'small', 'medium' and 'bigger', accommodating different interface contexts. - -- **Toggle Functionality:** Activate toggle behavior for the FAB to allow it to switch states on each user interaction, which can be useful for certain UI scenarios. - - -### RadioButton - -The RadioButton component enables users to select a single option from a group, offering a clear circular interface for making selections. It is a simple and efficient way to present mutually exclusive choices to users. - -Explore the demo here - -**Key features** - -- **Selection State:** Easily configure the RadioButton to be in a checked or unchecked state, indicating selected or unselected options within a group. - -- **Label Customization:** The RadioButton can be accompanied by a text label to describe its function, which helps users understand the purpose of the radio selection. - -- **Label Positioning:** Flexibly position the label relative to the RadioButton with options available for placing it before or after the button, enhancing UI layout consistency. - -- **Form Integration:** The value attribute of the RadioButton can be included as part of form data submitted to the server, facilitating efficient data processing. diff --git a/components/buttons/README.md b/components/buttons/README.md deleted file mode 100644 index 619744c..0000000 --- a/components/buttons/README.md +++ /dev/null @@ -1,143 +0,0 @@ -# React Buttons Components - -## What's Included in the React Button Package - -The React Button package includes the following list of components. - -### React Button - -The Button component is designed to create highly customizable and interactive button elements with a variety of styling and functional options. It allows for tailored interactions through different configurations such as size, color, icon positioning, and toggle capability. - -Explore the demo [here](https://react.syncfusion.com/button). - -**Key features** - -- **Color Variants:** Style buttons with distinct color options like 'warning', 'success', 'danger', and 'info' to fit your application's theme. - -- **Icon Support:** Integrate SVG icons within buttons for enhanced visual cues. Configure the icon's position to be left, right, top, or bottom relative to button text. - -- **Toggle Functionality:** Use the button as a toggle to maintain and represent states within your application, enabling buttons to switch between active and inactive states upon user interaction. - -- **Size Options:** Adjust button dimensions with size variants such as 'small', 'medium' and 'bigger', allowing for flexibility in different UI contexts. - -- **Variant Styles:** Choose from various button styles like 'outlind', 'filled', and 'flat' to seamlessly integrate with your design language. - -- **Selection Management:** Include prop configurations to set initial states, making it simple to handle selection states, especially useful for toggle buttons. - -### React Checkbox - -The Checkbox component offers a flexible and user-friendly way to allow users to make binary selections. It supports various states and configurations to accommodate different use cases in applications. - -Explore the demo [here](https://react.syncfusion.com/checkbox). - -**Key features** - -- **Selection States:** The Checkbox component can be configured to be in checked, unchecked, or indeterminate states. This provides a visual cue for users to understand the current selection state. - -- **Label Support:** Display informative text alongside the checkbox to clearly convey its purpose to users. The label can be positioned either before or after the checkbox element based on UI preferences. - -- **Label Positioning:** Configure the label placement with the `labelPlacement` prop, choosing whether the label appears before or after the Checkbox. - -### React Chip - -The Chip component represents information in a compact form, such as entity attribute, text, or action. It provides a versatile way to display content in a contained, interactive element. - -Explore the demo [here](https://react.syncfusion.com/chip). - -**Key features** - -- **Variants:** Display chips with different visual styles using either 'filled' or 'outlined' variants to match your design requirements. - -- **Colors:** Customize the appearance with predefined color options including primary, info, danger, success, and warning. - -- **Icons and Avatars:** Enhance visual representation with leading icons, trailing icons, or avatars to provide additional context. - -### React ChipList - -The ChipList component displays a collection of chips that can be used to represent multiple items in a compact form. It provides a flexible way to manage and interact with a group of chip elements. - -Explore the demo [here](https://react.syncfusion.com/chiplist). - -**Key features** - -- **Selection Modes:** Supports three selection types - 'single', 'multiple', and 'none' to control how users can select chips. - -- **Data Binding:** Easily populate the ChipList with an array of strings, numbers, or custom chip configurations. - -- **Customizable Chips:** Each chip can be individually styled with avatars, leading icons, trailing icons, and different variants. - -- **Removable Chips:** Configure chips to be removable with built-in delete event handling. - -- **Controlled & Uncontrolled Modes:** Supports both controlled and uncontrolled component patterns for selection and deletion. - -### React Floating Action Button - -The Floating Action Button (FAB) component provides a prominent primary action within an application interface, positioned for high visibility and customizable with various styling options. - -Explore the demo [here](https://react.syncfusion.com/floating-action-button). - -**Key features** - -- **Color Variants:** Customizable color options such as 'warning', 'success', 'danger', and 'info' are available to help the FAB blend seamlessly with your application's color scheme. - -- **Icon Customization:** Integrate SVG icons within buttons for enhanced visual appeal. Control icon placement relative to text with configurable options for positioning. - -- **Visibility Control:** Easily manage the visibility of the FAB using the `isVisible` prop, deciding if it should be displayed based on application logic. - -- **Positioning:** The FAB can be positioned flexibly with options like top-left, top-right, bottom-left, and bottom-right to fit different design requirements. - -- **Size Options:** Modify the size of the FAB with options for 'small', 'medium' and 'bigger', accommodating different interface contexts. - -- **Toggle Functionality:** Activate toggle behavior for the FAB to allow it to switch states on each user interaction, which can be useful for certain UI scenarios. - -### React RadioButton - -The RadioButton component enables users to select a single option from a group, offering a clear circular interface for making selections. It is a simple and efficient way to present mutually exclusive choices to users. - -Explore the demo [here](https://react.syncfusion.com/radio-button). - -**Key features** - -- **Selection State:** Easily configure the RadioButton to be in a checked or unchecked state, indicating selected or unselected options within a group. - -- **Label Customization:** The RadioButton can be accompanied by a text label to describe its function, which helps users understand the purpose of the radio selection. - -- **Label Positioning:** Flexibly position the label relative to the RadioButton with options available for placing it before or after the button, enhancing UI layout consistency. - -- **Form Integration:** The value attribute of the RadioButton can be included as part of form data submitted to the server, facilitating efficient data processing. - - -

-Trusted by the world's leading companies - - Syncfusion logo - -

- -## Setup - -To install `buttons` and its dependent packages, use the following command, - -```sh -npm install @syncfusion/react-buttons -``` - -## Support - -Product support is available through following mediums. - -* [Support ticket](https://support.syncfusion.com/support/tickets/create) - Guaranteed Response in 24 hours | Unlimited tickets | Holiday support -* Live chat - -## Changelog -Check the changelog [here](https://github.com/syncfusion/react-ui-components/blob/master/components/buttons/CHANGELOG.md). Get minor improvements and bug fixes every week to stay up to date with frequent updates. - -## License and copyright - -> This is a commercial product and requires a paid license for possession or use. Syncfusion’s licensed software, including this component, is subject to the terms and conditions of Syncfusion's [EULA](https://www.syncfusion.com/eula/es/). To acquire a license for [React UI components](https://www.syncfusion.com/react-components), you can [purchase](https://www.syncfusion.com/sales/products) or [start a free 30-day trial](https://www.syncfusion.com/account/manage-trials/start-trials). - -> A [free community license](https://www.syncfusion.com/products/communitylicense) is also available for companies and individuals whose organizations have less than $1 million USD in annual gross revenue and five or fewer developers. - -See [LICENSE FILE](https://github.com/syncfusion/react-ui-components/blob/master/license?utm_source=npm&utm_campaign=notification) for more info. - -© Copyright 2025 Syncfusion, Inc. All Rights Reserved. The Syncfusion Essential Studio license and copyright applies to this distribution. diff --git a/components/buttons/src/button/button.tsx b/components/buttons/src/button/button.tsx deleted file mode 100644 index eb967db..0000000 --- a/components/buttons/src/button/button.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { useEffect, useRef, useState, useImperativeHandle, forwardRef, ButtonHTMLAttributes, Ref } from 'react'; -import { preRender, SvgIcon, useProviderContext, useRippleEffect } from '@syncfusion/react-base'; -import * as React from 'react'; - -/** - * Defines the label position of the component. - * ```props - * After :- When the label is positioned After, it appears to the right of the component. - * Before :- When the label is positioned Before, it appears to the left of the component. - * ``` - */ -export type LabelPlacement = 'After' | 'Before' | 'Bottom'; - -/** - * Specifies the position of an icon relative to text content in a button component. - */ -export enum IconPosition { - /** - * Positions the icon to the left of text content in a button. - */ - Left = 'Left', - - /** - * Positions the icon to the right of text content in a button. - */ - Right = 'Right', - - /** - * Positions the icon above text content in a button. - */ - Top = 'Top', - - /** - * Positions the icon below text content in a button. - */ - Bottom = 'Bottom', -} - -/** - * Specifies the type of Color to display the Button with distinctive colors. - */ -export enum Color { - /** - * The button is displayed with colors that indicate success. - */ - Success = 'Success', - /** - * The button is displayed with colors that indicate information. - */ - Info = 'Info', - /** - * The button is displayed with colors that indicate a warning. - */ - Warning = 'Warning', - /** - * The button is displayed with colors that indicate danger. - */ - Danger = 'Danger', - /** - * The button is displayed with colors that indicate it is a primary button. - */ - Primary = 'Primary', - /** - * The button is displayed with colors that indicate it is a secondary button. - */ - Secondary = 'Secondary' -} - -/** - * Defines the visual style variants for a button component, controlling background, border, and text appearance. - */ -export enum Variant { - /** - * Displays a solid background color with contrasting text. - */ - Filled = 'Filled', - - /** - * Displays a border with a transparent background and colored text. - */ - Outlined = 'Outlined', - - /** - * Displays only colored text without background and border. - */ - Flat = 'Flat' -} - -/** - * Specifies the size of the Button for layout purposes. - */ -export enum Size { - /** - * The button is displayed in a smaller size. - */ - Small = 'Small', - - /** - * The button is displayed in a medium size. - */ - Medium = 'Medium', - - /** - * The button is displayed in a larger size. - */ - Large = 'Large' -} - -/** - * Button component properties interface. - * Extends standard HTMLButtonElement attributes. - */ -export interface ButtonProps { - /** - * Specifies the position of the icon relative to the button text. Options include placing the icon at the left, right, top, or bottom of the button content. - * - * @default IconPosition.Left - */ - iconPosition?: IconPosition; - - /** - * Defines an icon for the button, which can either be a CSS class name for custom styling or an SVG element for rendering. - * - * @default - - */ - icon?: string | React.ReactNode; - - /** - * Indicates whether the button functions as a toggle button. If true, the button can switch between active and inactive states each time it is clicked. - * - * @default false - */ - togglable?: boolean; - - /** - * Sets the initial selected state for a toggle button. When true, the button is initially rendered in a 'selected' or 'active' state, otherwise it's inactive. - * - * @default false - */ - selected?: boolean; - - /** - * Specifies the Color style of the button. Options include 'Primary', 'Secondary', 'Warning', 'Success', 'Danger', and 'Info'. - * - * @default Color.Primary - */ - color?: Color; - - /** - * Specifies the variant style of the button. Options include 'Outlined', 'Filled', and 'Flat'. - * - * @default Variant.Filled - */ - variant?: Variant; - - /** - * Specifies the size style of the button. Options include 'Small', 'Medium' and 'Large'. - * - * @default Size.Medium - */ - size?: Size; - - /** - * Styles the button to visually appear as a hyperlink. When true, the button text is underlined. - * - * @default false - */ - isLink?: boolean; - - /** - * Specifies the dropdown button icon. - * - * @default false - * @private - */ - dropIcon?: boolean; -} - -/** - * Interface representing the Button component methods. - */ -export interface IButton extends ButtonProps { - - /** - * This is button component element. - * - * @private - * @default null - */ - element?: HTMLElement | null; - -} - -type IButtonProps = IButton & ButtonHTMLAttributes; - -/** - * The Button component is a versatile element for creating styled buttons with functionalities like toggling, icon positioning, and HTML attribute support, enhancing interaction based on its configuration and state. - * - * ```typescript - * - * ``` - */ -export const Button: React.ForwardRefExoticComponent> = - forwardRef((props: IButtonProps, ref: Ref) => { - const buttonRef: React.RefObject = useRef(null); - const { - disabled = false, - iconPosition = IconPosition.Left, - icon, - className = '', - dropIcon = false, - togglable = false, - selected, - color = Color.Primary, - variant = Variant.Filled, - size, - isLink = false, - onClick, - children, - ...domProps - } = props; - - const [isActive, setIsActive] = useState(selected ?? false); - const { dir, ripple } = useProviderContext(); - const { rippleMouseDown, Ripple} = useRippleEffect(ripple, { duration: 500 }); - const caretIcon: string = 'M5 8.5L12 15.5L19 8.5'; - const publicAPI: Partial = { - iconPosition, - icon, - togglable, - selected, - color, - variant, - size, - isLink - }; - - const handleButtonClick: React.MouseEventHandler = (event: React.MouseEvent) => { - if (togglable && selected === undefined) { - setIsActive((prevState: boolean) => !prevState); - } - onClick?.(event); - }; - - useEffect(() => { - if (selected !== undefined) { - setIsActive(selected); - } - }, [selected]); - - useEffect(() => { - preRender('btn'); - }, []); - - useImperativeHandle(ref, () => ({ - ...publicAPI as IButton, - element: buttonRef.current - }), [publicAPI]); - - const classNames: string = [ - 'sf-btn', - className, - dir === 'rtl' ? 'sf-rtl' : '', - isActive ? 'sf-active' : '', - isLink ? 'sf-link' : '', - icon && !children ? 'sf-icon-btn' : '', - iconPosition && `sf-${iconPosition.toLowerCase()}`, - color && color.toLowerCase() !== 'secondary' ? `sf-${color.toLowerCase()}` : '', - variant ? `sf-${variant.toLowerCase() }` : '', - size && size.toLowerCase() !== 'medium' ? `sf-${size.toLowerCase()}` : '' - ].filter(Boolean).join(' '); - - return ( - - ); - }); - -export default React.memo(Button); diff --git a/components/buttons/src/button/index.ts b/components/buttons/src/button/index.ts deleted file mode 100644 index 1c9f264..0000000 --- a/components/buttons/src/button/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Button modules - */ -export * from './button'; diff --git a/components/buttons/src/check-box/check-box.tsx b/components/buttons/src/check-box/check-box.tsx deleted file mode 100644 index d9fecfa..0000000 --- a/components/buttons/src/check-box/check-box.tsx +++ /dev/null @@ -1,350 +0,0 @@ -import { useState, useEffect, useCallback, useImperativeHandle, useRef, forwardRef, Ref, JSX, InputHTMLAttributes, ChangeEvent } from 'react'; -import { preRender, useProviderContext, SvgIcon, useRippleEffect } from '@syncfusion/react-base'; -import {Color, LabelPlacement, Size} from '../button/button'; - -const CHECK: string = 'sf-check'; -const DISABLED: string = 'sf-checkbox-disabled'; -const FRAME: string = 'sf-frame'; -const INDETERMINATE: string = 'sf-stop'; -const LABEL: string = 'sf-label'; -const WRAPPER: string = 'sf-checkbox-wrapper'; -const CheckBoxClass: string = 'sf-control sf-checkbox sf-lib'; - -/** - * Properties interface for the CheckBox component - * - */ -export interface CheckBoxProps { - - /** - * Specifies if the CheckBox is in an `indeterminate` state, which visually presents it as neither checked nor unchecked; setting this to `true` will make the CheckBox appear in an indeterminate state. - * - * @default false - */ - indeterminate?: boolean; - - /** - * Defines the text label for the Checkbox component, helping users understand its purpose. - * - * @default - - */ - label?: string; - - /** - * Specifies the size style of the checkbox. Options include 'Small', 'Medium' and 'Large'. - * - * @default Size.Medium - */ - size?: Size; - - icon?: React.ReactNode; - - checkedIcon?: React.ReactNode; - - /** - * Specifies the position of the label relative to the CheckBox. It determines whether the label appears before or after the checkbox element in the UI. - * - * @default 'After' - */ - labelPlacement?: LabelPlacement; - - /** - * Specifies a value that indicates whether the CheckBox is `checked` or not. When set to `true`, the CheckBox will be in `checked` state. - * - * @default false - */ - checked?: boolean; - - /** - * Specifies the Color style of the button. Options include 'Primary', 'Secondary', 'Warning', 'Success', 'Danger', and 'Info'. - * - * @default - - */ - color?: Color; - - /** - * Defines `value` attribute for the CheckBox. It is a form data passed to the server when submitting the form. - * - * - * @default - - */ - value?: string; - - /** - * Triggers when the CheckBox state has been changed by user interaction, allowing custom logic to be executed in response to the state change. - * - * @event change - */ - onChange?: (args: ChangeEvent) => void; -} - -/** - * Interface to define the structure of the CheckBox component reference instance - * - */ -export interface ICheckBox extends CheckBoxProps { - /** - * This is checkbox component element. - * - * @private - * @default null - */ - element?: HTMLElement | null; -} - -type ICheckBoxProps = ICheckBox & InputHTMLAttributes; - -/** - * The CheckBox component allows users to select one or multiple options from a list, providing a visual representation of a binary choice with states like checked, unchecked, or indeterminate. - * - * ```typescript - * - * ``` - */ - -export const Checkbox: React.ForwardRefExoticComponent> = - forwardRef((props: ICheckBoxProps, ref: Ref) => { - const { - onChange, - checked, - color, - icon, - checkedIcon, - className = '', - disabled = false, - indeterminate = false, - labelPlacement = 'After', - name = '', - value = '', - size = Size.Medium, - ...domProps - } = props; - - const isControlled: boolean = checked !== undefined; - const [checkedState, setCheckedState] = useState(() => { - if (isControlled) { - return checked!; - } - return domProps.defaultChecked || false; - }); - - const [isIndeterminate, setIsIndeterminate] = useState(indeterminate); - const [isFocused, setIsFocused] = useState(false); - const [storedLabel, setStoredLabel] = useState(props.label ?? ''); - const [storedLabelPosition, setStoredLabelPosition] = useState( - labelPlacement ?? 'After' - ); - - const inputRef: React.RefObject = useRef(null); - const wrapperRef: React.RefObject = useRef(null); - const rippleContainerRef: React.RefObject = useRef(null); - const { dir, ripple } = useProviderContext(); - const { rippleMouseDown, Ripple} = useRippleEffect(ripple, {isCenterRipple: true}); - const checkIcon: string = 'M23.8284 3.75L8.5 19.0784L0.17157 10.75L3 7.92157L8.5 13.4216L21 0.92157L23.8284 3.75Z'; - const indeterminateIcon: string = 'M0.5 0.5H17.5V3.5H0.5V0.5Z'; - - const publicAPI: Partial = { - checked, - indeterminate, - value, - color, - size, - icon, - checkedIcon - }; - - useImperativeHandle(ref, () => ({ - ...publicAPI as ICheckBox, - element: inputRef.current - }), [publicAPI]); - - useEffect(() => { - if (isControlled) { - setCheckedState(!!checked); - } - }, [checked, isControlled]); - - useEffect(() => { - setStoredLabel(props.label ?? ''); - setStoredLabelPosition(labelPlacement ?? 'After'); - }, [props.label, labelPlacement]); - - useEffect(() => { - preRender('checkbox'); - }, []); - - useEffect(() => { - setIsIndeterminate(indeterminate); - }, [indeterminate]); - - const handleStateChange: React.ChangeEventHandler = useCallback( - (event: React.ChangeEvent): void => { - const newChecked: boolean = event.target.checked; - setIsIndeterminate(false); - setIsFocused(false); - if (!isControlled) { - setCheckedState(newChecked); - } - onChange?.(event); - }, - [onChange, isControlled] - ); - - const handleFocus: () => void = () => { - setIsFocused(true); - }; - - const handleBlur: () => void = () => { - setIsFocused(false); - }; - - const handleMouseDown: (e: React.MouseEvent) => void = useCallback((e: React.MouseEvent) => { - if (ripple && rippleContainerRef.current && rippleMouseDown) { - const syntheticEvent: React.MouseEvent = { - ...e, - currentTarget: rippleContainerRef.current, - target: rippleContainerRef.current, - preventDefault: e.preventDefault, - stopPropagation: e.stopPropagation - } as React.MouseEvent; - rippleMouseDown(syntheticEvent); - } - }, [ripple, rippleMouseDown]); - - const wrapperClass: string = [ - WRAPPER, - className, - color && color.toLowerCase() !== 'secondary' ? `sf-${color.toLowerCase()}` : '', - disabled ? DISABLED : '', - isFocused ? 'sf-focus' : '', - !disabled && dir === 'rtl' ? 'sf-rtl' : '', - size && size.toLowerCase() !== 'medium' ? `sf-${size.toLowerCase()}` : '' - ].filter(Boolean).join(' '); - - const renderRipple: () => JSX.Element = () => ( - - {ripple && } - - ); - - const renderLabel: (label: string) => JSX.Element = (label: string) => ( - {label} - ); - - const renderIcons: () => JSX.Element = () => { - const sizeDimensions: any = { - Small: { width: '12', height: '12', viewBox: '0 0 26 20' }, - Medium: { width: '12', height: '12', viewBox: '0 0 25 20' }, - Large: { width: '16', height: '16', viewBox: '0 0 26 20' } - }; - const dimensions: any = sizeDimensions[size as keyof typeof sizeDimensions] || sizeDimensions.Medium; - return ( - - {isIndeterminate && ( - - )} - {checkedState && !isIndeterminate && ( - - )} - - ); - }; - - return ( -
- -
- ); - }); - -Checkbox.displayName = 'CheckBox'; -export default Checkbox; - -// Define the type for the component's props -interface CSSCheckBoxProps { - className?: string; - checked?: boolean; - label?: string; -} - -const createCSSCheckBox: (props: CSSCheckBoxProps) => JSX.Element = (props: CSSCheckBoxProps): JSX.Element => { - const { - className = '', - checked = false, - label = '', - ...domProps - } = props; - const { dir, ripple } = useProviderContext(); - const checkIcon: string = 'M23.8284 3.75L8.5 19.0784L0.17157 10.75L3 7.92157L8.5 13.4216L21 0.92157L23.8284 3.75Z'; - const rippleContainerRef: React.RefObject = useRef(null); - const { rippleMouseDown, Ripple} = useRippleEffect(ripple, {isCenterRipple: true}); - const handleMouseDown: (e: React.MouseEvent) => void = useCallback((e: React.MouseEvent) => { - if (ripple && rippleContainerRef.current && rippleMouseDown) { - const syntheticEvent: React.MouseEvent = { - ...e, - currentTarget: rippleContainerRef.current, - target: rippleContainerRef.current, - preventDefault: e.preventDefault, - stopPropagation: e.stopPropagation - } as React.MouseEvent; - rippleMouseDown(syntheticEvent); - } - }, [ripple, rippleMouseDown]); - return ( -
- { - {ripple && } - } - - {checked && ( - - )} - - {label && ( - {label} - )} -
- ); -}; - -// Component definition for CheckBox using create function -export const CSSCheckbox: React.FC = (props: CSSCheckBoxProps): JSX.Element => { - return createCSSCheckBox(props); -}; diff --git a/components/buttons/src/check-box/index.ts b/components/buttons/src/check-box/index.ts deleted file mode 100644 index 5d25ea5..0000000 --- a/components/buttons/src/check-box/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * CheckBox modules - */ -export * from './check-box'; diff --git a/components/buttons/src/chipList/chip-list.tsx b/components/buttons/src/chip-list/chip-list.tsx similarity index 80% rename from components/buttons/src/chipList/chip-list.tsx rename to components/buttons/src/chip-list/chip-list.tsx index c152e69..4f988bf 100644 --- a/components/buttons/src/chipList/chip-list.tsx +++ b/components/buttons/src/chip-list/chip-list.tsx @@ -1,17 +1,12 @@ import { forwardRef, HTMLAttributes, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { ChipModel, Chip, DeleteEvent, IChip, ChipColor } from '../chip/chip'; +import { ChipBaseProps, Chip, ChipDeleteEvent, IChip, ChipColor } from '../chip/chip'; import { isNullOrUndefined, preRender, useProviderContext } from '@syncfusion/react-base'; import * as React from 'react'; /** * Selection types for ChipList */ -export type SelectionType = 'single' | 'multiple' | 'none'; - -/** - * Defines the possible chip data types - */ -export type ChipData = string | number | ChipItemProps; +export type SelectionType = 'Single' | 'Multiple' | 'None'; /** * @ignore @@ -25,13 +20,10 @@ export interface ChipListProps { * * @default [] */ - chips?: ChipData[]; + chips?: string[] | ChipItemProps[]; /** * Specifies a value that indicates whether the ChipList component is disabled or not. - * ```html - * - * ``` * * @default false */ @@ -48,20 +40,14 @@ export interface ChipListProps { selectedChips?: number[]; /** - * Defines the selection type of the ChipList. The available types are: - * 1. single - * 2. multiple - * 3. none + * Defines the selection type of the ChipList. The available types are single, multiple, and none. * - * @default 'none' + * @default 'None' */ selection?: SelectionType; /** * Enables or disables the delete functionality of a ChipList. - * ```html - * - * ``` * * @default false */ @@ -72,24 +58,21 @@ export interface ChipListProps { * * @event onDelete */ - onDelete?: (args: ChipDeleteEvent) => void; + onDelete?: (event: ChipListDeleteEvent) => void; /** * Triggers when the selected chips in the ChipList change. - * ```html - * console.log(args)} /> - * ``` * * @event onSelect */ - onSelect?: (args: ChipSelectEvent) => void; + onSelect?: (event: ChipListSelectEvent) => void; } /** * Represents the properties of a Chip component. */ -export interface ChipItemProps extends ChipModel { +export interface ChipItemProps extends ChipBaseProps { /** * Specifies the custom classes to be added to the chip element. @@ -117,7 +100,7 @@ export interface ChipItemProps extends ChipModel { /** * Represents the arguments for the chip selection event. */ -export interface ChipSelectEvent { +export interface ChipListSelectEvent { /** * Specifies the indexes of the chips that are currently selected. */ @@ -132,11 +115,11 @@ export interface ChipSelectEvent { /** * Represents the arguments for the chip deletion event. */ -export interface ChipDeleteEvent { +export interface ChipListDeleteEvent { /** * Specifies the remaining chips after deletion. */ - chips: ChipData[]; + chips: string[] | ChipItemProps[]; /** * Specifies the event that triggered the delete action. @@ -159,9 +142,9 @@ export interface IChipList extends ChipListProps { * Gets the selected chips from the ChipList. * * @public - * @returns {ChipData[]} + * @returns {string[] | ChipItemProps[]} */ - getSelectedChips(): ChipData[]; + getSelectedChips(): string[] | ChipItemProps[]; } type ChipListComponentProps = ChipListProps & Omit, | 'onSelect'>; @@ -171,13 +154,15 @@ type ChipListComponentProps = ChipListProps & Omit + * import { ChipList } from "@syncfusion/react-buttons"; + * + * * ``` */ export const ChipList: React.ForwardRefExoticComponent> = forwardRef((props: ChipListComponentProps, ref: React.Ref) => { const chipListRef: React.RefObject = useRef(null); - const [chipData, setChipData] = useState<(string | number | ChipItemProps)[]>([]); + const [chipData, setChipData] = useState([]); const [selectedIndexes, setSelectedIndexes] = useState([]); const [focusedIndex, setFocusedIndex] = useState(null); const prevSelectedChipsRef: React.RefObject = useRef([]); @@ -188,7 +173,7 @@ forwardRef((props: ChipListComponentProps, ref: React. className, disabled = false, selectedChips = [], - selection = 'none', + selection = 'None', removable = false, onClick, onDelete, @@ -197,9 +182,9 @@ forwardRef((props: ChipListComponentProps, ref: React. } = props; useEffect(() => { - let newChipData: (number | string | ChipItemProps)[] | null = null; + let newChipData: string[] | ChipItemProps[] | null = null; if (chips.length > 0) { - newChipData = chips.map((chip: string | number | ChipItemProps) => chip); + newChipData = chips.map((chip: ChipItemProps) => chip) as string[] | ChipItemProps[]; } if (newChipData !== null) { setChipData(newChipData); @@ -211,10 +196,10 @@ forwardRef((props: ChipListComponentProps, ref: React. useEffect(() => { if (selectedIndexes.length > 0) { - if (selection === 'single') { + if (selection === 'Single') { setSelectedIndexes((prev: number[]) => [prev[prev.length - 1]]); } - else if (selection === 'none') { + else if (selection === 'None') { setSelectedIndexes([]); } } @@ -222,11 +207,11 @@ forwardRef((props: ChipListComponentProps, ref: React. useEffect(() => { if ((selectedChips && chipData.length > 0 && JSON.stringify(prevSelectedChipsRef.current) !== JSON.stringify(selectedChips))) { - if (selection === 'none') { return; } + if (selection === 'None') { return; } let finalSelectedIndexes: number[] = []; - if (selection === 'single') { + if (selection === 'Single') { finalSelectedIndexes = selectedChips.slice(0, 1); - } else if (selection === 'multiple') { + } else if (selection === 'Multiple') { finalSelectedIndexes = selectedChips; } setSelectedIndexes(finalSelectedIndexes); @@ -236,7 +221,7 @@ forwardRef((props: ChipListComponentProps, ref: React. useEffect(() => { if (chips.length > 0) { - setChipData(chips as (string | number | ChipItemProps)[]); + setChipData(chips); } }, [chips]); @@ -258,21 +243,21 @@ forwardRef((props: ChipListComponentProps, ref: React. removable }; - refInstance.getSelectedChips = (): ChipData[] => { - if (selection === 'none' || selectedIndexes.length === 0) { + refInstance.getSelectedChips = (): string[] | ChipItemProps[] => { + if (selection === 'None' || selectedIndexes.length === 0) { return []; } - const data: ChipData[] = []; + const data: (string | ChipItemProps)[] = []; selectedIndexes.forEach((index: number) => { - const chip: string | number | ChipItemProps = chipData[index as number]; + const chip: string | ChipItemProps = chipData[index as number]; (data).push(chip); }); - if (selection === 'single') { - return data[0] ? [data[0]] : []; + if (selection === 'Single') { + return data[0] ? data as string[] | ChipItemProps[] : []; } - return data; + return data as string[] | ChipItemProps[]; }; useImperativeHandle(ref, () => ({ @@ -293,12 +278,12 @@ forwardRef((props: ChipListComponentProps, ref: React. if (onClick) { onClick(e as React.MouseEvent); } - if (selection !== 'none') { + if (selection !== 'None') { setFocusedIndex(null); let newSelectedIndexes: number[] = [...selectedIndexes]; - if (selection === 'single') { + if (selection === 'Single') { newSelectedIndexes = [index]; - } else if (selection === 'multiple') { + } else if (selection === 'Multiple') { newSelectedIndexes = selectedIndexes.includes(index) ? selectedIndexes.filter((i: number) => i !== index) : [...selectedIndexes, index]; @@ -316,13 +301,15 @@ forwardRef((props: ChipListComponentProps, ref: React. useCallback((e: React.MouseEvent | React.KeyboardEvent, index: number) => { e.stopPropagation(); if (onDelete) { - const updatedChips: (string | number | ChipItemProps)[] = - chipData.filter((_: string | number | ChipItemProps, i: number) => i !== index); + const updatedChips: string[] | ChipItemProps[] = + chipData.filter((_: string | ChipItemProps, i: number) => i !== index) as string[] | ChipItemProps[]; onDelete({event: e as React.MouseEvent, chips: updatedChips}); } else { - setChipData((prevChipData: ChipData[]) => prevChipData.filter((_: string | number | ChipItemProps, i: number) => i !== index)); + setChipData((prevChipData: string[] | ChipItemProps[]) => + prevChipData.filter((_: string | ChipItemProps, i: number) => + i !== index) as string[] | ChipItemProps[]); if (chipData.length > 1) { - if (selection !== 'none') { + if (selection !== 'None') { setSelectedIndexes((prevSelected: number[]) => prevSelected.filter((i: number) => i !== index) .map((i: number) => i > index ? i - 1 : i)); } @@ -354,8 +341,8 @@ forwardRef((props: ChipListComponentProps, ref: React. return (e: React.MouseEvent) => handleClick(e, index); }, [handleClick, selectedIndexes]); - const MemoizedOnDelete: (index: number) => (args: DeleteEvent) => void = useCallback((index: number) => { - return (args: DeleteEvent) => removable && handleDelete(args.event as React.MouseEvent, index); + const MemoizedOnDelete: (index: number) => (args: ChipDeleteEvent) => void = useCallback((index: number) => { + return (args: ChipDeleteEvent) => removable && handleDelete(args.event as React.MouseEvent, index); }, [removable, handleDelete]); const MemoizedOnFocus: (index: number) => () => void = useCallback((index: number) => { @@ -367,24 +354,24 @@ forwardRef((props: ChipListComponentProps, ref: React. return (e: React.KeyboardEvent) => handleKeyDown(e, index, chip); }, [handleKeyDown]); - const memoizedChipData: (string | number | ChipItemProps)[] = useMemo(() => chipData, [chipData]); + const memoizedChipData: string[] | ChipItemProps[] = useMemo(() => chipData, [chipData]); const memoizedSelectedIndexes: number[] = useMemo(() => selectedIndexes, [selectedIndexes]); const memoizedFocusedIndex: number | null = useMemo(() => focusedIndex, [focusedIndex]); - const renderChip: (chip: ChipItemProps | string | number, index: number, props: ChipListProps, selectedIndexes: number[], + const renderChip: (chip: string | ChipItemProps , index: number, props: ChipListProps, selectedIndexes: number[], focusedIndex: number | null, memoizedOnClick: (index: number) => (e: React.MouseEvent) => void, - MemoizedOnDelete: (index: number) => (args: DeleteEvent) => void, MemoizedOnFocus: (index: number) => () => void, + MemoizedOnDelete: (index: number) => (args: ChipDeleteEvent) => void, MemoizedOnFocus: (index: number) => () => void, handleBlur: () => void, MemoizedOnKeyDown: (index: number, chip: ChipItemProps) => (e: React.KeyboardEvent) => void ) => React.ReactNode = ( - chip: ChipItemProps | string | number, + chip: string | ChipItemProps, index: number, props: ChipListComponentProps, selectedIndexes: number[], focusedIndex: number | null, memoizedOnClick: (index: number) => (e: React.MouseEvent) => void, - MemoizedOnDelete: (index: number) => (args: DeleteEvent) => void, + MemoizedOnDelete: (index: number) => (args: ChipDeleteEvent) => void, MemoizedOnFocus: (index: number) => () => void, handleBlur: () => void, MemoizedOnKeyDown: (index: number, chip: ChipItemProps) => (e: React.KeyboardEvent) => void @@ -396,15 +383,15 @@ forwardRef((props: ChipListComponentProps, ref: React. const isEnabled: boolean = chipProps.disabled !== true && props.disabled !== true; const chipClassNames: string = [ 'sf-chip', - selection === 'multiple' ? 'sf-selectable' : '', + selection === 'Multiple' ? 'sf-selectable' : '', className ? className : props.className, - isEnabled ? '' : 'sf-disabled', + isEnabled ? '' : `sf-disabled sf-chip-${chipProps.color ? 'variant' : 'invariant'}-disabled`, isSelected ? 'sf-active' : '', isFocused ? 'sf-focused' : '', chipProps.avatar ? 'sf-chip-avatar-wrap' : chipProps.leadingIcon ? 'sf-chip-icon-wrap' : '', - chipProps.variant === 'outlined' ? 'sf-outline' : '', - chipProps.color ? `sf-${color}` : '' + chipProps.variant === 'Outlined' ? 'sf-outline' : '', + chipProps.color ? `sf-${color?.toLowerCase()}` : '' ].filter(Boolean).join(' '); const { onClick, ...otherHtmlAttributes }: React.HTMLAttributes = htmlAttributes || {}; return ( @@ -434,7 +421,7 @@ forwardRef((props: ChipListComponentProps, ref: React. }; const renderContent: React.ReactNode = useMemo(() => - memoizedChipData.map((chip: string | number | ChipItemProps, index: number) => + memoizedChipData.map((chip: string | ChipItemProps, index: number) => renderChip( chip, index, @@ -455,8 +442,7 @@ forwardRef((props: ChipListComponentProps, ref: React. 'sf-control', 'sf-chip-list', 'sf-chip-set', - selection === 'multiple' ? 'sf-multi-selection' : selection === 'single' ? 'sf-selection' : '', - 'sf-lib', + selection === 'Multiple' ? 'sf-chip-multi-selection' : selection === 'Single' ? 'sf-chip-selection' : '', dir === 'rtl' ? 'sf-rtl' : '', props.className, !disabled ? '' : 'sf-disabled' @@ -468,7 +454,7 @@ forwardRef((props: ChipListComponentProps, ref: React. ref={chipListRef} className={classes} role="listbox" - aria-multiselectable={selection === 'multiple' ? 'true' : 'false'} + aria-multiselectable={selection === 'Multiple' ? 'true' : 'false'} aria-disabled={(disabled) ? 'true' : 'false'} {...otherProps} > diff --git a/components/buttons/src/chipList/index.ts b/components/buttons/src/chip-list/index.ts similarity index 100% rename from components/buttons/src/chipList/index.ts rename to components/buttons/src/chip-list/index.ts diff --git a/components/buttons/src/chip/chip.tsx b/components/buttons/src/chip/chip.tsx deleted file mode 100644 index d446014..0000000 --- a/components/buttons/src/chip/chip.tsx +++ /dev/null @@ -1,338 +0,0 @@ -import { forwardRef, HTMLAttributes, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react'; -import { preRender, useProviderContext, SvgIcon, useRippleEffect } from '@syncfusion/react-base'; -import * as React from 'react'; - -/** - * Represents the variant types for the Chip component. - */ -export type ChipVariant = 'filled' | 'outlined'; - -/** - * Represents the color types for the Chip component. - */ -export type ChipColor = 'primary' | 'info' | 'danger' | 'success' | 'warning'; - -/** - * Represents the model for the Chip component. - * - */ -export interface ChipModel { - /** - * Specifies the text content for the Chip. - * - * @default - - */ - text?: string; - - /** - * Defines the value of the Chip. - * - * @default - - */ - value?: string | number; - - /** - * Specifies the icon CSS class or React node for the avatar in the Chip. - * - * @default - - */ - avatar?: string | React.ReactNode; - - /** - * Specifies the leading icon CSS class or React node for the Chip. - * - * @default - - */ - leadingIcon?: string | React.ReactNode; - - /** - * Specifies the trailing icon CSS or React node for the Chip. - * - * @default - - */ - trailingIcon?: string | React.ReactNode; - - /** - * Specifies whether the Chip component is disabled or not. - * - * @default false - */ - disabled?: boolean; - - /** - * Specifies the leading icon url for the Chip. - * - * @default - - */ - leadingIconUrl?: string; - - /** - * Specifies the trailing icon url for the Chip. - * - * @default - - */ - trailingIconUrl?: string; - - /** - * Specifies whether the Chip is removable. - * - * @default false - */ - removable?: boolean; - - /** - * Specifies the variant of the Chip, either 'filled' or 'outlined'. - * - * @default - - */ - variant?: ChipVariant; - - /** - * Specifies the color of the Chip, one of 'primary', 'info', 'danger', 'success', or 'warning'. - * - * @default - - */ - color?: ChipColor; -} - -/** - * Represents the props for the Chip component. - * - * @ignore - */ -export interface ChipProps extends ChipModel { - - /** - * Event handler for the delete action. - * @event onDelete - */ - onDelete?: (event: DeleteEvent) => void; -} - -/** - * Represents the arguments for the delete event of a Chip. - */ -export interface DeleteEvent { - /** - * Specifies the data associated with the deleted Chip. - */ - data: ChipModel; - - /** - * Specifies the event that triggered the delete action. - */ - event: React.MouseEvent | React.KeyboardEvent; -} - -/** - * Represents the interface for the Chip component. - */ -export interface IChip extends ChipProps { - /** - * Specifies the Chip component element. - * - * @private - */ - element?: HTMLDivElement | null; -} - -type ChipComponentProps = ChipProps & HTMLAttributes; - -/** - * The Chip component represents information in a compact form, such as entity attribute, text, or action. - * - * ```typescript - * Anne - * ``` - */ -export const Chip: React.ForwardRefExoticComponent> = -React.memo(forwardRef((props: ChipComponentProps, ref: React.Ref) => { - const { - value, - text, - avatar, - leadingIcon, - trailingIcon, - className, - disabled = false, - leadingIconUrl, - trailingIconUrl, - children, - removable, - variant, - color, - onDelete, - onClick, - ...otherProps - } = props; - - const publicAPI: Partial = { - value, - text, - avatar, - leadingIcon, - trailingIcon, - disabled, - leadingIconUrl, - trailingIconUrl, - removable, - variant, - color - }; - - const chipRef: React.RefObject = useRef(null); - const [isFocused, setIsFocused] = useState(false); - const { dir, ripple } = useProviderContext(); - const closeIcon: string = 'M10.5858 12.0001L2.58575 4.00003L3.99997 2.58582L12 10.5858L20 2.58582L21.4142 4.00003L13.4142 12.0001L21.4142 20L20 21.4142L12 13.4143L4.00003 21.4142L2.58581 20L10.5858 12.0001Z'; - const selectIcon: string = 'M21.4142 6L9.00003 18.4142L2.58582 12L4.00003 10.5858L9.00003 15.5858L20 4.58578L21.4142 6Z'; - const { rippleMouseDown, Ripple} = useRippleEffect(ripple); - - useLayoutEffect(() => { - preRender('chip'); - }, []); - - useImperativeHandle(ref, () => ({ - ...publicAPI as IChip, - element: chipRef.current - })); - - const handleDelete: (e: React.MouseEvent | React.KeyboardEvent) => void = - React.useCallback((e: React.MouseEvent | React.KeyboardEvent) => { - if (!removable) {return; } - e.stopPropagation(); - const eventArgs: DeleteEvent = { - event: e, - data: props - }; - - if (onDelete) { - onDelete(eventArgs); - } - }, [onDelete, text, props]); - - const handleSpanDelete: React.MouseEventHandler = React.useCallback((e: React.MouseEvent) => { - if (removable) { - handleDelete(e as unknown as React.MouseEvent); - } - }, [removable, handleDelete]); - - const handleClick: React.MouseEventHandler = - React.useCallback((e: React.MouseEvent | React.KeyboardEvent) => { - if (onClick) { - onClick(e as React.MouseEvent); - } - }, [onClick, text, props]); - - const handleKeyDown: React.KeyboardEventHandler = React.useCallback((e: React.KeyboardEvent) => { - switch (e.key) { - case 'Enter': - case ' ': - e.preventDefault(); - handleClick(e as unknown as React.MouseEvent); - break; - case 'Delete': - case 'Backspace': - if (removable) { - e.preventDefault(); - handleDelete(e); - } - break; - } - }, [removable, handleClick, handleDelete]); - - const handleFocus: React.FocusEventHandler = React.useCallback(() => { - setIsFocused(true); - }, []); - - const handleBlur: React.FocusEventHandler = React.useCallback(() => { - setIsFocused(false); - }, []); - - const chipClassName: string = React.useMemo(() => { - if (className?.includes('sf-chip')) { - return className; - } - return [ - 'sf-chip sf-control sf-lib sf-chip-list', - className, - disabled ? 'sf-disabled' : '', - dir === 'rtl' ? 'sf-rtl' : '', - avatar ? 'sf-chip-avatar-wrap' : - leadingIcon ? 'sf-chip-icon-wrap' : '', - isFocused ? 'sf-focused' : '', - variant === 'outlined' ? 'sf-outline' : '', - color ? `sf-${color}` : '' - ].filter(Boolean).join(' '); - }, [className, disabled, dir, avatar, leadingIcon, isFocused, variant, color]); - - const avatarClasses: string = React.useMemo(() => { - return [ - 'sf-chip-avatar', - typeof avatar === 'string' ? avatar : '' - ].filter(Boolean).join(' '); - }, [avatar]); - - const trailingIconClasses: string = React.useMemo(() => { - return [ - trailingIconUrl && !removable ? 'sf-trailing-icon-url' : 'sf-chip-delete', - removable ? 'sf-dlt-btn' : (typeof trailingIcon === 'string' ? trailingIcon : '') - ].filter(Boolean).join(' '); - }, [trailingIconUrl, removable, trailingIcon]); - - return ( -
- {chipClassName.includes('sf-selectable') && ( - - - - )} - {(avatar) && ( - - {typeof avatar !== 'string' && avatar} - - )} - {(leadingIcon && !avatar) && ( - typeof leadingIcon === 'string' ? - : - {leadingIcon} - )} - {(leadingIconUrl && !leadingIcon && !avatar) && ( - - {leadingIconUrl && (leading image)} - - )} - {children ? (
{children}
) : text ? ({text}) : null} - {(trailingIcon || trailingIconUrl || removable) && ( - - {removable && ( - - )} - {!removable && typeof trailingIcon !== 'string' && trailingIcon} - {!removable && trailingIconUrl && ( - trailing image - )} - - )} - {ripple && } -
- ); -})); - -export default Chip; diff --git a/components/buttons/src/chip/index.ts b/components/buttons/src/chip/index.ts deleted file mode 100644 index 8f133ba..0000000 --- a/components/buttons/src/chip/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Chip modules - */ -export * from './chip'; diff --git a/components/buttons/src/floating-action-button/floating-action-button.tsx b/components/buttons/src/floating-action-button/floating-action-button.tsx deleted file mode 100644 index 2e5b1e2..0000000 --- a/components/buttons/src/floating-action-button/floating-action-button.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { useRef, useImperativeHandle, forwardRef, ButtonHTMLAttributes, useEffect, Ref } from 'react'; -import { Button, IButton, IconPosition, Color, Size } from '../button/button'; -import { preRender, useProviderContext } from '@syncfusion/react-base'; -import * as React from 'react'; - -/** - * Defines the position of FAB (Floating Action Button) in target. - */ -export enum FabPosition { - /** - * Positions the FAB at the target's top left corner. - */ - TopLeft = 'TopLeft', - - /** - * Places the FAB on the top-center position of the target. - */ - TopCenter = 'TopCenter', - - /** - * Positions the FAB at the target's top right corner. - */ - TopRight = 'TopRight', - - /** - * Positions the FAB in the middle of target's left side. - */ - MiddleLeft = 'MiddleLeft', - - /** - * Positions the FAB in the center of target. - */ - MiddleCenter = 'MiddleCenter', - - /** - * Positions the FAB in the middle of target's right side. - */ - MiddleRight = 'MiddleRight', - - /** - * Positions the FAB at the target's bottom left corner. - */ - BottomLeft = 'BottomLeft', - - /** - * Places the FAB on the bottom-center position of the target. - */ - BottomCenter = 'BottomCenter', - - /** - * Positions the FAB at the target's bottom right corner. - */ - BottomRight = 'BottomRight' -} - -export interface FabButtonProps { - /** - * Specifies the position of the Floating Action Button (FAB) relative to its target element. Options may include positions such as top-left, top-right, bottom-left, and bottom-right. - * - * @default FabPosition.BottomRight - */ - position?: FabPosition; - - /** - * Determines the visibility of the Floating Action Button. When `true`, the FAB is visible; when `false`, it is hidden. - * - * @default true - */ - visible?: boolean; - - /** - * Enables toggle behavior for the FAB. If `true`, the FAB will act as a toggle button, changing state on each click. - * - * @default false - */ - togglable?: boolean; - - /** - * Defines an icon for the button, which can either be a CSS class name for custom styling or an SVG element for rendering. - * - * @default - - */ - icon?: string | React.ReactNode; - - /** - * Defines the position of the icon relative to the text on the FAB. Options may include 'Left', 'Right', 'Top', or 'Bottom'. - * - * @default IconPosition.Left - */ - iconPosition?: IconPosition; - - /** - * Specifies the Color style of the FAB button. Options include 'Primary', 'Secondary', 'Warning', 'Success', 'Danger', and 'Info'. - * - * @default Color.Primary - */ - color?: Color; - - /** - * Specifies the size style of the FAB button. Options include 'Small', 'Medium' and 'Large'. - * - * @default Size.Medium - */ - size?: Size; -} - -export interface IFabButton extends FabButtonProps { - - /** - * This is button component element. - * - * @private - * @default null - */ - element?: HTMLElement | null; - -} - -type IFabProps = FabButtonProps & ButtonHTMLAttributes; - -/** - * The Floating Action Button (FAB) component offers a prominent primary action for an application interface, prominently positioned and styled to stand out with custom icon support. - * - * ```typescript - * FAB - * ``` - */ - -export const Fab: React.ForwardRefExoticComponent> = - forwardRef((props: IFabProps, ref: Ref) => { - const buttonRef: any = useRef(null); - const { dir } = useProviderContext(); - const { - disabled = false, - position = FabPosition.BottomRight, - iconPosition = IconPosition.Left, - className = '', - togglable = false, - icon, - children, - color = Color.Primary, - size, - visible = true, - ...domProps - } = props; - const fabPositionClasses: string[] = getFabPositionClasses(position, dir); - const classNames: string = [ - 'sf-control', - 'sf-fab', - 'sf-lib', - 'sf-btn', - className || '', - visible ? '' : 'sf-fab-hidden', - dir === 'rtl' ? 'sf-rtl' : '', - icon && !children ? 'sf-icon-btn' : '', - ...fabPositionClasses - ].filter(Boolean).join(' '); - - const publicAPI: Partial = { - iconPosition, - icon, - togglable, - visible, - color, - size - }; - - useImperativeHandle(ref, () => ({ - ...publicAPI as IFabButton, - element: buttonRef.current?.element - }), [publicAPI]); - - useEffect(() => { - preRender('fab'); - }, []); - - function getFabPositionClasses(position: FabPosition | string, dir: string): string[] { - const positions: any = { - vertical: '', - horizontal: '', - middle: '', - align: '' - }; - if (['BottomLeft', 'BottomCenter', 'BottomRight'].indexOf(position) !== -1) { - positions.vertical = 'sf-fab-bottom'; - } else { - positions.middle = 'sf-fab-top'; - } - if (['MiddleLeft', 'MiddleRight', 'MiddleCenter'].indexOf(position) !== -1) { - positions.vertical = 'sf-fab-middle'; - } - if (['TopCenter', 'BottomCenter', 'MiddleCenter'].indexOf(position) !== -1) { - positions.align = 'sf-fab-center'; - } - const isRight: boolean = ['TopRight', 'MiddleRight', 'BottomRight'].indexOf(position) !== -1; - if ( - (!((dir === 'rtl') || isRight) || ((dir === 'rtl') && isRight)) - ) { - positions.horizontal = 'sf-fab-left'; - } else { - positions.horizontal = 'sf-fab-right'; - } - return Object.values(positions).filter(Boolean) as string[]; - } - - return ( - - ); - }); - -export default React.memo(Fab); diff --git a/components/buttons/src/floating-action-button/index.ts b/components/buttons/src/floating-action-button/index.ts deleted file mode 100644 index 702ad51..0000000 --- a/components/buttons/src/floating-action-button/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Floating Action Button modules - */ -export * from './floating-action-button'; diff --git a/components/buttons/src/index.ts b/components/buttons/src/index.ts deleted file mode 100644 index 7f97ebe..0000000 --- a/components/buttons/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Button all modules - */ -export * from './button/index'; -export * from './check-box/index'; -export * from './radio-button/index'; -export * from './floating-action-button/index'; -export * from './chip/index'; -export * from './chipList/index'; diff --git a/components/buttons/src/radio-button/index.ts b/components/buttons/src/radio-button/index.ts deleted file mode 100644 index c89029f..0000000 --- a/components/buttons/src/radio-button/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * RadioButton modules - */ -export * from './radio-button'; diff --git a/components/buttons/src/radio-button/radio-button.tsx b/components/buttons/src/radio-button/radio-button.tsx deleted file mode 100644 index 4a29d73..0000000 --- a/components/buttons/src/radio-button/radio-button.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import * as React from 'react'; -import { useRef, useImperativeHandle, useState, useEffect, forwardRef, Ref, ChangeEvent, InputHTMLAttributes } from 'react'; -import { preRender, useProviderContext, useRippleEffect } from '@syncfusion/react-base'; -import {Color, LabelPlacement, Size} from '../button/button'; - -/** - * Defines the properties for the RadioButton component. - */ -export interface RadioButtonProps { - /** - * Specifies a value that indicates whether the RadioButton is `checked` or not. When set to `true`, the RadioButton will be in `checked` state. - * - * @default false - */ - checked?: boolean; - - /** - * Defines the caption for the RadioButton, that describes the purpose of the RadioButton. - * - * @default - - */ - label?: string; - - /** - * Specifies the size style of the checkbox. Options include 'Small', 'Medium' and 'Large'. - * - * @default Size.Medium - */ - size?: Size; - - /** - * Specifies the Color style of the radio-button. Options include 'Primary', 'Secondary', 'Warning', 'Success', 'Danger', and 'Info'. - * - * @default - - */ - color?: Color; - - /** - * Specifies the position of the label relative to the RadioButton. It determines whether the label appears before or after the radio button element in the UI. - * - * @default LabelPlacement.After - */ - labelPlacement?: LabelPlacement; - - /** - * Defines `value` attribute for the RadioButton. It is a form data passed to the server when submitting the form. - * - * @default - - */ - value?: string; - - /** - * Event trigger when the RadioButton state has been changed by user interaction. - * - * @event change - */ - onChange?: (event: ChangeEvent) => void; -} - -export interface IRadioButton extends RadioButtonProps { - /** - * This is RadioButton component input element. - * - * @private - * @default null - */ - element?: HTMLInputElement | null; -} - -type IRadioButtonProps = IRadioButton & InputHTMLAttributes; - -/** - * The RadioButton component allows users to select a single option from a group, utilizing a circular input field that provides a clear user selection interface. - * - * ```typescript - * - * ``` - */ -export const RadioButton: React.ForwardRefExoticComponent> = - forwardRef((props: IRadioButtonProps, ref: Ref) => { - const { - checked, - className = '', - disabled = false, - label = '', - color, - size = Size.Medium, - labelPlacement = 'After', - name = '', - value = '', - onChange, - ...domProps - } = props; - const isControlled: boolean = checked !== undefined; - const [isChecked, setIsChecked] = useState(() => isControlled ? !!checked : !!domProps.defaultChecked); - const radioInputRef: React.RefObject = useRef(null); - const { dir, ripple } = useProviderContext(); - const { rippleMouseDown, Ripple} = useRippleEffect(ripple, { duration: 400, isCenterRipple: true }); - - useEffect(() => { - if (isControlled) { - setIsChecked(!!checked); - } - }, [checked, isControlled]); - - useEffect(() => { - preRender('radio'); - }, []); - - const publicAPI: Partial = { - checked: isChecked, - label, - labelPlacement, - value, - size, - color - }; - - useImperativeHandle(ref, () => ({ - ...publicAPI as IRadioButton, - element: radioInputRef.current - }), [publicAPI]); - - const onRadioChange: React.ChangeEventHandler = (event: ChangeEvent): void => { - if (!isControlled) { - setIsChecked(event.target.checked); - } - if (onChange) { - onChange(event); - } - }; - - const classNames: string = [ - 'sf-radio-wrapper', - 'sf-wrapper', - className, - size && size.toLowerCase() !== 'medium' ? `sf-${size.toLowerCase()}` : '', - color && color.toLowerCase() !== 'secondary' ? `sf-${color.toLowerCase()}` : '' - ].filter(Boolean).join(' '); - - const rtlClass: string = (dir === 'rtl') ? 'sf-rtl' : ''; - const labelBefore: boolean = labelPlacement === 'Before'; - const labelBottom: boolean = labelPlacement === 'Bottom'; - - return ( -
- - -
- ); - }); - -export default React.memo(RadioButton); diff --git a/components/buttons/styles/button/_layout.scss b/components/buttons/styles/button/_layout.scss deleted file mode 100644 index 273234a..0000000 --- a/components/buttons/styles/button/_layout.scss +++ /dev/null @@ -1,596 +0,0 @@ -@mixin icon-top-bottom { - display: block; - margin-top: 0; - width: auto; -} - -@mixin top-bottom-icon-btn { - line-height: 1; -} - -@mixin mat3-border-radius { - @if $skin-name == 'Material3' { - border-radius: 4px; - } -} - -@include export-module('button-layout') { - .sf-btn, - .sf-css.sf-btn { - /* stylelint-disable property-no-vendor-prefix */ - position: relative; - align-items: center; - display: inline-block; - -webkit-font-smoothing: antialiased; - border: $btn-border; - border-radius: $btn-border-radius; - box-sizing: border-box; - cursor: pointer; - font-family: $font-family; - font-size: $btn-font-size; - font-weight: $btn-font-weight; - justify-content: center; - line-height: $btn-text-line-height; - outline: none; - padding: $btn-padding; - text-align: center; - text-decoration: none; - text-transform: $btn-text-transform; - user-select: none; - vertical-align: middle; - white-space: nowrap; - @if $skin-name == 'Material3' { - letter-spacing: .15px; - } - @at-root { - &:disabled { - cursor: default; - } - - &:hover, - &:focus { - text-decoration: none; - } - - &::-moz-focus-inner { - border: 0; - padding: 0; - } - - & .sf-content { - vertical-align: text-bottom; - } - - & .sf-btn-icon { - display: inline-block; - font-size: $btn-icon-font-size; - margin-top: $btn-icon-margin-top; - vertical-align: middle; - width: $btn-icon-btn-width; - line-height: 1px; - - &.sf-icon-left { - padding-right: 8px; - margin-left: $btn-icon-margin; - } - - &.sf-icon-right { - width: $btn-icon-width; - margin-right: $btn-icon-margin; - } - - &.sf-icon-top { - padding-bottom: $btn-icon-top-bottom-padding; - @include icon-top-bottom; - } - - &.sf-icon-bottom { - padding-top: $btn-icon-top-bottom-padding; - @include icon-top-bottom; - } - - path { - fill-rule: evenodd; - clip-rule: evenodd; - fill: currentColor; - } - - svg { - height: 14px; - width: 14px; - } - } - - &.sf-icon-btn { - @if $skin-name != 'tailwind3' { - padding: $btn-icon-padding; - } - @if $skin-name == 'fluent2' { - line-height: 14px; - } - @include mat3-border-radius; - } - - &.sf-top-icon-btn, - &.sf-bottom-icon-btn { - @include top-bottom-icon-btn; - padding: $btn-top-icon-padding; - } - - &.sf-round { - border-radius: 50%; - height: $btn-round-height; - line-height: 1; - padding: 0; - width: $btn-round-width; - - & .sf-btn-icon { - font-size: $btn-round-font-size; - line-height: $btn-round-icon-line-height; - margin-top: 0; - width: auto; - } - } - - &.sf-rounded { - border-radius: 40px; - } - - &.sf-round-corner { - @if $skin-name == 'Material3' { - border-radius: 25px; - padding: $btn-padding; - } - } - - &.sf-rtl { - & .sf-icon-right { - margin-left: $btn-icon-margin; - margin-right: 0; - } - - & .sf-icon-left { - padding-left: 8px; - margin-left: 0; - margin-right: $btn-icon-margin; - } - } - - &.sf-flat { - border: $btn-flat-border; - } - - &.sf-small { - font-size: $btn-small-font-size; - line-height: $btn-small-text-line-height; - padding: $btn-small-padding; - &.sf-round-corner { - @if $skin-name == 'Material3' { - border-radius: 25px; - padding: $btn-small-padding; - } - } - - & .sf-btn-icon { - font-size: $btn-small-icon-font-size; - width: $btn-icon-small-width; - - &.sf-icon-left { - margin-left: $btn-small-icon-margin; - width: $btn-small-icon-width; - } - - &.sf-icon-right { - margin-right: $btn-small-icon-margin; - width: $btn-small-icon-width; - } - - &.sf-icon-top { - padding-bottom: $btn-small-icon-top-bottom-padding; - width: auto; - } - - &.sf-icon-bottom { - padding-top: $btn-small-icon-top-bottom-padding; - width: auto; - } - } - - &.sf-icon-btn { - padding: $btn-small-icon-padding; - @include mat3-border-radius; - } - - &.sf-top-icon-btn, - &.sf-bottom-icon-btn { - @include top-bottom-icon-btn; - padding: $btn-top-icon-padding; - } - - &.sf-round { - height: $btn-round-small-height; - line-height: 1; - padding: 0; - width: $btn-round-small-width; - @if $skin-name == 'bootstrap5.3' or $skin-name == 'Material3' { - border-radius: 50%; - } - - & .sf-btn-icon { - font-size: $btn-small-round-font-size; - line-height: $btn-small-round-icon-line-height; - width: auto; - } - } - - &.sf-rtl { - & .sf-icon-right { - margin-left: $btn-small-icon-margin; - margin-right: 0; - } - - & .sf-icon-left { - margin-left: 0; - margin-right: $btn-small-icon-margin; - } - } - } - - &.sf-block { - display: block; - width: 100%; - } - } - - &.sf-outlined.sf-link { - border-radius: 4px; - } - } - - .sf-small .sf-btn, - .sf-small.sf-btn, - .sf-small .sf-css.sf-btn, - .sf-small.sf-css.sf-btn { - font-size: $btn-small-font-size; - line-height: $btn-small-text-line-height; - padding: $btn-small-padding; - &.sf-round-corner { - @if $skin-name == 'Material3' { - border-radius: 25px; - padding: $btn-small-padding; - } - } - - & .sf-btn-icon { - font-size: $btn-small-icon-font-size; - width: $btn-icon-small-width; - - &.sf-icon-left { - margin-left: $btn-small-icon-margin; - width: $btn-small-icon-width; - } - - &.sf-icon-right { - margin-right: $btn-small-icon-margin; - width: $btn-small-icon-width; - } - - &.sf-icon-top { - padding-bottom: $btn-small-icon-top-bottom-padding; - width: auto; - } - - &.sf-icon-bottom { - padding-top: $btn-small-icon-top-bottom-padding; - width: auto; - } - - svg { - height: 12px; - width: 12px; - } - } - - &.sf-icon-btn { - padding: $btn-small-icon-padding; - @if $skin-name == 'fluent2' { - line-height: 14px; - } - @include mat3-border-radius; - } - - &.sf-top-icon-btn, - &.sf-bottom-icon-btn { - @include top-bottom-icon-btn; - padding: $btn-top-icon-padding; - } - - &.sf-round { - height: $btn-round-small-height; - line-height: 1; - padding: 0; - width: $btn-round-small-width; - @if $skin-name == 'bootstrap5.3' or $skin-name == 'Material3' { - border-radius: 50%; - } - - & .sf-btn-icon { - font-size: $btn-small-round-font-size; - line-height: $btn-small-round-icon-line-height; - width: auto; - } - } - - &.sf-rtl { - & .sf-icon-right { - margin-left: $btn-small-icon-margin; - margin-right: 0; - } - - & .sf-icon-left { - margin-left: 0; - margin-right: $btn-small-icon-margin; - } - } - } - - @if $skin-name == 'fluent2' { - .sf-btn.sf-icon-btn .sf-btn-icon, - .sf-small .sf-btn.sf-icon-btn .sf-btn-icon { - font-size: 14px; - line-height: 14px; - } - } -} - -.sf-btn.sf-bottom, -.sf-btn.sf-top { - flex-direction: column; -} - -@include export-module('button-bigger') { - .sf-large.sf-small .sf-btn, - .sf-large .sf-small.sf-btn, - .sf-large.sf-small .sf-css.sf-btn, - .sf-large .sf-small.sf-css.sf-btn { - font-size: $btn-bigger-small-font-size; - line-height: $btn-bigger-small-text-line-height; - padding: $btn-bigger-small-padding; - - &.sf-round-corner { - @if $skin-name == 'Material3' { - border-radius: 25px; - padding: $btn-bigger-small-padding; - } - } - - & .sf-btn-icon { - font-size: $btn-bigger-small-icon-font-size; - width: $btn-icon-bigger-small-width; - - &.sf-icon-left { - margin-left: $btn-bigger-small-icon-margin; - width: $btn-bigger-small-icon-width; - } - - &.sf-icon-right { - margin-right: $btn-bigger-small-icon-margin; - width: $btn-bigger-small-icon-width; - } - - &.sf-icon-top { - padding-bottom: $btn-small-icon-top-bottom-padding; - width: auto; - } - - &.sf-icon-bottom { - padding-top: $btn-small-icon-top-bottom-padding; - width: auto; - } - - } - - &.sf-icon-btn { - padding: $btn-bigger-small-icon-padding; - @include mat3-border-radius; - } - - &.sf-top-icon-btn, - &.sf-bottom-icon-btn { - @include top-bottom-icon-btn; - padding: $btn-top-icon-bigger-padding; - } - - &.sf-round { - height: $btn-round-bigger-small-height; - line-height: 1; - padding: 0; - width: $btn-round-bigger-small-width; - - & .sf-btn-icon { - font-size: $btn-bigger-small-round-font-size; - line-height: $btn-bigger-small-round-icon-line-height; - width: auto; - } - } - - &.sf-rtl { - & .sf-icon-right { - margin-left: $btn-bigger-small-icon-margin; - margin-right: 0; - } - - & .sf-icon-left { - margin-left: 0; - margin-right: $btn-bigger-small-icon-margin; - } - } - } - - .sf-large .sf-btn, - .sf-large.sf-btn, - .sf-large .sf-css.sf-btn, - .sf-large.sf-css.sf-btn { - font-size: $btn-bigger-font-size; - line-height: $btn-bigger-text-line-height; - padding: $btn-bigger-padding; - &.sf-round-corner { - @if $skin-name == 'Material3' { - border-radius: 25px; - padding: $btn-bigger-padding; - } - } - - & .sf-btn-icon { - font-size: $btn-bigger-icon-font-size; - width: $btn-icon-bigger-width; - - &.sf-icon-left { - margin-left: $btn-bigger-icon-margin; - width: $btn-bigger-icon-width; - } - - &.sf-icon-right { - margin-right: $btn-bigger-icon-margin; - width: $btn-bigger-icon-width; - } - - &.sf-icon-top { - padding-bottom: $btn-bigger-icon-top-bottom-padding; - width: auto; - } - - &.sf-icon-bottom { - padding-top: $btn-bigger-icon-top-bottom-padding; - width: auto; - } - - svg { - height: 18px; - width: 18px; - } - } - - &.sf-icon-btn { - @if $skin-name != 'tailwind3' { - padding: $btn-bigger-icon-padding; - } - @if $skin-name == 'fluent2' { - line-height: 16px; - } - @include mat3-border-radius; - } - - &.sf-top-icon-btn, - &.sf-bottom-icon-btn { - @include top-bottom-icon-btn; - padding: $btn-top-icon-bigger-padding; - } - - &.sf-round { - height: $btn-round-bigger-height; - line-height: 1; - padding: 0; - width: $btn-round-bigger-width; - @if $skin-name == 'Material3' { - border-radius: 50%; - } - - & .sf-btn-icon { - font-size: $btn-bigger-round-font-size; - line-height: $btn-bigger-round-icon-line-height; - width: auto; - } - } - - &.sf-rtl { - & .sf-icon-right { - margin-left: $btn-bigger-icon-margin; - margin-right: 0; - } - - & .sf-icon-left { - margin-left: 0; - margin-right: $btn-bigger-icon-margin; - } - } - - &.sf-small { - font-size: $btn-bigger-small-font-size; - line-height: $btn-bigger-small-text-line-height; - padding: $btn-bigger-small-padding; - - &.sf-round-corner { - @if $skin-name == 'Material3' { - border-radius: 25px; - padding: $btn-bigger-small-padding; - } - } - - & .sf-btn-icon { - font-size: $btn-bigger-small-icon-font-size; - width: $btn-icon-bigger-small-width; - - &.sf-icon-left { - margin-left: $btn-bigger-small-icon-margin; - width: $btn-bigger-small-icon-width; - } - - &.sf-icon-right { - margin-right: $btn-bigger-small-icon-margin; - width: $btn-bigger-small-icon-width; - } - - &.sf-icon-top { - padding-bottom: $btn-small-icon-top-bottom-padding; - width: auto; - } - - &.sf-icon-bottom { - padding-top: $btn-small-icon-top-bottom-padding; - width: auto; - } - } - - &.sf-icon-btn { - padding: $btn-bigger-small-icon-padding; - @include mat3-border-radius; - } - - &.sf-top-icon-btn, - &.sf-bottom-icon-btn { - @include top-bottom-icon-btn; - padding: $btn-top-icon-bigger-padding; - } - - &.sf-round { - height: $btn-round-bigger-small-height; - line-height: 1; - padding: 0; - width: $btn-round-bigger-small-width; - @if $skin-name == 'Material3' { - border-radius: 50%; - } - - & .sf-btn-icon { - font-size: $btn-bigger-small-round-font-size; - line-height: $btn-bigger-small-round-icon-line-height; - width: auto; - } - } - - &.sf-rtl { - & .sf-icon-right { - margin-left: $btn-bigger-small-icon-margin; - margin-right: 0; - } - - & .sf-icon-left { - margin-left: 0; - margin-right: $btn-bigger-small-icon-margin; - } - } - } - } -} - diff --git a/components/buttons/styles/button/_material3-dark-definition.scss b/components/buttons/styles/button/_material3-dark-definition.scss deleted file mode 100644 index 356e259..0000000 --- a/components/buttons/styles/button/_material3-dark-definition.scss +++ /dev/null @@ -1 +0,0 @@ -@import './material3-definition.scss'; diff --git a/components/buttons/styles/button/_material3-definition.scss b/components/buttons/styles/button/_material3-definition.scss deleted file mode 100644 index 38989e8..0000000 --- a/components/buttons/styles/button/_material3-definition.scss +++ /dev/null @@ -1,375 +0,0 @@ -//layout variables -$btn-border: 1px solid !default; -$btn-icon-margin-top: 0 !default; -$btn-icon-btn-width: 1em !default; -$btn-icon-top-bottom-padding: 8px !default; -$btn-icon-small-width: 1em !default; -$btn-icon-bigger-small-width: 1em !default; -$btn-top-icon-padding: 12px 12px !default; -$btn-top-icon-bigger-padding: 16px 16px !default; -$btn-small-icon-top-bottom-padding: 6px !default; -$btn-icon-bigger-width: 1em !default; -$btn-focus-outline-offset: 0 !default; -$btn-active-outline-offset: 0 !default; -$btn-font-weight: 500 !default; -$btn-font-size: $text-sm !default; -$btn-small-font-size: 11px !default; -$btn-border-radius: 4px !default; -$btn-text-transform: none !default; -$btn-icon-font-size: $text-lg !default; -$btn-small-icon-font-size: $text-base !default; -$btn-round-small-height: 24px !default; -$btn-round-small-width: 24px !default; -$btn-round-height: 32px !default; -$btn-round-width: 32px !default; -$btn-round-bigger-small-height: 36px !default; -$btn-round-bigger-small-width: 36px !default; -$btn-round-bigger-height: 40px !default; -$btn-round-bigger-width: 40px !default; -$btn-round-font-size: $text-lg !default; -$btn-small-round-font-size: $text-base !default; -$btn-icon-margin: -.5em !default; -$btn-small-icon-margin: -.57143em !default; -$btn-icon-width: 2em !default; -$btn-small-icon-width: 2em !default; -$btn-round-icon-line-height: 1.5em !default; -$btn-small-round-icon-line-height: 1 !default; -$btn-text-line-height: 1 !default; -$btn-small-text-line-height: 1.092 !default; -$btn-padding: 8px 16px !default; -$btn-small-padding: 5px 12px !default; -$btn-icon-padding: 7.5px 16px !default; -$btn-small-icon-padding: 4px 4px !default; -$btn-bigger-small-font-size: $text-sm !default; -$btn-bigger-small-text-line-height: 1.476 !default; -$btn-bigger-small-padding: 7px 20px !default; -$btn-bigger-small-icon-font-size: $text-lg !default; -$btn-bigger-small-icon-margin: -.5em !default; -$btn-bigger-small-icon-width: 2em !default; -$btn-bigger-small-round-icon-line-height: 0 !default; -$btn-bigger-small-round-font-size: $text-lg !default; -$btn-bigger-icon-margin: -.2em !default; -$btn-bigger-icon-width: 2em !default; -$btn-bigger-text-line-height: 1 !default; -$btn-bigger-padding: 10px 24px !default; -$btn-bigger-font-size: $text-sm !default; -$btn-bigger-icon-font-size: 18px !default; -$btn-bigger-icon-top-bottom-padding: 8px !default; -$btn-bigger-icon-padding: 10px 10px !default; -$btn-bigger-round-icon-line-height: 1.5em !default; -$btn-bigger-round-font-size: $text-lg !default; -$btn-bigger-small-icon-padding: 6px 13px !default; - -//normal -$btn-color: rgba($secondary-text-color) !default; -$btn-bgcolor: $secondary-bg-color !default; -$btn-border-color: $btn-secondary-border-color !default; -$btn-hover-bgcolor: $secondary-bg-color-hover !default; -$btn-hover-border-color: $secondary-border-color-hover !default; -$btn-hover-color: rgba($secondary-text-color-hover) !default; -$btn-focus-bgcolor: $secondary-bg-color-focus !default; -$btn-focus-border-color: $secondary-border-color-hover !default; -$btn-focus-color: rgba($secondary-text-color-focus) !default; -$btn-active-outline: $btn-bgcolor 0 solid !default; -$btn-focus-outline: $btn-bgcolor 0 solid !default; -$btn-focus-outline-round: rgba($white) 0 solid !default; -$btn-active-border-color: $secondary-border-color-pressed !default; -$btn-active-bgcolor: $secondary-bg-color-pressed !default; -$btn-active-color: rgba($secondary-text-color-pressed) !default; -$btn-disabled-color: $secondary-text-color-disabled !default; -$btn-disabled-bgcolor: $secondary-bg-color-disabled !default; -$btn-disabled-border-color: $secondary-border-color-disabled !default; -$btn-active-box-shadow: $shadow-sm !default; -$btn-ripple-bgcolor: rgba($secondary-text-color, .24) !default; -$btn-link-bgcolor: transparent !default; -$btn-link-border-color: transparent !default; -$btn-link-color: rgba($info-dark) !default; -$btn-link-hover-color: rgba($info-dark) !default; -$btn-link-disabled-bgcolor: transparent !default; - -//primary -$btn-primary-outline: $btn-bgcolor 0 solid !default; -$btn-primary-color: rgba($primary-text) !default; -$btn-primary-hover-color: rgba($primary-text-hover) !default; -$btn-primary-focus-color: rgba($primary-text-focus) !default; -$btn-primary-active-color: rgba($primary-text-pressed) !default; -$btn-primary-bgcolor: rgba($primary-bg-color) !default; -$btn-primary-hover-bgcolor: $primary-bg-color-hover !default; -$btn-primary-active-bgcolor: $primary-bg-color-pressed !default; -$btn-primary-border-color: rgba($primary-border-color) !default; -$btn-primary-hover-border-color: $primary-border-color-hover !default; -$btn-primary-focus-border-color: $btn-primary-border-color !default; -$btn-primary-active-border-color: rgba($primary-bg-color) !default; -$btn-primary-focus-bgcolor: $primary-bg-color-focus !default; -$btn-primary-disabled-bgcolor: $primary-bg-color-disabled !default; -$btn-primary-disabled-color: $primary-text-disabled !default; -$btn-primary-disabled-border-color: $primary-border-color-disabled !default; -$btn-ripple-primary-bgcolor: rgba($primary-text, .24) !default; - -//outline -$btn-outline-color: rgba($secondary-outline) !default; -$btn-outline-bgcolor: transparent !default; -$btn-outline-hover-bgcolor: rgba($secondary-outline, .08) !default; -$btn-outline-hover-color: rgba($secondary-outline) !default; -$btn-outline-focus-bgcolor: $btn-focus-bgcolor !default; -$btn-outline-focus-color: $btn-focus-color !default; -$btn-outline-active-color: $btn-outline-color !default; -$btn-outline-active-bgcolor: rgba($secondary-outline, .12) !default; -$btn-outline-border-color: rgba($info-outline-border) !default; -$btn-outline-hover-border-color: rgba($info-outline-border) !default; -$btn-outline-focus-border-color: $btn-outline-border-color !default; -$btn-outline-default-focus-border-color: $btn-outline-border-color !default; -$btn-outline-active-border-color: $btn-outline-border-color !default; -$btn-outline-active-box-shadow: none !default; -$btn-outline-disabled-bgcolor: $secondary-bg-color-disabled !default; -$btn-outline-disabled-border-color: $success-border-color-disabled !default; -$btn-outline-disabled-color: $secondary-text-color-disabled !default; -$btn-outline-primary-color: $primary-outline !default; -$btn-outline-primary-focus-bgcolor: $btn-primary-bgcolor !default; -$btn-outline-primary-focus-color: rgba($white) !default; -$btn-outline-success-color: $success-outline !default; -$btn-outline-warning-color: $warning-outline !default; -$btn-outline-danger-color: $danger-outline !default; -$btn-outline-info-color: $info-outline !default; -$btn-outline-primary-hover-bgcolor: rgba($btn-outline-primary-color, .08) !default; -$btn-outline-success-hover-bgcolor: rgba($btn-outline-success-color, .08) !default; -$btn-outline-warning-hover-bgcolor: rgba($btn-outline-warning-color, .08) !default; -$btn-outline-danger-hover-bgcolor: rgba($btn-outline-danger-color, .08) !default; -$btn-outline-info-hover-bgcolor: rgba($btn-outline-info-color, .08) !default; -$btn-outline-primary-active-bgcolor: rgba($btn-outline-primary-color, .12) !default; -$btn-outline-success-active-bgcolor: rgba($btn-outline-success-color, .12) !default; -$btn-outline-warning-active-bgcolor: rgba($btn-outline-warning-color, .12) !default; -$btn-outline-danger-active-bgcolor: rgba($btn-outline-danger-color, .12) !default; -$btn-outline-info-active-bgcolor: rgba($btn-outline-info-color, .12) !default; - -//flat -$btn-flat-color: rgba($secondary-text-color) !default; -$btn-flat-border: $btn-border !default; -$btn-flat-border-color: $transparent !default; -$btn-flat-hover-color: $btn-flat-color !default; -$btn-flat-focus-color: $btn-focus-color !default; -$btn-flat-active-color: $btn-active-color !default; -$btn-flat-box-shadow: none !default; -$btn-flat-active-bgcolor: $btn-active-bgcolor !default; -$btn-flat-bgcolor: $transparent !default; -$btn-flat-hover-bgcolor: rgba($secondary-text-color, .08) !default; -$btn-flat-focus-bgcolor: $btn-flat-hover-bgcolor !default; -$btn-flat-hover-border-color: none !default; -$btn-flat-active-border-color: $btn-active-border-color !default; -$btn-flat-focus-border-color: $btn-flat-hover-border-color !default; -$btn-flat-active-box-shadow: none !default; -$btn-flat-disabled-border-color: $transparent !default; -$btn-flat-disabled-bgcolor: $transparent !default; -$btn-flat-disabled-color: $btn-disabled-color !default; -$btn-ripple-flat-bgcolor: rgba($secondary-text-color, .24) !default; -$btn-ripple-flat-primary-bgcolor: rgba($primary-text, .24) !default; -$btn-ripple-flat-success-bgcolor: rgba($success-text, .24) !default; -$btn-ripple-flat-info-bgcolor: rgba($info-text, .24) !default; -$btn-ripple-flat-warning-bgcolor: rgba($warning-text, .24) !default; -$btn-ripple-flat-danger-bgcolor: rgba($danger-text, .24) !default; - -//success -$btn-success-color: rgba($success-text) !default; -$btn-success-bgcolor: rgba($success-bg-color) !default; -$btn-success-hover-bgcolor: $success-bg-color-hover !default; -$btn-success-focus-bgcolor: $success-bg-color-focus !default; -$btn-success-active-bgcolor: $success-bg-color-pressed !default; -$btn-success-border-color: rgba($success-border-color) !default; -$btn-success-hover-border-color: $success-border-color-hover !default; -$btn-success-focus-border-color: $btn-success-border-color !default; -$btn-success-active-border-color: rgba($success-bg-color) !default; -$btn-success-disabled-bgcolor: $success-bg-color-disabled !default; -$btn-success-disabled-color: $success-text-disabled !default; -$btn-success-disabled-border-color: $success-border-color-disabled !default; -$btn-success-hover-color: rgba($success-text-hover) !default; -$btn-success-focus-color: rgba($success-text-focus) !default; -$btn-ripple-success-bgcolor: rgba($success-text, .24) !default; -$btn-success-active-color: $btn-success-color !default; - -//warning -$btn-warning-bgcolor: rgba($warning-bg-color) !default; -$btn-warning-color: rgba($warning-text) !default; -$btn-warning-hover-color: rgba($warning-text-hover) !default; -$btn-warning-hover-bgcolor: $warning-bg-color-hover !default; -$btn-warning-focus-bgcolor: $warning-bg-color-focus !default; -$btn-warning-active-bgcolor: $warning-bg-color-pressed !default; -$btn-warning-border-color: rgba($warning-border-color) !default; -$btn-warning-hover-border-color: $warning-border-color-hover !default; -$btn-warning-focus-border-color: $btn-warning-border-color !default; -$btn-warning-focus-color: rgba($warning-text-focus) !default; -$btn-warning-active-color: rgba($warning-text-pressed) !default; -$btn-warning-active-border-color: rgba($warning-bg-color) !default; -$btn-warning-disabled-bgcolor: $warning-bg-color-disabled !default; -$btn-warning-disabled-color: $warning-text-disabled !default; -$btn-warning-disabled-border-color: $warning-border-color-disabled !default; -$btn-ripple-warning-bgcolor: rgba($warning-text, .24) !default; - -//danger -$btn-danger-color: rgba($danger-text) !default; -$btn-danger-bgcolor: rgba($danger-bg-color) !default; -$btn-danger-hover-bgcolor: $danger-bg-color-hover !default; -$btn-danger-focus-bgcolor: $danger-bg-color-focus !default; -$btn-danger-active-bgcolor: $danger-bg-color-pressed !default; -$btn-danger-active-color: rgba($danger-text-pressed) !default; -$btn-danger-border-color: rgba($danger-border-color) !default; -$btn-danger-hover-border-color: $danger-border-color-hover !default; -$btn-danger-focus-border-color: $btn-danger-border-color !default; -$btn-danger-active-border-color: rgba($danger-bg-color) !default; -$btn-danger-disabled-bgcolor: $danger-bg-color-disabled !default; -$btn-danger-disabled-color: $warning-text-disabled !default; -$btn-danger-disabled-border-color: $danger-border-color-disabled !default; -$btn-danger-hover-color: rgba($danger-text-hover) !default; -$btn-ripple-danger-bgcolor: rgba($danger-text, .24) !default; - -//info -$btn-info-bgcolor: rgba($info-bg-color) !default; -$btn-info-color: rgba($info-text) !default; -$btn-info-hover-bgcolor: $info-bg-color-hover !default; -$btn-info-focus-bgcolor: $info-bg-color-focus !default; -$btn-info-active-bgcolor: $info-bg-color-pressed !default; -$btn-info-border-color: rgba($info-border-color) !default; -$btn-info-hover-border-color: $info-border-color-hover !default; -$btn-info-focus-border-color: $btn-info-border-color !default; -$btn-info-active-border-color: rgba($info-bg-color) !default; -$btn-info-disabled-bgcolor: $info-bg-color-disabled !default; -$btn-info-disabled-color: $info-text-disabled !default; -$btn-info-disabled-border-color: $info-border-color-disabled !default; -$btn-info-active-color: rgba($info-text-pressed) !default; -$btn-info-hover-color: rgba($info-text-hover) !default; -$btn-ripple-info-bgcolor: rgba($info-text, .24) !default; - -//round -$btn-round-focus-color: rgba($secondary-text-color) !default; -$btn-round-active-color: rgba($secondary-text-color-pressed) !default; -$btn-round-bgcolor: $btn-bgcolor !default; -$btn-round-border-color: $btn-border-color !default; -$btn-round-color: $btn-color !default; -$btn-round-hover-bgcolor: $btn-hover-bgcolor !default; -$btn-round-hover-border-color: $btn-hover-border-color !default; -$btn-round-hover-color: $btn-hover-color !default; - -//flatprimary -$btn-flat-primary-hover-bgcolor: rgba($primary-bg-color, .08) !default; -$btn-flat-primary-border-color: $btn-flat-border-color !default; -$btn-flat-primary-hover-border-color: none !default; -$btn-flat-primary-active-border-color: $transparent !default; -$btn-flat-primary-focus-border-color: $btn-flat-primary-border-color !default; -$btn-flat-primary-disabled-border-color: $transparent !default; -$btn-flat-primary-focus-bgcolor: rgba($primary-bg-color, .12) !default; -$btn-flat-disabled-color: $secondary-text-color-disabled !default; -$btn-flat-primary-disabled-color: $primary-text-disabled !default; -$btn-flat-primary-bgcolor: $btn-flat-bgcolor !default; -$btn-flat-primary-color: rgba($primary-bg-color) !default; -$btn-flat-primary-hover-color: rgba($primary-bg-color) !default; -$btn-flat-primary-focus-color: rgba($primary-bg-color) !default; -$btn-flat-primary-active-color: rgba($primary-bg-color) !default; -$btn-flat-primary-active-bgcolor: rgba($primary-bg-color, .12) !default; - -//flatsuccess -$btn-flat-success-color: rgba($success-bg-color) !default; -$btn-flat-success-hover-color: rgba($success-bg-color) !default; -$btn-flat-success-focus-color: rgba($success-bg-color) !default; -$btn-flat-success-active-color: rgba($success-bg-color) !default; -$btn-flat-success-active-bgcolor: rgba($success-bg-color, .12) !default; -$btn-flat-success-disabled-color: $btn-success-disabled-color !default; -$btn-flat-success-bgcolor: $btn-flat-bgcolor !default; -$btn-flat-success-hover-bgcolor: rgba($success-bg-color, .08) !default; -$btn-flat-success-border-color: $transparent !default; -$btn-flat-success-hover-border-color: $transparent !default; -$btn-flat-success-active-border-color: $transparent !default; -$btn-flat-success-focus-border-color: $transparent !default; -$btn-flat-success-disabled-border-color: $transparent !default; -$btn-flat-success-focus-bgcolor: rgba($success-bg-color, .12) !default; - -//flatinfo -$btn-flat-info-bgcolor: $btn-flat-bgcolor !default; -$btn-flat-info-hover-bgcolor: rgba($info-bg-color, .08) !default; -$btn-flat-info-border-color: $btn-flat-border-color !default; -$btn-flat-info-hover-border-color: $btn-flat-info-border-color !default; -$btn-flat-info-active-border-color: $btn-flat-info-border-color !default; -$btn-flat-info-focus-border-color: $btn-flat-info-border-color !default; -$btn-flat-info-disabled-border-color: $transparent !default; -$btn-flat-info-focus-bgcolor: rgba($info-bg-color, .12) !default; -$btn-flat-info-color: rgba($info-bg-color) !default; -$btn-flat-info-disabled-color: $btn-info-disabled-color !default; -$btn-flat-info-hover-color: rgba($info-bg-color) !default; -$btn-flat-info-focus-color: rgba($info-bg-color) !default; -$btn-flat-info-active-color: rgba($info-bg-color) !default; -$btn-flat-info-active-bgcolor: rgba($info-bg-color, .12) !default; - -//flatwarning -$btn-flat-warning-bgcolor: $btn-flat-bgcolor !default; -$btn-flat-warning-hover-bgcolor: rgba($warning-bg-color, .08) !default; -$btn-flat-warning-border-color: $btn-flat-border-color !default; -$btn-flat-warning-hover-border-color: $btn-flat-warning-border-color !default; -$btn-flat-warning-active-border-color: $btn-flat-warning-border-color !default; -$btn-flat-warning-focus-border-color: $btn-flat-warning-border-color !default; -$btn-flat-warning-disabled-border-color: $transparent !default; -$btn-flat-warning-focus-bgcolor: rgba($warning-bg-color, .12) !default; -$btn-flat-warning-color: rgba($warning-bg-color) !default; -$btn-flat-warning-disabled-color: $btn-warning-disabled-color !default; -$btn-flat-warning-hover-color: rgba($warning-bg-color) !default; -$btn-flat-warning-focus-color: rgba($warning-bg-color) !default; -$btn-flat-warning-active-color: rgba($warning-bg-color) !default; -$btn-flat-warning-active-bgcolor: rgba($warning-bg-color, .12) !default; - -//flatdanger -$btn-flat-danger-bgcolor: $btn-flat-bgcolor !default; -$btn-flat-danger-hover-bgcolor: rgba($danger-bg-color, .08) !default; -$btn-flat-danger-border-color: $btn-flat-border-color !default; -$btn-flat-danger-hover-border-color: $btn-flat-danger-border-color !default; -$btn-flat-danger-active-border-color: $btn-flat-danger-border-color !default; -$btn-flat-danger-focus-border-color: $btn-flat-danger-border-color !default; -$btn-flat-danger-disabled-border-color: $transparent !default; -$btn-flat-danger-focus-bgcolor: rgba($danger-bg-color, .12) !default; -$btn-flat-danger-color: rgba($danger-bg-color) !default; -$btn-flat-danger-hover-color: rgba($danger-bg-color) !default; -$btn-flat-danger-focus-color: rgba($danger-bg-color) !default; -$btn-flat-danger-active-color: rgba($danger-bg-color) !default; -$btn-flat-danger-active-bgcolor: rgba($danger-bg-color, .12) !default; -$btn-flat-danger-disabled-color: $btn-danger-disabled-color !default; - -//outlineprimary -$btn-outline-primary-disabled-color: $primary-text-disabled !default; -$btn-outline-primary-disabled-border-color: $success-border-color-disabled !default; -$btn-outline-primary-hover-border-color: rgba($primary) !default; -$btn-outline-primary-hover-bgcolor: $btn-primary-bgcolor !default; -$btn-outline-primary-focus-border-color: rgba($primary) !default; -$btn-outline-primary-active-border-color: rgba($primary) !default; - -//outlinesuccess -$btn-outline-success-disabled-color: $success-text-disabled !default; -$btn-outline-success-disabled-border-color: $success-border-color-disabled !default; -$btn-outline-success-hover-bgcolor: $btn-success-bgcolor !default; - -//outlineinfo -$btn-outline-info-disabled-color: $info-text-disabled !default; -$btn-outline-info-disabled-border-color: $success-border-color-disabled !default; - -//outlinewarning -$btn-outline-warning-disabled-color: $warning-text-disabled !default; -$btn-outline-warning-disabled-border-color: $success-border-color-disabled !default; - -//outlinedanger -$btn-outline-danger-disabled-color: $danger-text-disabled !default; -$btn-outline-danger-disabled-border-color: $success-border-color-disabled !default; - -//size -$btn-box-shadow: $shadow-sm !default; -$btn-hover-focus-box-shadow: $shadow-md !default; -$btn-flat-primary-disabled-bgcolor: $transparent !default; -$btn-flat-success-disabled-bgcolor: $transparent !default; -$btn-flat-info-disabled-bgcolor: $transparent !default; -$btn-flat-warning-disabled-bgcolor: $transparent !default; -$btn-flat-danger-disabled-bgcolor: $transparent !default; -$btn-focus-box-shadow: none !default; - -// bootstrap5 theme variables -$btn-primary-focus-box-shadow: $shadow-md !default; -$btn-success-focus-box-shadow: $shadow-md !default; -$btn-danger-focus-box-shadow: $shadow-md !default; -$btn-info-focus-box-shadow: $shadow-md !default; -$btn-warning-focus-box-shadow: $shadow-md !default; - -//Material 3 -$btn-keyboard-focus-box-shadow: $shadow-focus-ring1 !default; diff --git a/components/buttons/styles/button/_mixin.scss b/components/buttons/styles/button/_mixin.scss deleted file mode 100644 index 8619278..0000000 --- a/components/buttons/styles/button/_mixin.scss +++ /dev/null @@ -1,480 +0,0 @@ -@use 'sass:color'; -@mixin button-focus { - background: $btn-focus-bgcolor; - @if $skin-name == 'Material3' { - border-image: $btn-focus-border-color; - } - @else { - border-color: $btn-focus-border-color; - } - color: $btn-focus-color; - outline: $btn-focus-outline; - outline-offset: $btn-focus-outline-offset; - @if $skin-name != 'bootstrap5.3' and $skin-name != 'tailwind3' { - box-shadow: $btn-hover-focus-box-shadow; - } -} - -@mixin button-active { - background: $btn-active-bgcolor; - @if $skin-name == 'Material3' { - border-image: $btn-active-border-color; - } - @else { - border-color: $btn-active-border-color; - } - color: $btn-active-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-active-box-shadow; - } -} - -@mixin primary-focus { - background: $btn-primary-focus-bgcolor; - border-color: $btn-primary-focus-border-color; - color: $btn-primary-focus-color; - outline: $btn-primary-outline; - @if $skin-name == 'Material3' { - box-shadow: $btn-primary-focus-box-shadow; - } - @else { - @if $skin-name != 'tailwind3' { - box-shadow: $btn-hover-focus-box-shadow; - } - } -} - -@mixin primary-active { - background: $btn-primary-active-bgcolor; - border-color: $btn-primary-active-border-color; - color: $btn-primary-active-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-active-box-shadow; - } -} - -@mixin primary-disabled { - background: $btn-primary-disabled-bgcolor; - border-color: $btn-primary-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-primary-disabled-color; -} - -@mixin outline-focus { - @if $skin-name == 'Material3' { - background: $btn-outline-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: $btn-outline-color; - } - @else if $skin-name == 'fluent2' { - color: $secondary-outline-button-text-color-hover; - } - @else if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-hover-focus-box-shadow; - } - @else if $skin-name == 'tailwind3' { - background: $secondary-bg-color-focus; - border: 1px solid $secondary-border-color-focus; - color: $secondary-text-color-focus; - } -} - -@mixin outline-active { - - background: $btn-outline-active-bgcolor; - border-color: $btn-outline-active-border-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-outline-active-box-shadow; - } - color: $btn-outline-active-color; -} - -@mixin outline-primary-focus { - @if $skin-name == 'Material3' { - background: $btn-outline-primary-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-primary-color); - } - @if $skin-name == 'tailwind3' { - background: $primary-bg-color-focus; - border: 1px solid $primary-border-color-focus; - color: $primary-text-focus; - } -} - -@mixin outline-primary-active { - @if ($skin-name == 'bootstrap5.3') { - background: $btn-primary-bgcolor; - } - @else { - background: $btn-primary-active-bgcolor; - border-color: $btn-outline-primary-active-border-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-outline-active-box-shadow; - } - } - color: $btn-primary-active-color; - @if $skin-name == 'Material3' { - background: $btn-outline-primary-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-primary-color); - } -} - -@mixin outline-success-focus { - @if $skin-name == 'Material3' { - background: $btn-outline-success-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-success-color); - } - @else if $skin-name == 'tailwind3' { - background: $success-bg-color-focus; - border: 1px solid $success-border-color-focus; - color: $success-text-focus; - } -} - -@mixin outline-success-active { - background: $btn-success-active-bgcolor; - border-color: $btn-success-active-border-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-outline-active-box-shadow; - } - color: $btn-success-active-color; - @if $skin-name == 'Material3' { - background: $btn-outline-success-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-success-color); - } -} - -@mixin outline-info-focus { - @if $skin-name == 'Material3' { - background: $btn-outline-info-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-info-color); - } - @else if $skin-name == 'tailwind3' { - background: $info-bg-color-focus; - border: 1px solid $info-border-color-focus; - color: $info-text-focus; - } -} - -@mixin outline-info-active { - @if ($skin-name == 'bootstrap5.3') { - background: $btn-info-bgcolor; - } - @else { - background: $btn-info-active-bgcolor; - border-color: $btn-info-active-border-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-outline-active-box-shadow; - } - } - color: $btn-info-active-color; - @if $skin-name == 'Material3' { - background: $btn-outline-info-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-info-color); - } -} - -@mixin outline-warning-focus { - @if $skin-name == 'Material3' { - background: $btn-outline-warning-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-warning-color); - } - @else if $skin-name == 'tailwind3' { - background: $warning-bg-color-focus; - border: 1px solid $warning-border-color-focus; - color: $warning-text-focus; - } -} - -@mixin outline-warning-active { - background: $btn-warning-bgcolor; - border-color: transparent; - background: $btn-warning-active-bgcolor; - border-color: $btn-warning-active-border-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-outline-active-box-shadow; - } - color: $btn-warning-active-color; - @if $skin-name == 'Material3' { - background: $btn-outline-warning-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-warning-color); - } -} - -@mixin outline-danger-focus { - @if $skin-name == 'Material3' { - background: $btn-outline-danger-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-danger-color); - } - @else if $skin-name == 'tailwind3' { - background: $danger-bg-color-focus; - border: 1px solid $danger-border-color-focus; - color: $danger-text-focus; - } -} - -@mixin outline-danger-active { - background: $btn-danger-active-bgcolor; - border-color: $btn-danger-active-border-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-outline-active-box-shadow; - } - color: $btn-danger-active-color; - @if $skin-name == 'Material3' { - background: $btn-outline-danger-active-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-danger-color); - } -} - -@mixin outline-disabled { - background: $btn-outline-bgcolor; - @if $skin-name == 'bootstrap5.3' { - border-color: $btn-outline-disabled-border-color; - color: $btn-outline-disabled-color; - } - @else if $skin-name == 'fluent2' { - background: $btn-outline-disabled-bgcolor !important; /* stylelint-disable-line declaration-no-important */ - border-color: $btn-outline-disabled-border-color !important; /* stylelint-disable-line declaration-no-important */ - color: $btn-outline-disabled-color !important; /* stylelint-disable-line declaration-no-important */ - } - @else if $skin-name == 'tailwind3' { - border: 1px solid $secondary-border-color-disabled; - color: $secondary-text-color-disabled; - } - @else { - border-color: $btn-outline-primary-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-outline-primary-disabled-color; - } -} - -@mixin outline-primary-disabled { - @if $skin-name == 'bootstrap5.3' { - background: $btn-outline-primary-disabled-border-color; - } - @else { - background: $btn-outline-bgcolor; - } - border-color: $btn-outline-primary-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-outline-primary-disabled-color; -} - -@mixin outline-success-disabled { - @if $skin-name == 'bootstrap5.3' { - background: $btn-outline-success-disabled-border-color; - } - @else { - background: $btn-outline-bgcolor; - } - border-color: $btn-outline-success-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-outline-success-disabled-color; -} - -@mixin outline-info-disabled { - @if $skin-name == 'bootstrap5.3' { - background: $btn-info-disabled-border-color; - } - @else { - background: $btn-outline-bgcolor; - } - border-color: $btn-outline-info-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-outline-info-disabled-color; -} - -@mixin outline-warning-disabled { - @if $skin-name == 'bootstrap5.3' { - background: $btn-outline-warning-disabled-border-color; - } - @else { - background: $btn-outline-bgcolor; - } - border-color: $btn-outline-warning-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-outline-warning-disabled-color; -} - -@mixin outline-danger-disabled { - @if $skin-name == 'bootstrap5.3' { - background: $btn-outline-danger-disabled-border-color; - } - @else { - background: $btn-outline-bgcolor; - } - border-color: $btn-outline-danger-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-outline-danger-disabled-color; -} - -@mixin success-focus { - background: $btn-success-focus-bgcolor; - border-color: $btn-success-focus-border-color; - color: $btn-success-hover-color; - @if $skin-name == 'Material3' { - box-shadow: $btn-success-focus-box-shadow; - } - @else { - @if $skin-name != 'tailwind3' { - box-shadow: $btn-hover-focus-box-shadow; - } - } -} - -@mixin success-active { - background: $btn-success-active-bgcolor; - border-color: $btn-success-active-border-color; - color: $btn-success-active-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-active-box-shadow; - } -} - -@mixin info-focus { - background: $btn-info-focus-bgcolor; - border-color: $btn-info-focus-border-color; - color: $btn-info-hover-color; - @if $skin-name != 'tailwind3' { - box-shadow: $btn-hover-focus-box-shadow; - } -} - -@mixin info-active { - background: $btn-info-active-bgcolor; - color: $btn-info-active-color; - border-color: $btn-info-active-border-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-active-box-shadow; - } -} - -@mixin warning-focus { - background: $btn-warning-focus-bgcolor; - border-color: $btn-warning-focus-border-color; - color: $btn-warning-hover-color; - @if $skin-name == 'Material3' { - box-shadow: $btn-warning-focus-box-shadow; - } - @else { - @if $skin-name != 'tailwind3' { - box-shadow: $btn-hover-focus-box-shadow; - } - } -} - -@mixin warning-active { - background: $btn-warning-active-bgcolor; - border-color: $btn-warning-active-border-color; - color: $btn-warning-active-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-active-box-shadow; - } -} - -@mixin danger-focus { - background: $btn-danger-focus-bgcolor; - border-color: $btn-danger-focus-border-color; - color: $btn-danger-hover-color; - @if $skin-name == 'Material3' { - box-shadow: $btn-danger-focus-box-shadow; - } - @else { - @if $skin-name != 'tailwind3' { - box-shadow: $btn-hover-focus-box-shadow; - } - } -} - -@mixin danger-active { - background: $btn-danger-active-bgcolor; - border-color: $btn-danger-active-border-color; - color: $btn-danger-active-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-active-box-shadow; - } -} - -@mixin link-focus { - border-radius: 4px; - text-decoration: underline; -} - -@mixin link-hover { - border-radius: 4px; - text-decoration: underline; -} - -@mixin success-disabled { - background: $btn-success-disabled-bgcolor; - @if $skin-name == 'tailwind3' or $skin-name == 'bootstrap5.3' { - border-color: $success-border-color-disabled; - } - @else { - border-color: $btn-disabled-border-color; - } - box-shadow: $btn-flat-box-shadow; - color: $btn-success-disabled-color; -} - -@mixin info-disabled { - background: $btn-info-disabled-bgcolor; - @if ($skin-name == 'tailwind3' or $skin-name == 'bootstrap5.3') { - border-color: $btn-info-disabled-border-color; - } - @else { - border-color: $btn-disabled-border-color; - } - box-shadow: $btn-flat-box-shadow; - color: $btn-info-disabled-color; -} - -@mixin warning-disabled { - background: $btn-warning-disabled-bgcolor; - @if ($skin-name == 'tailwind3' or $skin-name == 'bootstrap5.3') { - border-color: $btn-warning-disabled-border-color; - } - @else { - border-color: $btn-disabled-border-color; - } - box-shadow: $btn-flat-box-shadow; - color: $btn-warning-disabled-color; -} - -@mixin danger-disabled { - background: $btn-danger-disabled-bgcolor; - @if ($skin-name == 'tailwind3' or $skin-name == 'bootstrap5.3') { - border-color: $btn-danger-disabled-border-color; - } - @else { - border-color: $btn-disabled-border-color; - } - box-shadow: $btn-flat-box-shadow; - color: $btn-danger-disabled-color; -} - -@mixin link-disabled { - @if $skin-name == 'bootstrap5.3' { - color: $secondary-border-color-disabled; - text-decoration: underline; - } - @else if $skin-name == 'tailwind3' { - color: $btn-link-disabled-color; - } - @else { - color: $btn-disabled-color; - } - background: $btn-link-disabled-bgcolor; - box-shadow: $btn-flat-box-shadow; - text-decoration: none; -} diff --git a/components/buttons/styles/button/_theme.scss b/components/buttons/styles/button/_theme.scss deleted file mode 100644 index f346532..0000000 --- a/components/buttons/styles/button/_theme.scss +++ /dev/null @@ -1,1687 +0,0 @@ -@use 'sass:color'; -@import 'mixin.scss'; -@include export-module('button-theme') { - - /* stylelint-disable property-no-vendor-prefix */ - .sf-btn, - .sf-css.sf-btn { - -webkit-tap-highlight-color: transparent; - background: $btn-bgcolor; - @if $skin-name == 'Material3' { - border-image: $btn-border-color; - } - @else { - border-color: $btn-border-color; - } - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-box-shadow; - } - color: $btn-color; - transition: box-shadow 280ms cubic-bezier(.4, 0, .2, 1); - @at-root { - &:hover { - background: $btn-hover-bgcolor; - @if $skin-name == 'Material3' { - border-image: $btn-hover-border-color; - } - @else { - border-color: $btn-hover-border-color; - } - box-shadow: $btn-hover-focus-box-shadow; - color: $btn-hover-color; - } - - &:focus { // both keyboard and focus - @include button-focus; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-bgcolor; - color: $btn-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active { - @include button-active; - } - - &.sf-active { - background: $btn-active-bgcolor; - @if $skin-name == 'Material3' { - border-image: $btn-active-border-color; - } - @else { - border-color: $btn-active-border-color; - } - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-active-box-shadow; - } - color: $btn-active-color; - @if $skin-name == 'fluent2' { - background: $secondary-bg-color-selected !important; /* stylelint-disable-line declaration-no-important */ - border-color: $secondary-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - color: $secondary-text-color-selected !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - background: $btn-disabled-bgcolor; - border-color: $btn-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-disabled-color; - } - - &.sf-disabled { - @if $skin-name == 'fluent2' or $skin-name == 'bootstrap5.3' { - opacity: 1; - } - } - - & .sf-ripple-element { - background: $btn-ripple-bgcolor; - } - - &.sf-round, - &.sf-round-edge { - background: $btn-round-bgcolor; - border-color: $btn-round-border-color; - color: $btn-round-color; - - &:hover { - background: $btn-round-hover-bgcolor; - border-color: $btn-round-hover-border-color; - color: $btn-round-hover-color; - } - - &:focus { - background: $btn-focus-bgcolor; - @if $skin-name == 'Material3' { - border-image: $btn-focus-border-color; - } - @else { - border-color: $btn-focus-border-color; - } - box-shadow: $btn-box-shadow; - color: $btn-round-focus-color; - outline: $btn-focus-outline-round; - outline-offset: $btn-focus-outline-offset; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-bgcolor; - color: $btn-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active { - background: $btn-active-bgcolor; - @if $skin-name == 'Material3' { - border-image: $btn-active-border-color; - } - @else { - border-color: $btn-active-border-color; - } - box-shadow: $btn-active-box-shadow; - color: $btn-round-active-color; - outline: $btn-active-outline; - outline-offset: $btn-active-outline-offset; - } - - &:disabled, - &.sf-disabled { - background: $btn-disabled-bgcolor; - border-color: $btn-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-disabled-color; - } - - &.sf-primary, - &.sf-filled { - &:hover { - @if $skin-name != 'fluent2' { - border-color: $btn-primary-bgcolor; - } - } - - &:focus { - outline: $btn-focus-outline-round; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-primary-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-primary-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-primary-bgcolor; - color: $btn-primary-color; - box-shadow: $btn-focus-box-shadow; - } - } - } - - &.sf-success { - &:hover { - @if $skin-name != 'fluent2' { - border-color: $btn-success-bgcolor; - } - } - - &:focus { - outline: $btn-focus-outline-round; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-success-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-success-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-success-bgcolor; - color: $btn-success-color; - box-shadow: $btn-focus-box-shadow; - } - } - } - - &.sf-info { - &:hover { - @if $skin-name != 'fluent2' { - border-color: $btn-info-bgcolor; - } - } - - &:focus { - outline: $btn-focus-outline-round; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-info-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-info-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-info-bgcolor; - color: $btn-info-color; - box-shadow: $btn-focus-box-shadow; - } - } - } - - &.sf-warning { - &:hover { - @if $skin-name != 'fluent2' { - border-color: $btn-warning-bgcolor; - } - } - - &:focus { - outline: $btn-focus-outline-round; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-warning-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-warning-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-warning-bgcolor; - color: $btn-warning-color; - box-shadow: $btn-focus-box-shadow; - } - } - } - - &.sf-danger { - &:hover { - @if $skin-name != 'fluent2' { - border-color: $btn-danger-bgcolor; - } - } - - &:focus { - outline: $btn-focus-outline-round; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-danger-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-danger-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-danger-bgcolor; - color: $btn-danger-color; - box-shadow: $btn-focus-box-shadow; - } - } - } - } - - &.sf-filled { - background: $btn-primary-bgcolor; - border-color: $btn-primary-border-color; - color: $btn-primary-color; - @if $skin-name == 'Material3' { - box-shadow: $btn-focus-box-shadow; - } - - &:hover { - background: $btn-primary-hover-bgcolor; - @if $skin-name != 'Material3' { - border-color: $btn-primary-hover-border-color; - } - @if $skin-name == 'Material3' { - box-shadow: $btn-primary-focus-box-shadow; - } - @else { - box-shadow: $btn-hover-focus-box-shadow; - } - color: $btn-primary-hover-color; - } - - &:focus { - @include primary-focus; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-primary-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - box-shadow: $btn-primary-focus-box-shadow; - border-color: $btn-keyboard-focus-border-color; - background-color: $primary-bg-color-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-primary-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-primary-bgcolor; - color: $btn-primary-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active { - @include primary-active; - } - - &.sf-active { - background: $btn-primary-active-bgcolor; - border-color: $btn-primary-active-border-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-active-box-shadow; - } - color: $btn-primary-active-color; - @if $skin-name == 'fluent2' { - background: $primary-bg-color-selected !important; /* stylelint-disable-line declaration-no-important */ - border-color: $primary-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - color: $primary-text-selected !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include primary-disabled; - } - - & .sf-ripple-element { - background: $btn-ripple-primary-bgcolor; - } - } - - &.sf-success { - background: $btn-success-bgcolor; - border-color: $btn-success-border-color; - color: $btn-success-color; - @if $skin-name == 'Material3' { - box-shadow: $btn-focus-box-shadow; - } - - &:hover { - background: $btn-success-hover-bgcolor; - @if $skin-name != 'Material3' { - border-color: $btn-success-hover-border-color; - } - @if $skin-name == 'Material3' { - box-shadow: $btn-primary-focus-box-shadow; - } - @else { - box-shadow: $btn-hover-focus-box-shadow; - } - color: $btn-success-hover-color; - } - - &:focus { - @include success-focus; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-success-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-success-focus-box-shadow; - background-color: $success-bg-color-focus; - color: $success-text-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-success-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-success-bgcolor; - color: $btn-success-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - @include success-active; - } - - &.sf-active { - @if $skin-name == 'fluent2' { - background: $success-bg-color-selected !important; /* stylelint-disable-line declaration-no-important */ - border-color: $success-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - color: $success-text-pressed !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include success-disabled; - } - - & .sf-ripple-element { - background: $btn-ripple-success-bgcolor; - } - } - - &.sf-info { - background: $btn-info-bgcolor; - border-color: $btn-info-border-color; - color: $btn-info-color; - @if $skin-name == 'Material3' { - box-shadow: $btn-focus-box-shadow; - } - - &:hover { - background: $btn-info-hover-bgcolor; - @if $skin-name != 'Material3' { - border-color: $btn-info-hover-border-color; - } - @if $skin-name == 'Material3' { - box-shadow: $btn-primary-focus-box-shadow; - } - @else { - box-shadow: $btn-hover-focus-box-shadow; - } - color: $btn-info-hover-color; - } - - &:focus { - @include info-focus; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-info-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-info-focus-box-shadow; - background-color: $info-bg-color-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-info-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-info-bgcolor; - color: $btn-info-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - @include info-active; - } - - &.sf-active { - @if $skin-name == 'fluent2' { - background: $info-bg-color-selected !important; /* stylelint-disable-line declaration-no-important */ - border-color: $info-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - color: $btn-info-active-color !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include info-disabled; - } - - & .sf-ripple-element { - background: $btn-ripple-info-bgcolor; - } - } - - &.sf-warning { - background: $btn-warning-bgcolor; - border-color: $btn-warning-border-color; - color: $btn-warning-color; - @if $skin-name == 'Material3' { - box-shadow: $btn-focus-box-shadow; - } - - &:hover { - background: $btn-warning-hover-bgcolor; - @if $skin-name != 'Material3' { - border-color: $btn-warning-hover-border-color; - } - @if $skin-name == 'Material3' { - box-shadow: $btn-primary-focus-box-shadow; - } - @else { - box-shadow: $btn-hover-focus-box-shadow; - } - color: $btn-warning-hover-color; - } - - &:focus { - @include warning-focus; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-warning-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-warning-focus-box-shadow; - background-color: $warning-bg-color-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-warning-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-warning-bgcolor; - color: $btn-warning-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - @include warning-active; - } - - &.sf-active { - @if $skin-name == 'fluent2' { - background: $warning-bg-color-selected !important; /* stylelint-disable-line declaration-no-important */ - border-color: $warning-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - color: $btn-warning-active-color !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include warning-disabled; - } - - & .sf-ripple-element { - background: $btn-ripple-warning-bgcolor; - } - } - - &.sf-danger { - background: $btn-danger-bgcolor; - border-color: $btn-danger-border-color; - color: $btn-danger-color; - @if $skin-name == 'Material3' { - box-shadow: $btn-focus-box-shadow; - } - - &:hover { - background: $btn-danger-hover-bgcolor; - @if $skin-name != 'Material3' { - border-color: $btn-danger-hover-border-color; - } - @if $skin-name == 'Material3' { - box-shadow: $btn-primary-focus-box-shadow; - } - @else { - box-shadow: $btn-hover-focus-box-shadow; - } - color: $btn-danger-hover-color; - } - - &:focus { - @include danger-focus; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-danger-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-danger-focus-box-shadow; - background-color: $danger-bg-color-focus; - color: $danger-text-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-danger-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-danger-bgcolor; - color: $btn-danger-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active { - @include danger-active; - } - - &.sf-active { - background: $btn-danger-active-bgcolor; - border-color: $btn-danger-active-border-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-active-box-shadow; - } - color: $btn-danger-active-color; - @if $skin-name == 'fluent2' { - background: $danger-bg-color-selected !important; /* stylelint-disable-line declaration-no-important */ - border-color: $danger-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - color: $danger-text-pressed !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include danger-disabled; - } - - & .sf-ripple-element { - background: $btn-ripple-danger-bgcolor; - } - } - - &.sf-flat { - background: $btn-flat-bgcolor; - border-color: $btn-flat-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-flat-color; - - &:hover { - background: $btn-flat-hover-bgcolor; - border-color: $btn-flat-hover-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-flat-hover-color; - } - - &:focus { - background: $btn-flat-focus-bgcolor; - border-color: $btn-flat-focus-border-color; - color: $btn-flat-focus-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-box-shadow; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-bgcolor; - color: $btn-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - background: $btn-flat-active-bgcolor; - border-color: $btn-flat-active-border-color; - color: $btn-flat-active-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-active-box-shadow; - } - } - - &:disabled, - &.sf-disabled { - background: $btn-flat-disabled-bgcolor; - border-color: $btn-flat-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-flat-disabled-color; - } - - & .sf-ripple-element { - background: $btn-ripple-flat-bgcolor; - } - - &.sf-primary, - &.sf-filled { - background: $btn-flat-primary-bgcolor; - border-color: $btn-flat-primary-border-color; - color: $btn-flat-primary-color; - - &:hover { - background: $btn-flat-primary-hover-bgcolor; - border-color: $btn-flat-primary-hover-border-color; - color: $btn-flat-primary-hover-color; - } - - &:focus { - background: $btn-flat-primary-focus-bgcolor; - border-color: $btn-flat-primary-focus-border-color; - color: $btn-flat-primary-focus-color; - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-primary-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-primary-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-primary-focus-box-shadow; - background-color: $primary-bg-color-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-primary-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-primary-bgcolor; - color: $btn-primary-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - background: $btn-flat-primary-active-bgcolor; - border-color: $btn-flat-primary-active-border-color; - color: $btn-flat-primary-active-color; - } - - &:disabled, - &.sf-disabled { - background: $btn-flat-primary-disabled-bgcolor; - border-color: $btn-flat-primary-disabled-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-flat-primary-disabled-color; - } - - & .sf-ripple-element { - background: $btn-ripple-flat-primary-bgcolor; - } - } - - &.sf-success { - background: $btn-flat-success-bgcolor; - border-color: $btn-flat-success-border-color; - color: $btn-flat-success-color; - - &:hover { - background: $btn-flat-success-hover-bgcolor; - border-color: $btn-flat-success-hover-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-flat-success-hover-color; - } - - &:focus { - background: $btn-flat-success-focus-bgcolor; - border-color: $btn-flat-success-focus-border-color; - color: $btn-flat-success-focus-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-box-shadow; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-success-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-success-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-success-focus-box-shadow; - background-color: $success-bg-color-focus; - color: $success-text-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-success-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-success-bgcolor; - color: $btn-success-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - background: $btn-flat-success-active-bgcolor; - border-color: $btn-flat-success-active-border-color; - color: $btn-flat-success-active-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-active-box-shadow; - } - } - - &:disabled, - &.sf-disabled { - background: $btn-flat-success-disabled-bgcolor; - border-color: $btn-flat-success-disabled-border-color; - color: $btn-flat-success-disabled-color; - } - - & .sf-ripple-element { - background: $btn-ripple-flat-success-bgcolor; - } - } - - &.sf-info { - background: $btn-flat-info-bgcolor; - border-color: $btn-flat-info-border-color; - color: $btn-flat-info-color; - - &:hover { - background: $btn-flat-info-hover-bgcolor; - border-color: $btn-flat-info-hover-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-flat-info-hover-color; - } - - &:focus { - background: $btn-flat-info-focus-bgcolor; - border-color: $btn-flat-info-focus-border-color; - color: $btn-flat-info-focus-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-box-shadow; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-info-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-info-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-info-focus-box-shadow; - background-color: $info-bg-color-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-info-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-info-bgcolor; - color: $btn-info-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - background: $btn-flat-info-active-bgcolor; - border-color: $btn-flat-info-active-border-color; - color: $btn-flat-info-active-color; - @if $skin-name != 'bootstrap5' and $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-active-box-shadow; - } - } - - &:disabled, - &.sf-disabled { - background: $btn-flat-info-disabled-bgcolor; - border-color: $btn-flat-info-disabled-border-color; - color: $btn-flat-info-disabled-color; - } - - & .sf-ripple-element { - background: $btn-ripple-flat-info-bgcolor; - } - } - - &.sf-warning { - background: $btn-flat-warning-bgcolor; - border-color: $btn-flat-warning-border-color; - color: $btn-flat-warning-color; - - &:hover { - background: $btn-flat-warning-hover-bgcolor; - border-color: $btn-flat-warning-hover-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-flat-warning-hover-color; - } - - &:focus { - background: $btn-flat-warning-focus-bgcolor; - border-color: $btn-flat-warning-focus-border-color; - color: $btn-flat-warning-focus-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-box-shadow; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-warning-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-warning-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-warning-focus-box-shadow; - background-color: $warning-bg-color-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-warning-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-warning-bgcolor; - color: $btn-warning-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - background: $btn-flat-warning-active-bgcolor; - border-color: $btn-flat-warning-active-border-color; - color: $btn-flat-warning-active-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-active-box-shadow; - } - } - - &:disabled, - &.sf-disabled { - background: $btn-flat-warning-disabled-bgcolor; - border-color: $btn-flat-warning-disabled-border-color; - color: $btn-flat-warning-disabled-color; - } - - & .sf-ripple-element { - background: $btn-ripple-flat-warning-bgcolor; - } - } - - &.sf-danger { - background: $btn-flat-danger-bgcolor; - border-color: $btn-flat-danger-border-color; - color: $btn-flat-danger-color; - - &:hover { - background: $btn-flat-danger-hover-bgcolor; - border-color: $btn-flat-danger-hover-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-flat-danger-hover-color; - } - - &:focus { - background: $btn-flat-danger-focus-bgcolor; - border-color: $btn-flat-danger-focus-border-color; - color: $btn-flat-danger-focus-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-box-shadow; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - background: $btn-danger-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-danger-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-danger-focus-box-shadow; - background-color: $danger-bg-color-focus; - color: $danger-text-focus; - } - @if $skin-name == 'bootstrap5.3' { - box-shadow: $btn-danger-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $btn-danger-bgcolor; - color: $btn-danger-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - background: $btn-flat-danger-active-bgcolor; - border-color: $btn-flat-danger-active-border-color; - color: $btn-flat-danger-active-color; - @if $skin-name != 'bootstrap5.3' { - box-shadow: $btn-flat-active-box-shadow; - } - } - - &:disabled, - &.sf-disabled { - background: $btn-flat-danger-disabled-bgcolor; - border-color: $btn-flat-danger-disabled-border-color; - color: $btn-flat-danger-disabled-color; - } - - & .sf-ripple-element { - background: $btn-ripple-flat-danger-bgcolor; - } - } - } - - &.sf-outlined { - background: $btn-outline-bgcolor; - border-color: $btn-outline-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-outline-color; - @if $skin-name == 'Material3' { - border: 1px solid; - } - - &:hover { - background: $btn-outline-hover-bgcolor; - border-color: $btn-outline-hover-border-color; - box-shadow: $btn-flat-box-shadow; - color: $btn-outline-hover-color; - } - - &:focus { - @include outline-focus; - } - - &:focus:not(:focus-visible) { //mouse click - @if $skin-name == 'fluent2' { - background: transparent; - border-color: $secondary-border-color-hover; - color: $secondary-outline-button-text-color-pressed; - } - @if $skin-name == 'bootstrap5.3' { - background: $btn-outline-active-bgcolor; - color: $btn-outline-active-color; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'bootstrap5.3' { - background-color: $secondary-bg-color-focus; - color: $secondary-text-color-focus; - box-shadow: $btn-focus-box-shadow; - } - @if $skin-name == 'Material3' { - background: $btn-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $transparent; - color: $btn-color; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - @include outline-active; - @if $skin-name == 'fluent2' { - background: transparent; - color: $secondary-outline-button-text-color-pressed !important; /* stylelint-disable-line declaration-no-important */ - border-color: $btn-outline-active-border-color !important; /* stylelint-disable-line declaration-no-important */ - outline: 2px solid $secondary-outline-button-text-color-pressed; - } - } - - &.sf-active { - @if $skin-name == 'fluent2' { - background: transparent !important; /* stylelint-disable-line declaration-no-important */ - border-color: $btn-outline-active-border-color !important; /* stylelint-disable-line declaration-no-important */ - outline: 2px solid $secondary-outline-button-text-color-pressed; - } - } - - &:disabled, - &.sf-disabled { - @include outline-disabled; - } - - &.sf-primary, - &.sf-filled { - background: $btn-outline-bgcolor; - @if $skin-name == 'Material3' { - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-primary-color); - } - @else { - border-color: $btn-outline-primary-color; - color: $btn-outline-primary-color; - } - - &:hover { - @if $skin-name == 'Material3' { - background: $btn-outline-primary-hover-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-primary-color); - } - @else if $skin-name == 'fluent2' { - background: $btn-outline-success-hover-bgcolor; - border-color: $btn-outline-primary-hover-border-color; - color: $btn-primary-hover-bgcolor; - } - @else { - background: $btn-primary-hover-bgcolor; - border-color: $btn-outline-primary-hover-border-color; - color: $btn-primary-hover-color; - } - } - - &:focus { - @include outline-primary-focus; - } - - &:focus:not(:focus-visible) { //mouse click - @if $skin-name == 'fluent2' { - background: transparent; - border-color: $primary-border-color-hover; - color: $primary-border-color-hover; - } - - @if $skin-name == 'bootstrap5.3' { - background: $btn-outline-primary-focus-bgcolor; - color: $btn-outline-active-color; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'bootstrap5.3' { - background-color: $primary-bg-color-focus; - color: $primary-text-color; - box-shadow: $btn-primary-focus-box-shadow; - } - @if $skin-name == 'Material3' { - background: $btn-primary-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-primary-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-primary-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $transparent; - color: $btn-primary-bgcolor; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - @include outline-primary-active; - @if $skin-name == 'fluent2' { - background: transparent; - color: $primary-bg-color-pressed !important; /* stylelint-disable-line declaration-no-important */ - border-color: $btn-outline-primary-active-border-color !important; /* stylelint-disable-line declaration-no-important */ - outline: 2px solid $primary-bg-color-pressed; - } - } - - &.sf-active { - @if $skin-name == 'fluent2' { - background: transparent !important; /* stylelint-disable-line declaration-no-important */ - color: $primary-bg-color-pressed !important; /* stylelint-disable-line declaration-no-important */ - border-color: $btn-outline-primary-active-border-color !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include outline-primary-disabled; - } - } - - &.sf-success { - background: $btn-outline-bgcolor; - @if $skin-name == 'bootstrap5.3' { - border-color: $success-outline; - color: $success-outline; - } - @if $skin-name == 'Material3' { - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-success-color); - } - @else { - border-color: $btn-success-bgcolor; - color: $btn-success-bgcolor; - } - - &:hover { - @if $skin-name == 'Material3' { - background: $btn-outline-success-hover-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-success-color); - } - @else if $skin-name == 'fluent2' { - background: $btn-outline-success-hover-bgcolor; - border-color: $btn-success-hover-border-color; - color: $btn-success-hover-bgcolor; - } - @else { - background: $btn-success-hover-bgcolor; - border-color: $btn-success-hover-border-color; - color: $btn-success-color; - } - } - - &:focus { - @include outline-success-focus; - } - - &:focus:not(:focus-visible) { //mouse click - @if $skin-name == 'fluent2' { - background: transparent; - border-color: $success-border-color-hover; - color: $success-border-color-hover; - } - - @if $skin-name == 'bootstrap5.3' { - background: $btn-success-bgcolor; - color: $btn-success-color; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'bootstrap5.3' { - background-color: $success-bg-color-focus; - color: $success-text-focus; - box-shadow: $btn-success-focus-box-shadow; - } - @if $skin-name == 'Material3' { - background: $btn-success-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-success-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-success-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $transparent; - color: $btn-success-bgcolor; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - @include outline-success-active; - @if $skin-name == 'fluent2' { - background: transparent; - color: $success-bg-color-pressed !important; /* stylelint-disable-line declaration-no-important */ - border-color: $btn-success-active-border-color !important; /* stylelint-disable-line declaration-no-important */ - outline: 2px solid $success-bg-color-pressed; - } - } - - &.sf-active { - @if $skin-name == 'fluent2' { - background: transparent !important; /* stylelint-disable-line declaration-no-important */ - border-color: $success-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include outline-success-disabled; - } - } - - &.sf-info { - background: $btn-outline-bgcolor; - @if $skin-name == 'bootstrap5.3' { - border-color: $info-outline; - color: $info-outline; - } - @if $skin-name == 'Material3' { - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-info-color); - } - @else { - border-color: $btn-info-bgcolor; - color: $btn-info-bgcolor; - } - - &:hover { - @if $skin-name == 'Material3' { - background: $btn-outline-info-hover-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-info-color); - } - @else if $skin-name == 'fluent2' { - background: $btn-outline-info-hover-bgcolor; - border-color: $btn-info-hover-border-color; - color: $btn-info-hover-bgcolor; - } - @else { - background: $btn-info-hover-bgcolor; - border-color: $btn-info-hover-border-color; - color: $btn-info-color; - } - } - - &:focus { - @include outline-info-focus; - } - - &:focus:not(:focus-visible) { //mouse click - @if $skin-name == 'fluent2' { - background: transparent; - border-color: $info-bg-color-hover; - color: $info-bg-color-hover; - } - - @if $skin-name == 'bootstrap5.3' { - background: $btn-info-bgcolor; - color: $btn-info-color; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'bootstrap5.3' { - background-color: $info-bg-color-focus; - color: $info-text-focus; - box-shadow: $btn-info-focus-box-shadow; - } - @if $skin-name == 'Material3' { - background: $btn-info-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-info-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-info-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $transparent; - color: $btn-info-bgcolor; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - @include outline-info-active; - @if $skin-name == 'fluent2' { - background: transparent; - color: $info-bg-color-pressed !important; /* stylelint-disable-line declaration-no-important */ - border-color: $btn-info-active-border-color !important; /* stylelint-disable-line declaration-no-important */ - outline: 2px solid $info-bg-color-pressed; - } - } - - &.sf-active { - @if $skin-name == 'fluent2' { - background: transparent !important; /* stylelint-disable-line declaration-no-important */ - border-color: $info-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include outline-info-disabled; - } - } - - &.sf-warning { - background: $btn-outline-bgcolor; - @if $skin-name == 'bootstrap5.3' { - border-color: $warning-outline; - color: $warning-outline; - } - @if $skin-name == 'Material3' { - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-warning-color); - } - @else { - border-color: $btn-warning-bgcolor; - color: $btn-warning-bgcolor; - } - - &:hover { - @if $skin-name == 'Material3' { - background: $btn-outline-warning-hover-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-warning-color); - } - @else if $skin-name == 'fluent2' { - background: $btn-outline-warning-hover-bgcolor; - border-color: $btn-warning-hover-border-color; - color: $btn-warning-hover-bgcolor; - } - @else { - background: $btn-warning-hover-bgcolor; - border-color: $btn-warning-hover-border-color; - color: $btn-warning-color; - } - } - - &:focus { - @include outline-warning-focus; - } - - &:focus:not(:focus-visible) { //mouse click - @if $skin-name == 'fluent2' { - background: transparent; - border-color: $warning-border-color-hover; - color: $warning-border-color-hover; - } - - @if $skin-name == 'bootstrap5.3' { - background: $btn-warning-bgcolor; - color: $btn-warning-color; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'bootstrap5.3' { - background-color: $warning-bg-color-focus; - color: $warning-text-focus; - box-shadow: $btn-warning-focus-box-shadow; - } - @if $skin-name == 'Material3' { - background: $btn-warning-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-warning-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-warning-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $transparent; - color: $btn-warning-bgcolor; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - @include outline-warning-active; - @if $skin-name == 'fluent2' { - background: transparent; - color: $warning-bg-color-pressed !important; /* stylelint-disable-line declaration-no-important */ - border-color: $btn-warning-active-border-color !important; /* stylelint-disable-line declaration-no-important */ - outline: 2px solid $warning-bg-color-pressed; - } - } - - &.sf-active { - @if $skin-name == 'fluent2' { - background: transparent !important; /* stylelint-disable-line declaration-no-important */ - border-color: $warning-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include outline-warning-disabled; - } - } - - &.sf-danger { - background: $btn-outline-bgcolor; - @if $skin-name == 'bootstrap5.3' { - border-color: $danger-outline; - color: $danger-outline; - } - @if $skin-name == 'Material3' { - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-danger-color); - } - @else { - border-color: $btn-danger-bgcolor; - color: $btn-danger-bgcolor; - } - - &:hover { - @if $skin-name == 'Material3' { - background: $btn-outline-danger-hover-bgcolor; - border: 1px solid $btn-outline-border-color; - color: rgba($btn-outline-danger-color); - } - @if $skin-name == 'fluent2' { - background: $btn-outline-danger-hover-bgcolor; - border-color: $btn-danger-hover-border-color; - color: $btn-danger-hover-bgcolor; - } - @else { - background: $btn-danger-hover-bgcolor; - border-color: $btn-danger-hover-border-color; - color: $btn-danger-color; - } - } - - &:focus { - @include outline-danger-focus; - } - - &:focus:not(:focus-visible) { //mouse click - @if $skin-name == 'fluent2' { - background: transparent; - border-color: $danger-border-color-hover; - color: $danger-border-color-hover; - } - - @if $skin-name == 'bootstrap5.3' { - background: $btn-danger-bgcolor; - color: $btn-danger-color; - } - } - - &:focus-visible { // only for keybord - @if $skin-name == 'bootstrap5.3' { - background-color: $danger-bg-color-focus; - color: $danger-text-focus; - box-shadow: $btn-danger-focus-box-shadow; - } - @if $skin-name == 'Material3' { - background: $btn-danger-active-bgcolor; - box-shadow: $btn-keyboard-focus-box-shadow; - color: $btn-danger-color; - } - @if $skin-name == 'fluent2' { - border-color: $btn-keyboard-focus-border-color; - box-shadow: $btn-danger-focus-box-shadow; - } - @if $skin-name == 'tailwind3' { - background: $transparent; - color: $btn-danger-bgcolor; - box-shadow: $btn-focus-box-shadow; - } - } - - &:active, - &.sf-active { - @include outline-danger-active; - @if $skin-name == 'fluent2' { - background: transparent; - color: $danger-bg-color-pressed !important; /* stylelint-disable-line declaration-no-important */ - border-color: $btn-danger-active-border-color !important; /* stylelint-disable-line declaration-no-important */ - outline: 2px solid $danger-bg-color-pressed; - } - } - - &.sf-active { - @if $skin-name == 'fluent2' { - background: transparent !important; /* stylelint-disable-line declaration-no-important */ - border-color: $danger-border-color-selected !important; /* stylelint-disable-line declaration-no-important */ - } - } - - &:disabled, - &.sf-disabled { - @include outline-danger-disabled; - } - } - } - - &.sf-link { - background: $btn-link-bgcolor; - border-color: $btn-link-border-color; - border-radius: 0; - box-shadow: none; - color: $btn-link-color; - - &:hover { - @include link-hover; - } - - &:focus { - @include link-focus; - } - - &:focus:not(:focus-visible) { - outline: none !important; /* stylelint-disable-line declaration-no-important */ - } - - &:focus-visible { // only for keybord - @if $skin-name == 'Material3' { - box-shadow: $btn-keyboard-focus-box-shadow; - } - @if $skin-name == 'fluent2' { - text-decoration-color: $btn-link-focus-underline-color !important; /* stylelint-disable-line declaration-no-important */ - text-decoration: underline; - text-decoration-style: double; - } - } - - &:active, - &.sf-active { - @if $skin-name == 'tailwind3' { - color: $btn-link-active-color; - text-decoration: underline; - } - } - - &:disabled { - @include link-disabled; - } - } - - &.sf-link.sf-filled { - background: $btn-primary-bgcolor; - border-color: $btn-primary-border-color; - color: $btn-primary-color; - @if $skin-name == 'Material3' { - box-shadow: $btn-focus-box-shadow; - } - border-radius: 4px; - } - - &.sf-inherit { - color: inherit; - background: inherit; - border-color: transparent; - box-shadow: none; - - &:hover, - &:focus, - &:active, - &.sf-active { - background: rgba(transparent, .056); - border-color: transparent; - box-shadow: none; - color: inherit; - outline: none; - } - - &:disabled { - background: inherit; - color: inherit; - border-color: transparent; - box-shadow: none; - opacity: .5; - } - } - } - } -} - -@if $skin-name == 'bootstrap5.3' { - .sf-btn:not(.sf-outlined) { - &:disabled, - &.sf-disabled { - border-color: transparent !important; /* stylelint-disable-line declaration-no-important */ - } - } -} diff --git a/components/buttons/styles/button/material3-dark.scss b/components/buttons/styles/button/material3-dark.scss deleted file mode 100644 index c14a867..0000000 --- a/components/buttons/styles/button/material3-dark.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'material3-dark-definition.scss'; -@import 'all.scss'; diff --git a/components/buttons/styles/check-box/_layout.scss b/components/buttons/styles/check-box/_layout.scss deleted file mode 100644 index 3a678f9..0000000 --- a/components/buttons/styles/check-box/_layout.scss +++ /dev/null @@ -1,816 +0,0 @@ -@mixin ripple-container { - bottom: $cbox-ripple-small-size; - height: $cbox-ripple-small-height; - left: $cbox-ripple-small-size; - right: -5px; - top: -7.5px; - width: $cbox-ripple-small-width; -} - -@include export-module('checkbox-layout') { - .sf-checkbox-wrapper, - .sf-css.sf-checkbox-wrapper { - cursor: pointer; - display: inline-block; - line-height: 1; - outline: none; - user-select: none; - @if $skin-name == 'fluent2' { - border-radius: 4px; - border: 1px solid transparent; - line-height: 0; - } - @at-root { - & label { - cursor: pointer; - display: inline-block; - line-height: 0; - margin: 0; - position: relative; - white-space: nowrap; - - .sf-label { - &.sf-bottom { - padding-top: 4px; - padding-left: 0; - margin-left: 0; - font-size: 14px; - text-align: center; - display: block; - margin-top: 8px; - } - - &.sf-top { - padding-bottom: 4px; - padding-left: 0; - margin-left: 0; - font-size: 14px; - text-align: center; - display: block; - margin-bottom: 8px; - } - } - } - - &:hover { - & .sf-ripple-container { - @if $skin-name == 'Material3' { - background: $cbox-ripple-bgcolor; - } - &.sf-ripple-check { - @if $skin-name == 'Material3' { - background: $cbox-checked-ripple-bgcolo; - } - } - } - - & .sf-frame { - background-color: $cbox-hover-bgcolor; - border-color: $cbox-hover-border-color; - - &.sf-check { - background-color: $cbox-checkmark-hover-bgcolor; - border-color: $cbox-checkmark-hover-border-color; - color: $cbox-checkmark-hover-color; - } - - &.sf-stop { - @if $skin-name == 'bootstrap5.3' or $skin-name == 'Material3' { - background-color: $cbox-checkmark-hover-bgcolor; - border-color: $cbox-checkmark-hover-border-color; - } - @else if $skin-name == 'fluent2' { - border-color: $cbox-checkmark-hover-border-color; - } - @else { - color: $cbox-indeterminate-hover-color; - } - @if $skin-name == 'fluent2' { - color: $primary-bg-color-hover; - } - @if $skin-name == 'tailwind3' { - background-color: $cbox-focussed-checkmark-bgcolor; - border-color: $cbox-indeterminate-border-color; - } - } - } - - & .sf-label { - color: $cbox-hover-color; - } - } - - - &:focus, - &.sf-focus { - & .sf-ripple-container { - background-color: $cbox-key-focussed-bgcolor; - - &.sf-ripple-check { - background-color: $cbox-checked-ripple-bgcolo; - } - } - - & .sf-frame, - & .sf-frame.sf-check { - outline: $cbox-focus-outline; - outline-offset: $cbox-focus-outline-offset; - } - - & .sf-frame { - & { - box-shadow: $cbox-focussed-box-shadow; - } - @if $skin-name == 'bootstrap5.3' { - & { - border-color: $border-focus; - } - } - } - @if $skin-name == 'fluent2' { - & { - border-radius: 4px; - box-shadow: $shadow-focus-ring1; - } - } - } - - &:active { - & .sf-frame { - @if $skin-name == 'bootstrap5.3' { - box-shadow: $cbox-focussed-box-shadow; - border-color: $border-focus !important; /* stylelint-disable-line declaration-no-important */ - background-color: $content-bg-color-pressed; - } - @if $skin-name == 'tailwind3' { - box-shadow: $cbox-focussed-box-shadow; - } - } - - & .sf-ripple-element { - background: $cbox-ripple-bgcolor; - } - - & .sf-ripple-check { - & .sf-ripple-element { - background: $cbox-checked-ripple-bgcolo; - } - } - } - - & .sf-ripple-check { - & .sf-ripple-element { - background: $cbox-ripple-bgcolor; - } - } - - & .sf-ripple-container { - border-radius: 50%; - bottom: $cbox-ripple-size; - height: $cbox-ripple-height; - left: $cbox-ripple-size; - pointer-events: none; - position: absolute; - right: $cbox-ripple-size; - top: $cbox-ripple-size; - width: $cbox-ripple-width; - z-index: 1; - - & .sf-ripple-element { - @if $skin-name == 'Material3' { - border-radius: 50%; - } - } - } - - & .sf-ripple-element { - background: $cbox-checked-ripple-bgcolo; - } - - & .sf-label { - color: $cbox-color; - cursor: pointer; - display: inline-block; - font-family: $font-family; - font-size: $cbox-font-size; - font-weight: normal; - line-height: $cbox-height; - user-select: none; - vertical-align: middle; - white-space: normal; - @if $skin-name == 'tailwind3' { - font-weight: $font-weight-medium; - } - } - - & .sf-checkbox { - height: 1px; - opacity: 0; - position: absolute; - width: 1px; - - +.sf-label { - @if $skin-name == 'fluent2' { - margin: 6px 4px 6px 8px; - } - @else { - margin-right: $cbox-margin; - } - } - } - - & .sf-checkbox { - &:focus, - &:active { - +.sf-frame { - background-color: $cbox-focussed-bgcolor; - border-color: $cbox-focussed-border-color; - box-shadow: $cbox-focussed-box-shadow; - - &.sf-check { - background-color: $cbox-focussed-checkmark-bgcolor; - border-color: $cbox-focussed-checkmark-border-color; - box-shadow: $cbox-focussed-box-shadow; - color: $cbox-focussed-checkmark-color; - } - - &.sf-stop { - background-color: $cbox-focussed-checkmark-bgcolor; - border-color: $cbox-focussed-checkmark-border-color; - box-shadow: $cbox-focussed-box-shadow; - color: $cbox-indeterminate-hover-color; - } - } - } - } - - & .sf-frame { - background-color: $cbox-bgcolor; - border-color: $cbox-border-color; - border: $cbox-border; - border-radius: $cbox-border-radius; - box-sizing: border-box; - cursor: pointer; - display: inline-block; - font-family: 'e-icons'; - height: $cbox-height; - padding: $cbox-padding; - text-align: center; - vertical-align: middle; - width: $cbox-width; - @if $skin-name == 'fluent2' { - margin: 7.2px 8px; - } - - +.sf-label { - @if $skin-name == 'fluent2' { - margin: 6px 8px 6px 4px; - } - @else { - margin-left: $cbox-margin; - } - } - - +.sf-ripple-container { - left: auto; - } - - &.sf-check { - background-color: $cbox-checkmark-bgcolor; - border-color: $cbox-checkmark-border-color; - color: $cbox-checkmark-color; - } - - &.sf-stop { - background-color: $cbox-indeterminate-bgcolor; - border-color: $cbox-indeterminate-border-color; - color: $cbox-indeterminate-color; - } - } - - & .sf-check { - font-size: $cbox-check-fontsize; - } - - & .sf-stop { - font-size: $cbox-indeterminate-fontsize; - } - - &.sf-checkbox-disabled { - cursor: default; - pointer-events: none; - @if $skin-name == 'bootstrap5.3' { - opacity: .5; - } - - & .sf-frame { - cursor: default; - background-color: $cbox-disabled-bgcolor; - @if $skin-name == 'Material3' { - border: $cbox-border $cbox-disabled-border-color; - } - @else { - border-color: $cbox-disabled-border-color; - } - color: $cbox-disabled-color; - - &.sf-check { - background-color: $cbox-checkmark-disabled-bgcolor; - @if $skin-name == 'Material3' { - border: none; - border-color: $cbox-checkmark-disabled-border-color; - line-height: 14px; - } - @else { - border-color: $cbox-checkmark-disabled-border-color; - } - color: $cbox-checkmark-disabled-color; - } - - &.sf-stop { - background-color: $cbox-indeterminate-disabled-bgcolor; - @if $skin-name == 'Material3' { - border: none; - border: $cbox-indeterminate-disabled-border-color; - line-height: 14px; - } - @else { - border-color: $cbox-indeterminate-disabled-border-color; - } - color: $cbox-indeterminate-disabled-color; - } - } - - & .sf-label { - color: $cbox-disabled-color; - cursor: default; - } - } - - &.sf-rtl { - .sf-ripple-container { - right: $cbox-ripple-size; - } - - & .sf-frame { - @if $skin-name != 'fluent2' { - margin: 0; - } - - &:hover { - background-color: $cbox-hover-bgcolor; - border-color: $cbox-hover-border-color; - } - - + .sf-ripple-container { - left: $cbox-ripple-size; - right: auto; - } - } - - & .sf-label { - @if $skin-name == 'fluent2' { - margin: 6px 4px 6px 8px; - } - @else { - margin-left: 0; - margin-right: $cbox-margin; - } - - +.sf-frame { - @if $skin-name != 'fluent2' { - margin: 0; - } - } - } - - & .sf-checkbox { - +.sf-label { - @if $skin-name == 'fluent2' { - margin: 6px 4px 6px 8px; - } - @else { - margin-left: $cbox-margin; - margin-right: 0; - } - } - } - } - - &.sf-small { - & .sf-frame { - height: $cbox-small-height; - line-height: $cbox-small-lineheight; - width: $cbox-small-width; - } - - & .sf-check { - font-size: $cbox-small-check-fontsize; - } - - & .sf-stop { - font-size: $cbox-small-indeterminate-fontsize; - line-height: $cbox-small-indeterminate-lineheight; - } - - & .sf-label { - font-size: $cbox-small-font-size; - line-height: $cbox-small-height; - } - - & .sf-ripple-container { - @include ripple-container; - } - } - - &:hover .sf-checkbox:focus + .sf-frame.sf-check { - @if $theme-name == 'fluent2-highcontrast' { - background-color: $primary-border-color-hover !important; /* stylelint-disable-line declaration-no-important */ - border-color: $primary-border-color-hover !important; /* stylelint-disable-line declaration-no-important */ - } - } - } - } - - - - .sf-css.sf-checkbox-wrapper { - & .sf-ripple-container { - right: $cbox-ripple-size; - top: -4.5px; - } - - &:hover { - & .sf-ripple-container { - @if $skin-name == 'Material3' { - background: transparent; - } - } - - & .sf-ripple-container.sf-ripple-check { - @if $skin-name == 'Material3' { - background: transparent; - } - } - } - } - - .sf-checkbox-wrapper[readonly] { - pointer-events: none; - } - - .sf-small .sf-checkbox-wrapper, - .sf-small.sf-checkbox-wrapper, - .sf-small .sf-css.sf-checkbox-wrapper, - .sf-small.sf-css.sf-checkbox-wrapper { - & .sf-frame { - height: $cbox-small-height; - line-height: $cbox-small-lineheight; - width: $cbox-small-width; - @if $skin-name == 'fluent2' { - margin: 6.2px 8px; - } - } - - & .sf-check { - font-size: $cbox-small-check-fontsize; - } - - & .sf-stop { - font-size: $cbox-small-indeterminate-fontsize; - line-height: $cbox-small-indeterminate-lineheight; - } - - & .sf-label { - font-size: $cbox-small-font-size; - line-height: $cbox-small-height; - } - - & .sf-ripple-container { - @include ripple-container; - } - } - - .sf-large.sf-small .sf-checkbox-wrapper, - .sf-large.sf-small.sf-checkbox-wrapper, - .sf-large.sf-small .sf-css.sf-checkbox-wrapper, - .sf-large.sf-small.sf-css.sf-checkbox-wrapper { - & .sf-frame { - height: $cbox-bigger-small-height; - line-height: $cbox-bigger-small-lineheight; - width: $cbox-bigger-small-width; - @if $skin-name == 'fluent2' { - margin: 8.2px 8px; - +label { - margin: 7.2px 8px 7.2px 4px; - } - } - - &:hover { - @if $skin-name != 'FluentUI' { - &.sf-check { - background-color: $cbox-checkmark-hover-bgcolor; - border-color: $cbox-checkmark-hover-border-color; - } - } - @if $skin-name == 'FluentUI' { - &:not(.sf-check), - &:not(.sf-stop)::before { - content: $cbox-check-content; - font-size: $cbox-bigger-small-check-fontsize; - } - } - } - } - - & .sf-check { - font-size: $cbox-bigger-small-check-fontsize; - } - - & .sf-stop { - font-size: $cbox-bigger-small-indeterminate-fontsize; - line-height: $cbox-bigger-small-indeterminate-lineheight; - } - - & .sf-label { - font-size: $cbox-bigger-small-font-size; - line-height: $cbox-bigger-small-height; - } - - & .sf-ripple-container { - bottom: $cbox-ripple-bigger-small-size; - height: $cbox-ripple-bigger-small-height; - left: $cbox-ripple-bigger-small-size; - right: $cbox-ripple-bigger-small-size; - top: $cbox-ripple-bigger-small-size; - width: $cbox-ripple-bigger-small-width; - } - } - - .sf-large .sf-checkbox-wrapper, - .sf-large.sf-checkbox-wrapper, - .sf-large .sf-css.sf-checkbox-wrapper, - .sf-large.sf-css.sf-checkbox-wrapper { - & .sf-checkbox { - +.label { - @if $skin-name == 'fluent2' { - margin: 9px 6px 9px 8px; - } - } - } - & .sf-frame { - height: $cbox-bigger-height; - line-height: $cbox-bigger-lineheight; - width: $cbox-bigger-width; - @if $skin-name == 'fluent2' { - margin: 9.2px 8px; - } - - &:hover { - @if $skin-name == 'FluentUI' { - &:not(.sf-check), - &:not(.sf-stop)::before { - content: $cbox-check-content; - font-size: $cbox-bigger-check-fontsize; - } - } - } - - +.sf-label { - font-size: $cbox-bigger-font-size; - line-height: $cbox-bigger-height; - @if $skin-name == 'fluent2' { - margin: 9px 8px 9px 6px; - } - @else { - margin-left: $cbox-bigger-margin; - } - } - - +.sf-ripple-container { - left: auto; - } - } - - & .sf-check { - font-size: $cbox-bigger-check-fontsize; - } - - & .sf-stop { - font-size: $cbox-bigger-indeterminate-fontsize; - line-height: $cbox-bigger-indeterminate-lineheight; - } - - & .sf-label { - font-size: $cbox-bigger-font-size; - } - - & .sf-ripple-container { - bottom: $cbox-ripple-bigger-size; - height: $cbox-ripple-bigger-height; - left: $cbox-ripple-bigger-size; - right: $cbox-ripple-bigger-size; - top: $cbox-ripple-bigger-size; - width: $cbox-ripple-bigger-width; - } - - &.sf-rtl { - & .sf-frame { - @if $skin-name != 'fluent2' { - margin: 0; - } - - &:hover { - @if $skin-name != 'FluentUI' { - &.sf-check { - background-color: $cbox-checkmark-hover-bgcolor; - border-color: $cbox-checkmark-hover-border-color; - } - } - @if $skin-name == 'FluentUI' { - &:not(.sf-check), - &:not(.sf-stop)::before { - content: $cbox-check-content; - font-size: $cbox-bigger-check-fontsize; - } - } - } - - +.sf-label { - @if $skin-name == 'fluent2' { - margin: 9px 6px 9px 8px; - } - @else { - margin-left: 0; - margin-right: $cbox-bigger-margin; - } - } - - +.sf-ripple-container { - left: auto; - } - - +.sf-ripple-container { - right: auto; - } - } - } - - &.sf-small { - & .sf-frame { - height: $cbox-bigger-small-height; - line-height: $cbox-bigger-small-lineheight; - width: $cbox-bigger-small-width; - } - - & .sf-check { - font-size: $cbox-bigger-small-check-fontsize; - } - - & .sf-stop { - font-size: $cbox-bigger-small-indeterminate-fontsize; - line-height: $cbox-bigger-small-indeterminate-lineheight; - } - - & .sf-label { - font-size: $cbox-bigger-small-font-size; - line-height: $cbox-bigger-small-height; - } - - & .sf-ripple-container { - bottom: $cbox-ripple-bigger-small-size; - height: $cbox-ripple-bigger-small-height; - left: $cbox-ripple-bigger-small-size; - right: $cbox-ripple-bigger-small-size; - top: $cbox-ripple-bigger-small-size; - width: $cbox-ripple-bigger-small-width; - } - } - } - - .sf-large .sf-checkbox-wrapper, - .sf-large.sf-checkbox-wrapper, - .sf-large .sf-css.sf-checkbox-wrapper, - .sf-large.sf-css.sf-checkbox-wrapper { - &:hover { - & .sf-frame { - @if $skin-name == 'FluentUI' { - font-size: $cbox-bigger-check-fontsize; - - &:not(.sf-check):not(.sf-stop) { - color: $cbox-icon-color; - @media (max-width: 768px) { - color: $cbox-bgcolor; - } - } - } - } - } - - &.sf-checkbox-disabled { - & .sf-frame { - &.sf-check, - &.sf-stop { - @if $skin-name == 'Material3' { - line-height: 18px; - } - } - } - } - } - - .sf-large.sf-small .sf-checkbox-wrapper, - .sf-large.sf-small.sf-checkbox-wrapper, - .sf-large.sf-small .sf-css.sf-checkbox-wrapper, - .sf-large.sf-small.sf-css.sf-checkbox-wrapper { - &:hover { - & .sf-frame { - @if $skin-name == 'FluentUI' { - font-size: $cbox-bigger-small-check-fontsize; - - &:not(.sf-check):not(.sf-stop) { - color: $cbox-icon-color; - } - } - @if $skin-name == 'bootstrap4' or $skin-name == 'tailwind' { - background-color: $cbox-checkmark-hover-bgcolor; - border-color: $cbox-checkmark-hover-border-color; - } - } - } - } - - .sf-small .sf-label.sf-bottom { - font-size: 12px; - margin-top: 6px; - margin-left: 0; - } - - .sf-small .sf-label.sf-top { - font-size: 12px; - margin-bottom: 6px; - margin-left: 0; - } - - .sf-large .sf-label.sf-bottom { - font-size: 16px; - margin-top: 10px; - margin-left: 0; - } - - .sf-large .sf-label.sf-top { - font-size: 16px; - margin-top: 10px; - margin-left: 0; - } - - .sf-checkbox-wrapper.sf-primary .sf-frame.sf-check, - .sf-checkbox-wrapper.sf-primary:hover .sf-frame.sf-check { - background-color: #e03872; - border-color: transparent; - } - - .sf-checkbox-wrapper.sf-success .sf-frame.sf-check, - .sf-checkbox-wrapper.sf-success .sf-checkbox:focus+.sf-frame.sf-check { - background-color: #689f38; - border-color: transparent; - } - - .sf-checkbox-wrapper.sf-success:hover .sf-frame.sf-check { - background-color: #449d44; - border-color: transparent; - } - - .sf-checkbox-wrapper.sf-info .sf-frame.sf-check, - .sf-checkbox-wrapper.sf-info .sf-checkbox:focus+.sf-frame.sf-check { - background-color: #2196f3; - border-color: transparent; - } - - .sf-checkbox-wrapper.sf-info:hover .sf-frame.sf-check { - background-color: #0b7dda; - border-color: transparent; - } - - .sf-checkbox-wrapper.sf-warning .sf-frame.sf-check, - .sf-checkbox-wrapper.sf-warning .sf-checkbox:focus+.sf-frame.sf-check { - background-color: #ef6c00; - border-color: transparent; - } - - .sf-checkbox-wrapper.sf-warning:hover .sf-frame.sf-check { - background-color: #cc5c00; - border-color: transparent; - } - - .sf-checkbox-wrapper.sf-danger .sf-frame.sf-check, - .sf-checkbox-wrapper.sf-danger .sf-checkbox:focus+.sf-frame.sf-check { - background-color: #d84315; - border-color: transparent; - } - - .sf-checkbox-wrapper.sf-danger:hover .sf-frame.sf-check { - border-color: transparent; - background-color: #ba3912; - } -} diff --git a/components/buttons/styles/check-box/_material3-dark-definition.scss b/components/buttons/styles/check-box/_material3-dark-definition.scss deleted file mode 100644 index 356e259..0000000 --- a/components/buttons/styles/check-box/_material3-dark-definition.scss +++ /dev/null @@ -1 +0,0 @@ -@import './material3-definition.scss'; diff --git a/components/buttons/styles/check-box/_material3-definition.scss b/components/buttons/styles/check-box/_material3-definition.scss deleted file mode 100644 index d3a55b6..0000000 --- a/components/buttons/styles/check-box/_material3-definition.scss +++ /dev/null @@ -1,83 +0,0 @@ -$cbox-bigger-check-fontsize: 14px !default; -$cbox-bigger-font-size: 14px !default; -$cbox-bigger-height: $text-lg !default; -$cbox-bigger-indeterminate-fontsize: 14px !default; -$cbox-bigger-indeterminate-lineheight: 14px !default; -$cbox-bigger-lineheight: 14px !default; -$cbox-bigger-margin: 12px !default; -$cbox-bigger-small-check-fontsize: 12px !default; -$cbox-bigger-small-font-size: 15px !default; -$cbox-bigger-small-height: 18px !default; -$cbox-bigger-small-indeterminate-fontsize: 10px !default; -$cbox-bigger-small-indeterminate-lineheight: 12px !default; -$cbox-bigger-small-lineheight: 12px !default; -$cbox-bigger-small-width: 18px !default; -$cbox-bigger-width: $text-lg !default; -$cbox-border: 2px solid !default; -$cbox-font-size: 14px !default; -$cbox-height: 16px !default; -$cbox-border-radius: 2px !default; -$cbox-check-fontsize: 10px !default; -$cbox-indeterminate-fontsize: 10px !default; -$cbox-indeterminate-lineheight: 11px !default; -$cbox-lineheight: 11px !default; -$cbox-margin: 8px !default; -$cbox-padding: 0 !default; -$cbox-ripple-size: -9.5px !default; -$cbox-ripple-height: 34px !default; -$cbox-ripple-width: 34px !default; -$cbox-ripple-small-size: -6.5px !default; -$cbox-ripple-small-height: 28px !default; -$cbox-ripple-small-width: 28px !default; -$cbox-ripple-bigger-size: -11px !default; -$cbox-ripple-bigger-height: 40px !default; -$cbox-ripple-bigger-width: 40px !default; -$cbox-ripple-bigger-small-size: -10px !default; -$cbox-ripple-bigger-small-height: 36px !default; -$cbox-ripple-bigger-small-width: 36px !default; -$cbox-small-check-fontsize: 8px !default; -$cbox-small-font-size: 10px !default; -$cbox-small-height: 14px !default; -$cbox-small-indeterminate-fontsize: 8px !default; -$cbox-small-indeterminate-lineheight: 11px !default; -$cbox-small-lineheight: 11px !default; -$cbox-small-width: 14px !default; -$cbox-width: 16px !default; -$cbox-focus-outline-offset: 0 !default; -$cbox-focus-outline: rgba($primary, .25) !default; -$cbox-border-color: rgba($content-text-color) !default; -$cbox-bgcolor: $transparent !default; -$cbox-checkmark-bgcolor: rgba($primary) !default; -$cbox-checkmark-border-color: rgba($primary) !default; -$cbox-checkmark-color: rgba($primary-text-color) !default; -$cbox-checked-ripple-bgcolo: rgba($primary, .08) !default; -$cbox-checkmark-disabled-bgcolor: rgba($content-text-color, .38) !default; -$cbox-checkmark-disabled-border-color: rgba($content-text-color, .38) !default; -$cbox-checkmark-disabled-color: rgba($primary-text-color) !default; -$cbox-checkmark-hover-bgcolor: rgba($primary) !default; -$cbox-checkmark-hover-border-color: rgba($primary) !default; -$cbox-checkmark-hover-color: rgba($primary-text-color) !default; -$cbox-color: rgba($content-text-color) !default; -$cbox-disabled-bgcolor: $transparent !default; -$cbox-disabled-border-color: rgba($content-text-color, .38) !default; -$cbox-disabled-color: $content-text-color-disabled !default; -$cbox-focussed-box-shadow: none !default; -$cbox-hover-bgcolor: $transparent !default; -$cbox-hover-border-color: rgba($content-text-color) !default; -$cbox-hover-color: rgba($content-text-color) !default; -$cbox-indeterminate-bgcolor: rgba($primary) !default; -$cbox-indeterminate-border-color: rgba($primary) !default; -$cbox-indeterminate-color: rgba($primary-text-color) !default; -$cbox-indeterminate-content: '\e7d6' !default; -$cbox-indeterminate-disabled-bgcolor: rgba($content-text-color, .38) !default; -$cbox-indeterminate-disabled-border-color: rgba($content-text-color, .38) !default; -$cbox-indeterminate-disabled-color: rgba($primary-text-color) !default; -$cbox-indeterminate-hover-color: rgba($primary-text-color) !default; -$cbox-key-focussed-bgcolor: rgba($content-text-color, .12) !default; -$cbox-ripple-bgcolor: rgba($content-text-color, .08) !default; -$cbox-focussed-bgcolor: $cbox-hover-bgcolor !default; -$cbox-focussed-border-color: $primary-border-color-pressed !default; -$cbox-focussed-checkmark-bgcolor: rgba($primary-bg-color) !default; -$cbox-focussed-checkmark-border-color: rgba($primary-bg-color) !default; -$cbox-focussed-checkmark-color: rgba($primary-text-color) !default; -$cbox-border-style: solid !default; diff --git a/components/buttons/styles/check-box/material3-dark.scss b/components/buttons/styles/check-box/material3-dark.scss deleted file mode 100644 index c14a867..0000000 --- a/components/buttons/styles/check-box/material3-dark.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'material3-dark-definition.scss'; -@import 'all.scss'; diff --git a/components/buttons/styles/chips/_layout.scss b/components/buttons/styles/chips/_layout.scss deleted file mode 100644 index 30e1e87..0000000 --- a/components/buttons/styles/chips/_layout.scss +++ /dev/null @@ -1,192 +0,0 @@ -@mixin align-chip($border-radius, $font-size, $margin, $line-height) { - border-radius: $border-radius; - font-size: $font-size; - margin: $margin; - align-items: center; - justify-content: center; - line-height: $line-height; -} - -@mixin icon-style($height, $width) { - background-size: cover; - display: flex; - overflow: hidden; - height: $height; - width: $width; -} - -@include export-module('chip-layout') { - .sf-chip-list { - display: flex; - padding: $chip-list-padding; - - &.sf-chip, - .sf-chip { - -webkit-tap-highlight-color: transparent; - border: $chip-border-size solid; - @include align-chip($chip-border-radius, $chip-font-size, $chip-margin, $chip-line-height); - box-shadow: $chip-box-shadow; - box-sizing: border-box; - cursor: pointer; - display: inline-flex; - font-weight: $chip-font-weight; - height: $chip-height; - outline: none; - overflow: hidden; - padding: $chip-padding; - position: relative; - transition: box-shadow 300ms cubic-bezier(.4, 0, .2, 1); - user-select: none; - - .sf-chip-avatar { - @include icon-style($chip-avatar-size, $chip-avatar-size); - @include align-chip($chip-avatar-border-radius, $chip-avatar-content-font-size, $chip-avatar-margin, $chip-icon-line-height); - } - - .sf-chip-avatar-wrap, - &.sf-chip-avatar-wrap { - border-radius: $chip-avatar-wrapper-border-radius; - } - - .sf-chip-icon { - @include icon-style($chip-leading-icon-size, $chip-leading-icon-size); - @include align-chip($chip-leading-icon-border-radius, $chip-leading-icon-font-size, $chip-leading-icon-margin, $chip-icon-line-height); - } - - .sf-chip-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .sf-chip-delete { - @include icon-style($chip-delete-icon-size, $chip-delete-icon-width); - @include align-chip($chip-delete-icon-border-radius, $chip-delete-icon-font-size, $chip-delete-icon-margin, $chip-icon-line-height); - } - - .sf-image-url { - @include icon-style($chip-leading-icon-size, $chip-leading-icon-size); - @include align-chip($chip-leading-icon-border-radius, $chip-leading-icon-font-size, $chip-leading-icon-margin, $chip-icon-line-height); - - .sf-leading-image { - width: 100%; - height: 100%; - object-fit: cover; - } - } - - .sf-trailing-icon-url { - @include icon-style($chip-delete-icon-size, $chip-delete-icon-size); - @include align-chip($chip-delete-icon-border-radius, $chip-delete-icon-font-size, $chip-delete-icon-margin, $chip-icon-line-height); - - .sf-trailing-image { - width: 100%; - height: 100%; - object-fit: cover; - } - } - - .sf-chip-template { - display: inline-flex; - } - } - - &:not(.sf-chip) { - flex-wrap: wrap; - } - - &.sf-multi-selection .sf-chip { - & .sf-selectable-icon { - align-items: center; - justify-content: center; - line-height: 1; - display: flex; - height: $chip-leading-icon-size; - width: $chip-leading-icon-size; - overflow: hidden; - transition: width 300ms cubic-bezier(.4, 0, .2, 1), margin 300ms cubic-bezier(.4, 0, .2, 1); - } - - &:not(.sf-chip-icon-wrap):not(.sf-chip-avatar-wrap) { - & .sf-selectable-icon { - width: 0; - } - } - - &.sf-chip-icon-wrap, - &.sf-chip-avatar-wrap { - & .sf-selectable-icon { - display: none; - } - } - - &.sf-chip-avatar-wrap { - & .sf-selectable-icon { - height: $chip-avatar-size; - width: $chip-avatar-size; - margin: $chip-avatar-margin; - margin-top: $chip-multi-selection-icon-margin-top; - } - } - - &.sf-active { - & .sf-chip-icon, - & .sf-chip-avatar { - display: none; - } - - & .sf-selectable-icon { - margin: $chip-multi-selection-icon-margin; - } - - &.sf-chip-icon-wrap, - &.sf-chip-avatar-wrap { - & .sf-selectable-icon { - display: flex; - } - } - - &:not(.sf-chip-icon-wrap):not(.sf-chip-avatar-wrap) { - & .sf-selectable-icon { - width: $chip-leading-icon-size; - } - } - } - } - - &.sf-rtl { - &.sf-chip, - & .sf-chip { - .sf-chip-avatar { - margin: $chip-rtl-avatar-margin; - } - - .sf-chip-icon { - margin: $chip-rtl-leading-icon-margin; - } - - .sf-chip-delete { - margin: $chip-rtl-delete-icon-margin; - } - - .sf-trailing-icon-url { - margin: $chip-rtl-delete-icon-margin; - } - } - - &.sf-multi-selection .sf-chip { - & .sf-selectable-icon { - margin: $chip-rtl-leading-icon-margin; - margin-top: $chip-multi-selection-icon-margin-top; - } - - &.sf-chip-avatar-wrap { - & .sf-selectable-icon { - margin: $chip-rtl-avatar-margin; - margin-top: $chip-multi-selection-icon-margin-top; - } - } - } - } - } -} diff --git a/components/buttons/styles/chips/_material3-dark-definition.scss b/components/buttons/styles/chips/_material3-dark-definition.scss deleted file mode 100644 index 356e259..0000000 --- a/components/buttons/styles/chips/_material3-dark-definition.scss +++ /dev/null @@ -1 +0,0 @@ -@import './material3-definition.scss'; diff --git a/components/buttons/styles/chips/_material3-definition.scss b/components/buttons/styles/chips/_material3-definition.scss deleted file mode 100644 index df53974..0000000 --- a/components/buttons/styles/chips/_material3-definition.scss +++ /dev/null @@ -1,564 +0,0 @@ -$chip-list-padding: 0 !default; -$chip-height: 24px !default; -$chip-border-radius: 12px !default; -$chip-box-shadow: none !default; -$chip-padding: 0 8px !default; -$chip-margin: 4px !default; -$chip-border-size: 1px !default; - -$chip-font-size: 11px !default; -$chip-line-height: 14px !default; -$chip-font-weight: 500 !default; - -$chip-avatar-wrapper-border-radius: 12px !default; -$chip-avatar-border-radius: 12px !default; -$chip-avatar-size: 20px !default; -$chip-avatar-margin: 0 4px 0 -6px !default; -$chip-avatar-content-font-size: $text-xs !default; -$chip-rtl-avatar-wrapper-border-radius: 12px !default; -$chip-rtl-avatar-margin: 0 -6px 0 4px !default; - -$chip-leading-icon-border-radius: 50% !default; -$chip-delete-icon-border-radius: 50% !default; -$chip-leading-icon-font-size: 14px !default; -$chip-leading-icon-size: 20px !default; -$chip-leading-icon-margin: 0 4px 0 -6px !default; -$chip-rtl-leading-icon-margin: 0 -6px 0 4px !default; -$chip-multi-selection-icon-margin: 0 4px 0 -6px !default; -$chip-multi-selection-icon-margin-top: 0 !default; -$chip-delete-icon-font-size: 14px !default; -$chip-delete-icon-size: 18px !default; -$chip-delete-icon-margin: 0 -2px 0 8px !default; -$chip-rtl-delete-icon-margin: 0 8px 0 -2px !default; -$chip-delete-icon-width: $chip-delete-icon-font-size !default; -$chip-icon-line-height: 1 !default; - -$chip-default-bg-color: $secondary-border-color !default; -$chip-hover-bg-color: $content-bg-color-hover !default; -$chip-focus-bg-color: $content-bg-color-pressed !default; -$chip-active-bg-color: $content-bg-color-pressed !default; -$chip-focus-active-bg-color: $secondary-border-color-pressed !default; -$chip-pressed-bg-color: $content-bg-color-pressed !default; -$chip-disabled-bg-color: $secondary-border-color-disabled !default; -$chip-choice-active-bg-color: rgba($primary) !default; -$chip-choice-focus-active-bg-color: rgba($primary) !default; -$chip-pressed-active-bg-color: $primary-bg-color-pressed!default; - -$chip-avatar-bg-color: rgba($primary-light) !default; -$chip-avatar-hover-bg-color: $secondary-bg-color !default; -$chip-avatar-focus-bg-color: $secondary-bg-color !default; -$chip-avatar-active-bg-color: $secondary-bg-color !default; -$chip-avatar-focus-active-bg-color: $content-bg-color-alt4 !default; -$chip-avatar-pressed-bg-color: $secondary-bg-color !default; -$chip-avatar-pressed-active-bg-color: $secondary-bg-color !default; -$chip-avatar-disabled-bg-color: $chip-disabled-bg-color !default; -$chip-avatar-choice-active-bg-color: rgba($primary) !default; -$chip-avatar-choice-focus-active-bg-color: $chip-choice-focus-active-bg-color !default; - -$chip-default-font-color: rgba($content-text-color-alt1) !default; -$chip-hover-font-color: rgba($content-text-color-alt1) !default; -$chip-focus-font-color: rgba($secondary-text-color) !default; -$chip-active-font-color: rgba($secondary-text-color-pressed) !default; -$chip-focus-active-font-color: rgba($secondary-text-color-hover) !default; -$chip-pressed-font-color: rgba($secondary-text-color-pressed) !default; -$chip-selection-pressed-font-color: rgba($primary-text) !default; -$chip-disabled-font-color: $secondary-text-color-disabled !default; -$chip-choice-active-font-color: rgba($primary-text) !default; - -$chip-close-icon-font-color: rgba($secondary-text-color) !default; -$chip-close-icon-hover-font-color: rgba($secondary-text-color-hover) !default; -$chip-close-icon-pressed-font-color: rgba($secondary-text-color-pressed) !default; -$chip-icon-font-color: rgba($secondary-text-color) !default; -$chip-icon-hover-font-color: rgba($secondary-text-color-hover) !default; -$chip-icon-focus-font-color: rgba($secondary-text-color-hover) !default; -$chip-icon-active-font-color: rgba($icon-color) !default; -$chip-icon-focus-active-font-color: rgba($secondary-text-color-pressed) !default; -$chip-icon-pressed-font-color: rgba($secondary-text-color-pressed) !default; -$chip-icon-selection-pressed-font-color: rgba($primary-text-color) !default; -$chip-choice-icon-focus-active-font-color: rgba($primary-text-color) !default; - -$chip-avatar-font-color: rgba($secondary-text-color) !default; -$chip-avatar-hover-font-color: rgba($secondary-text-color) !default; -$chip-avatar-focus-font-color: rgba($secondary-text-color) !default; -$chip-avatar-active-font-color: rgba($secondary-text-color) !default; -$chip-avatar-focus-active-font-color: $chip-icon-focus-active-font-color !default; -$chip-avatar-pressed-font-color: rgba($secondary-text-color) !default; -$chip-avatar-pressed-active-font-color: rgba($secondary-text-color) !default; -$chip-avatar-disabled-font-color: $chip-disabled-font-color !default; -$chip-avatar-choice-active-font-color: rgba($primary-text-color) !default; -$chip-avatar-choice-focus-active-font-color: $chip-choice-icon-focus-active-font-color !default; - -$chip-default-border-color: rgba($border) !default; -$chip-hover-border-color: rgba($border) !default; -$chip-focus-border-color: rgba($border) !default; -$chip-active-border-color: rgba($border) !default; -$chip-focus-active-border-color: $secondary-border-color-pressed !default; -$chip-pressed-border-color: rgba($border) !default; -$chip-disabled-border-color: $secondary-border-color-disabled !default; -$chip-choice-active-border-color: $chip-choice-active-bg-color !default; -$chip-choice-focus-active-border-color: rgba($primary) !default; -$chip-choice-focus-active-font-color: rgba($primary-text-color) !default; -$chip-focussed-box-shadow: none !default; -$chip-focussed-active-box-shadow: none !default; -$chip-pressed-box-shadow: none !default; - -$chip-outline-bg-color: transparent !default; -$chip-outline-hover-bg-color: $content-bg-color-hover !default; -$chip-outline-focus-bg-color: $secondary-bg-color !default; -$chip-outline-active-bg-color: $secondary-border-color-pressed !default; -$chip-outline-focus-active-bg-color: $chip-outline-active-bg-color !default; -$chip-outline-pressed-bg-color: $content-bg-color-pressed !default; -$chip-outline-pressed-active-bg-color: $content-bg-color-pressed !default; -$chip-outline-disabled-bg-color: transparent !default; -$chip-outline-choice-active-bg-color: rgba($primary) !default; -$chip-outline-choice-focus-active-bg-color: rgba($primary) !default; - -$chip-outline-avatar-bg-color: $secondary-bg-color !default; -$chip-outline-avatar-hover-bg-color: $secondary-bg-color !default; -$chip-outline-avatar-focus-bg-color: $secondary-bg-color !default; -$chip-outline-avatar-active-bg-color: $secondary-bg-color !default; -$chip-outline-avatar-focus-active-bg-color: $secondary-bg-color !default; -$chip-outline-avatar-pressed-bg-color: $secondary-bg-color !default; -$chip-outline-avatar-pressed-active-bg-color: $secondary-bg-color !default; -$chip-outline-avatar-disabled-bg-color: $chip-outline-disabled-bg-color !default; -$chip-outline-avatar-choice-active-bg-color: rgba($primary) !default; -$chip-outline-avatar-choice-focus-active-bg-color: rgba($primary) !default; - -$chip-outline-font-color: rgba($content-text-color-alt1) !default; -$chip-outline-hover-font-color: rgba($secondary-text-color-hover) !default; -$chip-outline-focus-font-color: rgba($secondary-text-color) !default; -$chip-outline-active-font-color: rgba($secondary-text-color-pressed) !default; -$chip-outline-focus-active-font-color: $chip-outline-active-font-color !default; -$chip-outline-pressed-font-color: rgba($content-text-color-alt1) !default; -$chip-outline-disabled-font-color: $content-text-color-disabled !default; -$chip-outline-choice-active-font-color: rgba($primary-text-color) !default; -$chip-outline-choice-focus-active-font-color: $chip-outline-choice-active-font-color !default; - -$chip-outline-close-icon-font-color: $secondary-bg-color !default; -$chip-outline-close-icon-hover-font-color: rgba($secondary-text-color-hover) !default; -$chip-outline-close-icon-pressed-font-color: rgba($secondary-text-color-pressed) !default; -$chip-outline-icon-font-color: $icon-color !default; -$chip-outline-icon-hover-font-color: rgba($secondary-text-color-hover) !default; -$chip-outline-icon-focus-font-color: rgba($secondary-text-color-pressed) !default; -$chip-outline-icon-active-font-color: rgba($secondary-text-color-pressed) !default; -$chip-outline-icon-focus-active-font-color: rgba($secondary-text-color-pressed) !default; -$chip-outline-icon-pressed-font-color: rgba($secondary-text-color-pressed) !default; -$chip-outline-choice-icon-focus-active-font-color: rgba($primary-text-color) !default; - -$chip-outline-avatar-font-color: rgba($secondary-text-color) !default; -$chip-outline-avatar-hover-font-color: rgba($secondary-text-color) !default; -$chip-outline-avatar-focus-font-color: rgba($secondary-text-color) !default; -$chip-outline-avatar-active-font-color: rgba($secondary-text-color) !default; -$chip-outline-avatar-focus-active-font-color: rgba($secondary-text-color) !default; -$chip-outline-avatar-pressed-font-color: rgba($secondary-text-color) !default; -$chip-outline-avatar-pressed-active-font-color: rgba($secondary-text-color) !default; -$chip-outline-avatar-disabled-font-color: $content-text-color-disabled !default; -$chip-outline-avatar-choice-active-font-color: $chip-outline-choice-active-font-color !default; -$chip-outline-avatar-choice-focus-active-font-color: $chip-outline-choice-icon-focus-active-font-color !default; - -$chip-outline-border-color: rgba($border) !default; -$chip-outline-hover-border-color: rgba($border) !default; -$chip-outline-focus-border-color: rgba($border) !default; -$chip-outline-active-border-color: $secondary-border-color-pressed !default; -$chip-outline-focus-active-border-color: $secondary-border-color-pressed !default; -$chip-outline-pressed-border-color: rgba($border) !default; -$chip-outline-disabled-border-color: $content-bg-color-alt3 !default; -$chip-outline-choice-active-border-color: rgba($primary) !default; -$chip-outline-choice-focus-active-border-color: rgba($primary) !default; -$chip-outline-active-border-width: 1px !default; -$chip-outline-focus-active-box-shadow: $chip-focussed-active-box-shadow !default; - -$chip-primary-bg-color: rgba($primary) !default; -$chip-primary-hover-bg-color: $primary-bg-color-hover !default; -$chip-primary-focus-bg-color: $primary-bg-color-hover !default; -$chip-primary-active-bg-color: $primary-bg-color-pressed !default; -$chip-primary-focus-active-bg-color: $chip-primary-active-bg-color !default; -$chip-primary-pressed-bg-color: $primary-bg-color-pressed !default; -$chip-primary-disabled-bg-color: $primary-bg-color-disabled !default; -$chip-primary-avatar-bg-color: $chip-primary-bg-color !default; -$chip-primary-avatar-hover-bg-color: $chip-primary-hover-bg-color !default; -$chip-primary-avatar-focus-bg-color: $chip-primary-focus-bg-color !default; -$chip-primary-avatar-active-bg-color: $chip-primary-active-bg-color !default; -$chip-primary-avatar-focus-active-bg-color: $chip-primary-focus-active-bg-color !default; -$chip-primary-avatar-pressed-bg-color: $chip-primary-pressed-bg-color !default; -$chip-primary-avatar-disabled-bg-color: $chip-primary-disabled-bg-color !default; -$chip-primary-font-color: rgba($primary-text-color) !default; -$chip-primary-hover-font-color: rgba($primary-text-color) !default; -$chip-primary-focus-font-color: rgba($primary-text-color) !default; -$chip-primary-active-font-color: rgba($primary-text-color) !default; -$chip-primary-focus-active-font-color: rgba($primary-text-color) !default; -$chip-primary-pressed-font-color: rgba($primary-text-color) !default; -$chip-primary-disabled-font-color: $primary-text-disabled !default; -$chip-primary-close-icon-font-color: rgba($primary-text-color) !default; -$chip-primary-close-icon-hover-font-color: rgba($primary-text-color) !default; -$chip-primary-close-icon-pressed-font-color: rgba($primary-text-color) !default; -$chip-primary-avatar-font-color: $chip-primary-font-color !default; -$chip-primary-avatar-hover-font-color: $chip-primary-hover-font-color !default; -$chip-primary-avatar-focus-font-color: $chip-primary-focus-font-color !default; -$chip-primary-avatar-active-font-color: $chip-primary-active-font-color !default; -$chip-primary-avatar-focus-active-font-color: $chip-primary-focus-active-font-color !default; -$chip-primary-avatar-pressed-font-color: $chip-primary-pressed-font-color !default; -$chip-primary-avatar-disabled-font-color: $chip-primary-disabled-font-color !default; -$chip-primary-border-color: rgba($primary) !default; -$chip-primary-hover-border-color: $primary-border-color-hover !default; -$chip-primary-focus-border-color: $primary-border-color-hover !default; -$chip-primary-active-border-color: $primary-border-color-pressed !default; -$chip-primary-focus-active-border-color: $primary-border-color-pressed !default; -$chip-primary-pressed-border-color: $primary-border-color-pressed !default; -$chip-primary-disabled-border-color: $primary-border-color-disabled !default; -$chip-primary-focus-active-box-shadow: none !default; - -$chip-success-bg-color: rgba($success-bg-color) !default; -$chip-success-hover-bg-color: $success-bg-color-hover !default; -$chip-success-focus-bg-color: $success-bg-color-hover !default; -$chip-success-active-bg-color: $success-bg-color-pressed !default; -$chip-success-focus-active-bg-color: $success-bg-color-pressed !default; -$chip-success-pressed-bg-color: $success-bg-color-pressed !default; -$chip-success-disabled-bg-color: $success-bg-color-disabled !default; -$chip-success-avatar-bg-color: $chip-success-bg-color !default; -$chip-success-avatar-hover-bg-color: $chip-success-hover-bg-color !default; -$chip-success-avatar-focus-bg-color: $chip-success-focus-bg-color !default; -$chip-success-avatar-active-bg-color: $chip-success-active-bg-color !default; -$chip-success-avatar-focus-active-bg-color: $chip-success-focus-active-bg-color !default; -$chip-success-avatar-pressed-bg-color: $chip-success-pressed-bg-color !default; -$chip-success-avatar-disabled-bg-color: $chip-success-disabled-bg-color !default; -$chip-success-font-color: rgba($success-text) !default; -$chip-success-hover-font-color: rgba($success-text-hover) !default; -$chip-success-focus-font-color: rgba($success-text-hover) !default; -$chip-success-active-font-color: rgba($success-text-pressed) !default; -$chip-success-focus-active-font-color: rgba($success-text-pressed) !default; -$chip-success-pressed-font-color: rgba($success-text-pressed) !default; -$chip-success-disabled-font-color: $success-text-disabled !default; -$chip-success-close-icon-font-color: rgba($success-text) !default; -$chip-success-close-icon-hover-font-color: $chip-success-hover-font-color !default; -$chip-success-close-icon-pressed-font-color: $chip-success-pressed-font-color !default; -$chip-success-avatar-font-color: $chip-success-font-color !default; -$chip-success-avatar-hover-font-color: $chip-success-hover-font-color !default; -$chip-success-avatar-focus-font-color: $chip-success-focus-font-color !default; -$chip-success-avatar-active-font-color: $chip-success-active-font-color !default; -$chip-success-avatar-focus-active-font-color: $chip-success-focus-active-font-color !default; -$chip-success-avatar-pressed-font-color: $chip-success-pressed-font-color !default; -$chip-success-avatar-disabled-font-color: $chip-success-disabled-font-color !default; -$chip-success-border-color: rgba($success-bg-color) !default; -$chip-success-hover-border-color: $success-border-color-hover !default; -$chip-success-focus-border-color: $success-border-color-pressed !default; -$chip-success-active-border-color: $success-border-color-pressed !default; -$chip-success-focus-active-border-color: $success-border-color-pressed !default; -$chip-success-pressed-border-color: $success-border-color-pressed !default; -$chip-success-disabled-border-color: $success-border-color-disabled !default; -$chip-success-focus-active-box-shadow: none !default; - -$chip-info-bg-color: rgba($info-bg-color) !default; -$chip-info-hover-bg-color: $info-bg-color-hover !default; -$chip-info-focus-bg-color: $info-bg-color-hover !default; -$chip-info-active-bg-color: $info-bg-color-pressed !default; -$chip-info-focus-active-bg-color: $info-bg-color-pressed !default; -$chip-info-pressed-bg-color: $info-bg-color-pressed !default; -$chip-info-disabled-bg-color: $info-bg-color-disabled !default; -$chip-info-avatar-bg-color: $chip-info-bg-color !default; -$chip-info-avatar-hover-bg-color: $chip-info-hover-bg-color !default; -$chip-info-avatar-focus-bg-color: $chip-info-focus-bg-color !default; -$chip-info-avatar-active-bg-color: $chip-info-active-bg-color !default; -$chip-info-avatar-focus-active-bg-color: $chip-info-focus-active-bg-color !default; -$chip-info-avatar-pressed-bg-color: $chip-info-pressed-bg-color !default; -$chip-info-avatar-disabled-bg-color: $chip-info-disabled-bg-color !default; -$chip-info-font-color: rgba($info-text) !default; -$chip-info-hover-font-color: rgba($info-text) !default; -$chip-info-focus-font-color: rgba($info-text) !default; -$chip-info-active-font-color: rgba($info-text-pressed) !default; -$chip-info-focus-active-font-color: rgba($info-text-pressed) !default; -$chip-info-pressed-font-color: rgba($info-text-pressed) !default; -$chip-info-disabled-font-color: $info-text-disabled !default; -$chip-info-close-icon-font-color: rgba($info-text) !default; -$chip-info-close-icon-hover-font-color: $chip-info-hover-font-color !default; -$chip-info-close-icon-pressed-font-color: $chip-info-pressed-font-color !default; -$chip-info-avatar-font-color: $chip-info-font-color !default; -$chip-info-avatar-hover-font-color: $chip-info-hover-font-color !default; -$chip-info-avatar-focus-font-color: $chip-info-focus-font-color !default; -$chip-info-avatar-active-font-color: $chip-info-active-font-color !default; -$chip-info-avatar-focus-active-font-color: $chip-info-focus-active-font-color !default; -$chip-info-avatar-pressed-font-color: $chip-info-pressed-font-color !default; -$chip-info-avatar-disabled-font-color: $chip-info-disabled-font-color !default; -$chip-info-border-color: rgba($info-bg-color) !default; -$chip-info-hover-border-color: $info-border-color-hover !default; -$chip-info-focus-border-color: $info-border-color-hover !default; -$chip-info-active-border-color: $info-border-color-pressed !default; -$chip-info-focus-active-border-color: $info-border-color-pressed !default; -$chip-info-pressed-border-color: $info-border-color-pressed !default; -$chip-info-disabled-border-color: $info-border-color-disabled !default; -$chip-info-focus-active-box-shadow: none !default; - -$chip-warning-bg-color: rgba($warning-bg-color) !default; -$chip-warning-hover-bg-color: $warning-bg-color-hover !default; -$chip-warning-focus-bg-color: $warning-bg-color-hover !default; -$chip-warning-active-bg-color: $warning-bg-color-pressed !default; -$chip-warning-focus-active-bg-color: $warning-bg-color-pressed !default; -$chip-warning-pressed-bg-color: $warning-bg-color-pressed !default; -$chip-warning-disabled-bg-color: $warning-bg-color-disabled !default; -$chip-warning-avatar-bg-color: $chip-warning-bg-color !default; -$chip-warning-avatar-hover-bg-color: $chip-warning-hover-bg-color !default; -$chip-warning-avatar-focus-bg-color: $chip-warning-focus-bg-color !default; -$chip-warning-avatar-active-bg-color: $chip-warning-active-bg-color !default; -$chip-warning-avatar-focus-active-bg-color: $chip-warning-focus-active-bg-color !default; -$chip-warning-avatar-pressed-bg-color: $chip-warning-pressed-bg-color !default; -$chip-warning-avatar-disabled-bg-color: $chip-warning-disabled-bg-color !default; -$chip-warning-font-color: rgba($warning-text) !default; -$chip-warning-hover-font-color: rgba($warning-text) !default; -$chip-warning-focus-font-color: rgba($warning-text) !default; -$chip-warning-active-font-color: rgba($warning-text-pressed) !default; -$chip-warning-focus-active-font-color: rgba($warning-text-pressed) !default; -$chip-warning-pressed-font-color: rgba($warning-text-pressed) !default; -$chip-warning-disabled-font-color: $warning-text-disabled !default; -$chip-warning-close-icon-font-color: rgba($warning-text) !default; -$chip-warning-close-icon-hover-font-color: $chip-warning-hover-font-color !default; -$chip-warning-close-icon-pressed-font-color: $chip-warning-pressed-font-color !default; -$chip-warning-avatar-font-color: $chip-warning-font-color !default; -$chip-warning-avatar-hover-font-color: $chip-warning-hover-font-color !default; -$chip-warning-avatar-focus-font-color: $chip-warning-focus-font-color !default; -$chip-warning-avatar-active-font-color: $chip-warning-active-font-color !default; -$chip-warning-avatar-focus-active-font-color: $chip-warning-focus-active-font-color !default; -$chip-warning-avatar-pressed-font-color: $chip-warning-pressed-font-color !default; -$chip-warning-avatar-disabled-font-color: $chip-warning-disabled-font-color !default; -$chip-warning-border-color: rgba($warning-bg-color) !default; -$chip-warning-hover-border-color: $warning-border-color-hover !default; -$chip-warning-focus-border-color: $warning-border-color-hover !default; -$chip-warning-active-border-color: $warning-border-color-pressed !default; -$chip-warning-focus-active-border-color: $warning-border-color-pressed !default; -$chip-warning-pressed-border-color: $warning-border-color-pressed !default; -$chip-warning-disabled-border-color: $warning-border-color-disabled !default; -$chip-warning-focus-active-box-shadow: none !default; - -$chip-danger-bg-color: rgba($danger-bg-color) !default; -$chip-danger-hover-bg-color: $danger-bg-color-hover !default; -$chip-danger-focus-bg-color: $danger-bg-color-hover !default; -$chip-danger-active-bg-color: $danger-bg-color-pressed !default; -$chip-danger-focus-active-bg-color: $danger-bg-color-pressed !default; -$chip-danger-pressed-bg-color: $danger-bg-color-pressed !default; -$chip-danger-disabled-bg-color: $danger-border-color-disabled !default; -$chip-danger-avatar-bg-color: rgba($danger-bg-color) !default; -$chip-danger-avatar-hover-bg-color: $chip-danger-hover-bg-color !default; -$chip-danger-avatar-focus-bg-color: $chip-danger-focus-bg-color !default; -$chip-danger-avatar-active-bg-color: $chip-danger-active-bg-color !default; -$chip-danger-avatar-focus-active-bg-color: $chip-danger-focus-active-bg-color !default; -$chip-danger-avatar-pressed-bg-color: $chip-danger-pressed-bg-color !default; -$chip-danger-avatar-disabled-bg-color: $chip-danger-disabled-bg-color !default; -$chip-danger-font-color: rgba($success-text) !default; -$chip-danger-hover-font-color: rgba($success-text) !default; -$chip-danger-focus-font-color: rgba($success-text) !default; -$chip-danger-active-font-color: rgba($danger-text-pressed) !default; -$chip-danger-focus-active-font-color: rgba($danger-text-pressed) !default; -$chip-danger-pressed-font-color: rgba($danger-text-pressed) !default; -$chip-danger-disabled-font-color: $warning-text-disabled !default; -$chip-danger-close-icon-font-color: rgba($success-text) !default; -$chip-danger-close-icon-hover-font-color: $chip-danger-hover-font-color !default; -$chip-danger-close-icon-pressed-font-color: $chip-danger-pressed-font-color !default; -$chip-danger-avatar-font-color: $chip-danger-font-color !default; -$chip-danger-avatar-hover-font-color: $chip-danger-hover-font-color !default; -$chip-danger-avatar-focus-font-color: $chip-danger-focus-font-color !default; -$chip-danger-avatar-active-font-color: $chip-danger-active-font-color !default; -$chip-danger-avatar-focus-active-font-color: $chip-danger-focus-active-font-color !default; -$chip-danger-avatar-pressed-font-color: $chip-danger-pressed-font-color !default; -$chip-danger-avatar-disabled-font-color: $chip-danger-disabled-font-color !default; -$chip-danger-border-color: rgba($danger-bg-color) !default; -$chip-danger-hover-border-color: $danger-border-color-hover !default; -$chip-danger-focus-border-color: $danger-border-color-hover !default; -$chip-danger-active-border-color: $danger-border-color-pressed !default; -$chip-danger-focus-active-border-color: $danger-border-color-pressed !default; -$chip-danger-pressed-border-color: $danger-border-color-pressed !default; -$chip-danger-disabled-border-color: $danger-border-color-disabled !default; -$chip-danger-focus-active-box-shadow: none !default; - -$chip-outline-primary-bg-color: transparent !default; -$chip-outline-primary-hover-bg-color: rgba($primary, $opacity8) !default; -$chip-outline-primary-focus-bg-color: rgba($primary, $opacity12) !default; -$chip-outline-primary-active-bg-color: rgba($primary, $opacity16) !default; -$chip-outline-primary-focus-active-bg-color: rgba($primary, $opacity16) !default; -$chip-outline-primary-pressed-bg-color: rgba($primary, $opacity16) !default; -$chip-outline-primary-disabled-bg-color: transparent !default; -$chip-outline-primary-avatar-bg-color: rgba($primary) !default; -$chip-outline-primary-avatar-hover-bg-color: $chip-outline-primary-avatar-bg-color !default; -$chip-outline-primary-avatar-focus-bg-color: $chip-outline-primary-avatar-bg-color !default; -$chip-outline-primary-avatar-active-bg-color: $chip-outline-primary-avatar-bg-color !default; -$chip-outline-primary-avatar-focus-active-bg-color: $chip-outline-primary-avatar-bg-color !default; -$chip-outline-primary-avatar-pressed-bg-color: $chip-outline-primary-avatar-bg-color !default; -$chip-outline-primary-avatar-disabled-bg-color: $chip-outline-primary-disabled-bg-color !default; -$chip-outline-primary-font-color: rgba($primary) !default; -$chip-outline-primary-hover-font-color: rgba($primary) !default; -$chip-outline-primary-focus-font-color: rgba($primary) !default; -$chip-outline-primary-active-font-color: rgba($primary) !default; -$chip-outline-primary-focus-active-font-color: rgba($primary) !default; -$chip-outline-primary-pressed-font-color: rgba($primary) !default; -$chip-outline-primary-disabled-font-color: $content-text-color-disabled !default; -$chip-outline-primary-close-icon-font-color: rgba($primary) !default; -$chip-outline-primary-close-icon-hover-font-color: $chip-outline-primary-hover-font-color !default; -$chip-outline-primary-close-icon-pressed-font-color: $chip-outline-primary-pressed-font-color !default; -$chip-outline-primary-avatar-font-color: rgba($primary-text-color) !default; -$chip-outline-primary-avatar-hover-font-color: $chip-outline-primary-avatar-font-color !default; -$chip-outline-primary-avatar-focus-font-color: $chip-outline-primary-avatar-font-color !default; -$chip-outline-primary-avatar-active-font-color: $chip-outline-primary-avatar-font-color !default; -$chip-outline-primary-avatar-focus-active-font-color: $chip-outline-primary-avatar-font-color !default; -$chip-outline-primary-avatar-pressed-font-color: $chip-outline-primary-avatar-font-color !default; -$chip-outline-primary-avatar-disabled-font-color: $chip-outline-primary-disabled-font-color !default; -$chip-outline-primary-border-color: rgba($primary) !default; -$chip-outline-primary-hover-border-color: rgba($primary) !default; -$chip-outline-primary-focus-border-color: rgba($primary) !default; -$chip-outline-primary-focus-active-border-color: rgba($primary) !default; -$chip-outline-primary-active-border-color: rgba($primary) !default; -$chip-outline-primary-pressed-border-color: rgba($primary) !default; -$chip-outline-primary-disabled-border-color: rgba($content-text-color) !default; - -$chip-outline-success-bg-color: transparent !default; -$chip-outline-success-hover-bg-color: rgba($success-bg-color, $opacity8) !default; -$chip-outline-success-focus-bg-color: rgba($success-bg-color, $opacity12) !default; -$chip-outline-success-active-bg-color: rgba($success-bg-color, $opacity16) !default; -$chip-outline-success-focus-active-bg-color: rgba($success-bg-color, $opacity16) !default; -$chip-outline-success-pressed-bg-color: rgba($success-bg-color, $opacity16) !default; -$chip-outline-success-disabled-bg-color: transparent !default; -$chip-outline-success-avatar-bg-color: rgba($success) !default; -$chip-outline-success-avatar-hover-bg-color: $chip-outline-success-avatar-bg-color !default; -$chip-outline-success-avatar-focus-bg-color: $chip-outline-success-avatar-bg-color !default; -$chip-outline-success-avatar-active-bg-color: $chip-outline-success-avatar-bg-color !default; -$chip-outline-success-avatar-focus-active-bg-color: $chip-outline-success-avatar-bg-color !default; -$chip-outline-success-avatar-pressed-bg-color: $chip-outline-success-avatar-bg-color !default; -$chip-outline-success-avatar-disabled-bg-color: $chip-outline-success-disabled-bg-color !default; -$chip-outline-success-font-color: rgba($success-bg-color) !default; -$chip-outline-success-hover-font-color: rgba($success-bg-color) !default; -$chip-outline-success-focus-font-color: rgba($success-bg-color) !default; -$chip-outline-success-active-font-color: rgba($success-bg-color) !default; -$chip-outline-success-focus-active-font-color: rgba($success-bg-color) !default; -$chip-outline-success-pressed-font-color: rgba($success-bg-color) !default; -$chip-outline-success-disabled-font-color: $content-text-color-disabled !default; -$chip-outline-success-close-icon-font-color: rgba($success-bg-color) !default; -$chip-outline-success-close-icon-hover-font-color: rgba($success-bg-color) !default; -$chip-outline-success-close-icon-pressed-font-color: rgba($success-bg-color) !default; -$chip-outline-success-avatar-font-color: rgba($success-text) !default; -$chip-outline-success-avatar-hover-font-color: $chip-outline-success-avatar-font-color !default; -$chip-outline-success-avatar-focus-font-color: $chip-outline-success-avatar-font-color !default; -$chip-outline-success-avatar-active-font-color: $chip-outline-success-avatar-font-color !default; -$chip-outline-success-avatar-focus-active-font-color: $chip-outline-success-avatar-font-color !default; -$chip-outline-success-avatar-pressed-font-color: $chip-outline-success-avatar-font-color !default; -$chip-outline-success-avatar-disabled-font-color: $chip-outline-success-disabled-font-color !default; -$chip-outline-success-border-color: rgba($success) !default; -$chip-outline-success-hover-border-color: rgba($success-bg-color) !default; -$chip-outline-success-focus-border-color: rgba($success-bg-color) !default; -$chip-outline-success-active-border-color: rgba($success-bg-color) !default; -$chip-outline-success-focus-active-border-color: rgba($success-bg-color) !default; -$chip-outline-success-pressed-border-color: rgba($success-bg-color) !default; -$chip-outline-success-disabled-border-color: $content-bg-color-alt3 !default; - -$chip-outline-info-bg-color: transparent !default; -$chip-outline-info-hover-bg-color: rgba($info-bg-color, $opacity8) !default; -$chip-outline-info-focus-bg-color: rgba($info-bg-color, $opacity12) !default; -$chip-outline-info-active-bg-color: rgba($info-bg-color, $opacity16) !default; -$chip-outline-info-focus-active-bg-color: rgba($info-bg-color, $opacity16) !default; -$chip-outline-info-pressed-bg-color: rgba($info-bg-color, $opacity12) !default; -$chip-outline-info-disabled-bg-color: transparent !default; -$chip-outline-info-avatar-bg-color: rgba($info) !default; -$chip-outline-info-avatar-hover-bg-color: $chip-outline-info-avatar-bg-color !default; -$chip-outline-info-avatar-focus-bg-color: $chip-outline-info-avatar-bg-color !default; -$chip-outline-info-avatar-active-bg-color: $chip-outline-info-avatar-bg-color !default; -$chip-outline-info-avatar-focus-active-bg-color: $chip-outline-info-avatar-bg-color !default; -$chip-outline-info-avatar-pressed-bg-color: $chip-outline-info-avatar-bg-color !default; -$chip-outline-info-avatar-disabled-bg-color: $chip-outline-info-disabled-bg-color !default; -$chip-outline-info-font-color: rgba($info-bg-color) !default; -$chip-outline-info-hover-font-color: rgba($info-bg-color) !default; -$chip-outline-info-focus-font-color: rgba($info-bg-color) !default; -$chip-outline-info-active-font-color: rgba($info-bg-color) !default; -$chip-outline-info-focus-active-font-color: rgba($info-bg-color) !default; -$chip-outline-info-pressed-font-color: rgba($info-bg-color) !default; -$chip-outline-info-disabled-font-color: rgba($content-text-color) !default; -$chip-outline-info-close-icon-font-color: rgba($info-bg-color) !default; -$chip-outline-info-close-icon-hover-font-color: rgba($info-bg-color) !default; -$chip-outline-info-close-icon-pressed-font-color: rgba($info-bg-color) !default; -$chip-outline-info-avatar-font-color: rgba($info-text) !default; -$chip-outline-info-avatar-hover-font-color: $chip-outline-info-avatar-font-color !default; -$chip-outline-info-avatar-focus-font-color: $chip-outline-info-avatar-font-color !default; -$chip-outline-info-avatar-active-font-color: $chip-outline-info-avatar-font-color !default; -$chip-outline-info-avatar-focus-active-font-color: $chip-outline-info-avatar-font-color !default; -$chip-outline-info-avatar-pressed-font-color: $chip-outline-info-avatar-font-color !default; -$chip-outline-info-avatar-disabled-font-color: $content-text-color-disabled !default; -$chip-outline-info-border-color: rgba($info-bg-color) !default; -$chip-outline-info-hover-border-color: rgba($info-bg-color) !default; -$chip-outline-info-focus-border-color: rgba($info-bg-color) !default; -$chip-outline-info-active-border-color: rgba($info-bg-color) !default; -$chip-outline-info-focus-active-border-color: rgba($info-bg-color) !default; -$chip-outline-info-pressed-border-color: rgba($info-bg-color) !default; -$chip-outline-info-disabled-border-color: rgba($content-text-color) !default; - -$chip-outline-warning-bg-color: transparent !default; -$chip-outline-warning-hover-bg-color: rgba($warning-bg-color, $opacity8) !default; -$chip-outline-warning-focus-bg-color: rgba($warning-bg-color, $opacity12) !default; -$chip-outline-warning-active-bg-color: rgba($warning-bg-color, $opacity16) !default; -$chip-outline-warning-focus-active-bg-color: rgba($warning-bg-color, $opacity16) !default; -$chip-outline-warning-pressed-bg-color: rgba($warning-bg-color, $opacity16) !default; -$chip-outline-warning-disabled-bg-color: transparent !default; -$chip-outline-warning-avatar-bg-color: rgba($warning) !default; -$chip-outline-warning-avatar-hover-bg-color: $chip-outline-warning-avatar-bg-color !default; -$chip-outline-warning-avatar-focus-bg-color: $chip-outline-warning-avatar-bg-color !default; -$chip-outline-warning-avatar-active-bg-color: $chip-outline-warning-avatar-bg-color !default; -$chip-outline-warning-avatar-focus-active-bg-color: $chip-outline-warning-avatar-bg-color !default; -$chip-outline-warning-avatar-pressed-bg-color: $chip-outline-warning-avatar-bg-color !default; -$chip-outline-warning-avatar-disabled-bg-color: $chip-outline-warning-disabled-bg-color !default; -$chip-outline-warning-font-color: rgba($warning-bg-color) !default; -$chip-outline-warning-hover-font-color: rgba($warning-bg-color) !default; -$chip-outline-warning-focus-font-color: rgba($warning-bg-color) !default; -$chip-outline-warning-active-font-color: rgba($warning-bg-color) !default; -$chip-outline-warning-focus-active-font-color: rgba($warning-bg-color) !default; -$chip-outline-warning-pressed-font-color: rgba($warning-bg-color) !default; -$chip-outline-warning-disabled-font-color: $content-text-color-disabled !default; -$chip-outline-warning-close-icon-font-color: rgba($warning-bg-color) !default; -$chip-outline-warning-close-icon-hover-font-color: $chip-outline-warning-hover-font-color !default; -$chip-outline-warning-close-icon-pressed-font-color: $chip-outline-warning-pressed-font-color !default; -$chip-outline-warning-avatar-font-color: rgba($warning-text) !default; -$chip-outline-warning-avatar-hover-font-color: $chip-outline-warning-avatar-font-color !default; -$chip-outline-warning-avatar-focus-font-color: $chip-outline-warning-avatar-font-color !default; -$chip-outline-warning-avatar-active-font-color: $chip-outline-warning-avatar-font-color !default; -$chip-outline-warning-avatar-focus-active-font-color: $chip-outline-warning-avatar-font-color !default; -$chip-outline-warning-avatar-pressed-font-color: $chip-outline-warning-avatar-font-color !default; -$chip-outline-warning-avatar-disabled-font-color: $chip-outline-warning-disabled-font-color !default; -$chip-outline-warning-border-color: rgba($warning) !default; -$chip-outline-warning-hover-border-color: rgba($warning-bg-color) !default; -$chip-outline-warning-focus-border-color: rgba($warning-bg-color) !default; -$chip-outline-warning-active-border-color: rgba($warning-bg-color) !default; -$chip-outline-warning-focus-active-border-color: rgba($warning-bg-color) !default; -$chip-outline-warning-pressed-border-color: rgba($warning-bg-color) !default; -$chip-outline-warning-disabled-border-color: $content-bg-color-alt3 !default; - -$chip-outline-danger-bg-color: transparent !default; -$chip-outline-danger-hover-bg-color: rgba($danger-bg-color, $opacity8) !default; -$chip-outline-danger-focus-bg-color: rgba($danger-bg-color, $opacity12) !default; -$chip-outline-danger-active-bg-color: rgba($danger-bg-color, $opacity16) !default; -$chip-outline-danger-focus-active-bg-color: rgba($danger-bg-color, $opacity16) !default; -$chip-outline-danger-pressed-bg-color: rgba($danger-bg-color, $opacity16) !default; -$chip-outline-danger-disabled-bg-color: transparent !default; -$chip-outline-danger-avatar-bg-color: rgba($danger) !default; -$chip-outline-danger-avatar-hover-bg-color: $chip-outline-danger-avatar-bg-color !default; -$chip-outline-danger-avatar-focus-bg-color: $chip-outline-danger-avatar-bg-color !default; -$chip-outline-danger-avatar-active-bg-color: $chip-outline-danger-avatar-bg-color !default; -$chip-outline-danger-avatar-focus-active-bg-color: $chip-outline-danger-avatar-bg-color !default; -$chip-outline-danger-avatar-pressed-bg-color: $chip-outline-danger-avatar-bg-color !default; -$chip-outline-danger-avatar-disabled-bg-color: $chip-outline-danger-disabled-bg-color !default; -$chip-outline-danger-font-color: rgba($danger) !default; -$chip-outline-danger-hover-font-color: rgba($danger-bg-color) !default; -$chip-outline-danger-focus-font-color: rgba($danger-bg-color) !default; -$chip-outline-danger-active-font-color: rgba($danger-bg-color) !default; -$chip-outline-danger-focus-active-font-color: rgba($danger-bg-color) !default; -$chip-outline-danger-pressed-font-color: rgba($danger-bg-color) !default; -$chip-outline-danger-disabled-font-color: $content-text-color-disabled !default; -$chip-outline-danger-close-icon-font-color: rgba($danger) !default; -$chip-outline-danger-close-icon-hover-font-color: $chip-outline-danger-hover-font-color !default; -$chip-outline-danger-close-icon-pressed-font-color: $chip-outline-danger-pressed-font-color !default; -$chip-outline-danger-avatar-font-color: rgba($danger-text) !default; -$chip-outline-danger-avatar-hover-font-color: $chip-outline-danger-avatar-font-color !default; -$chip-outline-danger-avatar-focus-font-color: $chip-outline-danger-avatar-font-color !default; -$chip-outline-danger-avatar-active-font-color: $chip-outline-danger-avatar-font-color !default; -$chip-outline-danger-avatar-focus-active-font-color: $chip-outline-danger-avatar-font-color !default; -$chip-outline-danger-avatar-pressed-font-color: $chip-outline-danger-avatar-font-color !default; -$chip-outline-danger-avatar-disabled-font-color: $chip-outline-danger-disabled-font-color !default; -$chip-outline-danger-border-color: rgba($danger) !default; -$chip-outline-danger-hover-border-color: rgba($danger-bg-color) !default; -$chip-outline-danger-focus-border-color: rgba($danger-bg-color) !default; -$chip-outline-danger-active-border-color: rgba($danger-bg-color) !default; -$chip-outline-danger-focus-active-border-color: rgba($danger-bg-color) !default; -$chip-outline-danger-pressed-border-color: rgba($danger-bg-color) !default; -$chip-outline-danger-disabled-border-color: rgba($danger-bg-color) !default; \ No newline at end of file diff --git a/components/buttons/styles/chips/_theme.scss b/components/buttons/styles/chips/_theme.scss deleted file mode 100644 index 12b9452..0000000 --- a/components/buttons/styles/chips/_theme.scss +++ /dev/null @@ -1,451 +0,0 @@ -@mixin chip-color($bg-color, $border-color, $color, $icon-color, $avatar-bg-color, $avatar-font-color, $delete-icon-color: null) { - background: $bg-color; - @if $skin-name != 'Material3' { - border-color: $border-color; - } - @else { - border-image: $border-color; - } - color: $color; - - .sf-selectable-icon path { - fill: $color; - } - - .sf-chip-icon, - .sf-chip-delete { - color: $icon-color; - - path { - fill: $icon-color; - } - } - - .sf-chip-delete.sf-dlt-btn path { - fill: $delete-icon-color; - } - - .sf-chip-avatar { - background-color: $avatar-bg-color; - color: $avatar-font-color; - } -} - -@mixin chip-dlt-btn-color($hover-color, $pressed-color) { - &:not(.sf-active) { - .sf-chip-delete.sf-dlt-btn { - &:hover path { - fill: $hover-color; - } - - &:active path { - fill: $pressed-color; - } - } - } -} - -@include export-module('chip-theme') { - .sf-chip-list { - &.sf-selection .sf-chip { - &.sf-active { - @include chip-color($chip-choice-active-bg-color, $chip-choice-active-border-color, $chip-choice-active-font-color, $chip-choice-active-font-color, $chip-avatar-choice-active-bg-color, $chip-avatar-choice-active-font-color); - - &.sf-focused { - @include chip-color($chip-choice-focus-active-bg-color, $chip-choice-focus-active-border-color, $chip-choice-focus-active-font-color, $chip-choice-icon-focus-active-font-color, $chip-avatar-choice-focus-active-bg-color, $chip-avatar-choice-focus-active-font-color); - } - - &.sf-disabled { - @include chip-color($chip-disabled-bg-color, $chip-disabled-border-color, $chip-disabled-font-color, $chip-disabled-font-color, $chip-avatar-disabled-bg-color, $chip-avatar-disabled-font-color); - } - - &.sf-outline { - @include chip-color($chip-outline-choice-active-bg-color, $chip-outline-choice-active-border-color, $chip-outline-choice-active-font-color, $chip-outline-choice-active-font-color, $chip-outline-avatar-choice-active-bg-color, $chip-outline-avatar-choice-active-font-color); - border-width: $chip-outline-active-border-width; - &.sf-focused { - @include chip-color($chip-outline-choice-focus-active-bg-color, $chip-outline-choice-focus-active-border-color, $chip-outline-choice-focus-active-font-color, $chip-outline-choice-icon-focus-active-font-color, $chip-outline-avatar-choice-focus-active-bg-color, $chip-outline-avatar-choice-focus-active-font-color); - box-shadow: $chip-outline-focus-active-box-shadow; - } - - &.sf-disabled { - @include chip-color($chip-outline-disabled-bg-color, $chip-outline-disabled-border-color, $chip-disabled-font-color, $chip-disabled-font-color, $chip-outline-avatar-disabled-bg-color, $chip-outline-avatar-disabled-font-color); - } - } - } - - &:active { - @include chip-color($chip-pressed-active-bg-color, $chip-pressed-border-color, $chip-selection-pressed-font-color, $chip-icon-selection-pressed-font-color, $chip-avatar-pressed-active-bg-color, $chip-avatar-pressed-active-font-color); - - &.sf-outline { - @include chip-color($chip-outline-pressed-active-bg-color, $chip-outline-pressed-border-color, $chip-outline-pressed-font-color, $chip-outline-icon-pressed-font-color, $chip-outline-avatar-pressed-active-bg-color, $chip-outline-avatar-pressed-active-font-color); - } - } - } - - &.sf-chip, - & .sf-chip { - @include chip-color($chip-default-bg-color, $chip-default-border-color, $chip-default-font-color, $chip-icon-font-color, $chip-avatar-bg-color, $chip-avatar-font-color, $chip-close-icon-font-color); - @include chip-dlt-btn-color($chip-close-icon-hover-font-color, $chip-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-hover-bg-color, $chip-hover-border-color, $chip-hover-font-color, $chip-icon-hover-font-color, $chip-avatar-hover-bg-color, $chip-avatar-hover-font-color); - } - - &.sf-focused { - box-shadow: $chip-focussed-box-shadow; - @include chip-color($chip-focus-bg-color, $chip-focus-border-color, $chip-focus-font-color, $chip-icon-focus-font-color, $chip-avatar-hover-bg-color, $chip-avatar-hover-font-color); - &.sf-active { - @include chip-color($chip-focus-active-bg-color, $chip-focus-active-border-color, $chip-focus-active-font-color, $chip-icon-focus-active-font-color, $chip-avatar-focus-active-bg-color, $chip-avatar-focus-active-font-color); - box-shadow: $chip-focussed-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-active-bg-color, $chip-active-border-color, $chip-active-font-color, $chip-icon-active-font-color, $chip-avatar-active-bg-color, $chip-avatar-active-font-color); - box-shadow: none; - } - - &:active { - @include chip-color($chip-pressed-bg-color, $chip-pressed-border-color, $chip-pressed-font-color, $chip-icon-pressed-font-color, $chip-avatar-pressed-bg-color, $chip-avatar-pressed-font-color); - box-shadow: $chip-pressed-box-shadow; - } - - &.sf-disabled { - @include chip-color($chip-disabled-bg-color, $chip-disabled-border-color, $chip-disabled-font-color, $chip-disabled-font-color, $chip-avatar-disabled-bg-color, $chip-avatar-disabled-font-color); - opacity: 1; - pointer-events: none; - } - - &.sf-outline { - @include chip-color(transparent, $chip-outline-border-color, $chip-outline-font-color, $chip-outline-icon-font-color, $chip-outline-avatar-bg-color, $chip-outline-avatar-font-color, $chip-outline-close-icon-font-color); - @include chip-dlt-btn-color($chip-outline-close-icon-hover-font-color, $chip-outline-close-icon-pressed-font-color); - border-width: 1px; - - &:hover { - @include chip-color($chip-outline-hover-bg-color, $chip-outline-hover-border-color, $chip-outline-hover-font-color, $chip-outline-icon-hover-font-color, $chip-outline-avatar-hover-bg-color, $chip-outline-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-outline-focus-bg-color, $chip-outline-focus-border-color, $chip-outline-focus-font-color, $chip-outline-icon-focus-font-color, $chip-outline-avatar-focus-bg-color, $chip-outline-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-outline-focus-active-bg-color, $chip-outline-focus-active-border-color, $chip-outline-focus-active-font-color, $chip-outline-icon-focus-active-font-color, $chip-outline-avatar-focus-active-bg-color, $chip-outline-avatar-focus-active-font-color); - box-shadow: $chip-outline-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-outline-active-bg-color, $chip-outline-active-border-color, $chip-outline-active-font-color, $chip-outline-icon-active-font-color, $chip-outline-avatar-active-bg-color, $chip-outline-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-outline-pressed-bg-color, $chip-outline-pressed-border-color, $chip-outline-pressed-font-color, $chip-outline-icon-pressed-font-color, $chip-outline-avatar-pressed-bg-color, $chip-outline-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-outline-disabled-bg-color, $chip-outline-disabled-border-color, $chip-disabled-font-color, $chip-disabled-font-color, $chip-outline-avatar-disabled-bg-color, $chip-outline-avatar-disabled-font-color); - } - } - - &.sf-primary { - @include chip-color($chip-primary-bg-color, $chip-primary-border-color, $chip-primary-font-color, $chip-primary-font-color, $chip-primary-avatar-bg-color, $chip-primary-avatar-font-color, $chip-primary-close-icon-font-color); - @include chip-dlt-btn-color($chip-primary-close-icon-hover-font-color, $chip-primary-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-primary-hover-bg-color, $chip-primary-hover-border-color, $chip-primary-hover-font-color, $chip-primary-hover-font-color, $chip-primary-avatar-hover-bg-color, $chip-primary-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-primary-focus-bg-color, $chip-primary-focus-border-color, $chip-primary-focus-font-color, $chip-primary-focus-font-color, $chip-primary-avatar-focus-bg-color, $chip-primary-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-primary-focus-active-bg-color, $chip-primary-focus-active-border-color, $chip-primary-focus-active-font-color, $chip-primary-focus-active-font-color, $chip-primary-avatar-focus-active-bg-color, $chip-primary-avatar-focus-active-font-color); - box-shadow: $chip-primary-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-primary-active-bg-color, $chip-primary-active-border-color, $chip-primary-active-font-color, $chip-primary-active-font-color, $chip-primary-avatar-active-bg-color, $chip-primary-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-primary-pressed-bg-color, $chip-primary-pressed-border-color, $chip-primary-pressed-font-color, $chip-primary-pressed-font-color, $chip-primary-avatar-pressed-bg-color, $chip-primary-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-primary-disabled-bg-color, $chip-primary-disabled-border-color, $chip-primary-disabled-font-color, $chip-primary-disabled-font-color, $chip-primary-avatar-disabled-bg-color, $chip-primary-avatar-disabled-font-color); - } - - &.sf-outline { - @include chip-color($chip-outline-primary-bg-color, $chip-outline-primary-border-color, $chip-outline-primary-font-color, $chip-outline-primary-font-color, $chip-outline-primary-avatar-bg-color, $chip-outline-primary-avatar-font-color, $chip-outline-primary-close-icon-font-color); - @include chip-dlt-btn-color($chip-outline-primary-close-icon-hover-font-color, $chip-outline-primary-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-outline-primary-hover-bg-color, $chip-outline-primary-hover-border-color, $chip-outline-primary-hover-font-color, $chip-outline-primary-hover-font-color, $chip-outline-primary-avatar-hover-bg-color, $chip-outline-primary-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-outline-primary-focus-bg-color, $chip-outline-primary-focus-border-color, $chip-outline-primary-focus-font-color, $chip-outline-primary-focus-font-color, $chip-outline-primary-avatar-focus-bg-color, $chip-outline-primary-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-outline-primary-focus-active-bg-color, $chip-outline-primary-focus-active-border-color, $chip-outline-primary-focus-active-font-color, $chip-outline-primary-focus-active-font-color, $chip-outline-primary-avatar-focus-active-bg-color, $chip-outline-primary-avatar-focus-active-font-color); - box-shadow: $chip-primary-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-outline-primary-active-bg-color, $chip-outline-primary-active-border-color, $chip-outline-primary-active-font-color, $chip-outline-primary-active-font-color, $chip-outline-primary-avatar-active-bg-color, $chip-outline-primary-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-outline-primary-pressed-bg-color, $chip-outline-primary-pressed-border-color, $chip-outline-primary-pressed-font-color, $chip-outline-primary-pressed-font-color, $chip-outline-primary-avatar-pressed-bg-color, $chip-outline-primary-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-outline-primary-disabled-bg-color, $chip-outline-primary-disabled-border-color, $chip-outline-primary-disabled-font-color, $chip-outline-primary-disabled-font-color, $chip-outline-primary-avatar-disabled-bg-color, $chip-outline-primary-avatar-disabled-font-color); - } - } - } - - &.sf-success { - @include chip-color($chip-success-bg-color, $chip-success-border-color, $chip-success-font-color, $chip-success-font-color, $chip-success-avatar-bg-color, $chip-success-avatar-font-color, $chip-success-close-icon-font-color); - @include chip-dlt-btn-color($chip-success-close-icon-hover-font-color, $chip-success-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-success-hover-bg-color, $chip-success-hover-border-color, $chip-success-hover-font-color, $chip-success-hover-font-color, $chip-success-avatar-hover-bg-color, $chip-success-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-success-focus-bg-color, $chip-success-focus-border-color, $chip-success-focus-font-color, $chip-success-focus-font-color, $chip-success-avatar-focus-bg-color, $chip-success-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-success-focus-active-bg-color, $chip-success-focus-active-border-color, $chip-success-focus-active-font-color, $chip-success-focus-active-font-color, $chip-success-avatar-focus-active-bg-color, $chip-success-avatar-focus-active-font-color); - box-shadow: $chip-success-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-success-active-bg-color, $chip-success-active-border-color, $chip-success-active-font-color, $chip-success-active-font-color, $chip-success-avatar-active-bg-color, $chip-success-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-success-pressed-bg-color, $chip-success-pressed-border-color, $chip-success-pressed-font-color, $chip-success-pressed-font-color, $chip-success-avatar-pressed-bg-color, $chip-success-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-success-disabled-bg-color, $chip-success-disabled-border-color, $chip-success-disabled-font-color, $chip-success-disabled-font-color, $chip-success-avatar-disabled-bg-color, $chip-success-avatar-disabled-font-color); - } - - &.sf-outline { - @include chip-color($chip-outline-success-bg-color, $chip-outline-success-border-color, $chip-outline-success-font-color, $chip-outline-success-font-color, $chip-outline-success-avatar-bg-color, $chip-outline-success-avatar-font-color, $chip-outline-success-close-icon-font-color); - @include chip-dlt-btn-color($chip-outline-success-close-icon-hover-font-color, $chip-outline-success-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-outline-success-hover-bg-color, $chip-outline-success-hover-border-color, $chip-outline-success-hover-font-color, $chip-outline-success-hover-font-color, $chip-outline-success-avatar-hover-bg-color, $chip-outline-success-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-outline-success-focus-bg-color, $chip-outline-success-focus-border-color, $chip-outline-success-focus-font-color, $chip-outline-success-focus-font-color, $chip-outline-success-avatar-focus-bg-color, $chip-outline-success-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-outline-success-focus-active-bg-color, $chip-outline-success-focus-active-border-color, $chip-outline-success-focus-active-font-color, $chip-outline-success-focus-active-font-color, $chip-outline-success-avatar-focus-active-bg-color, $chip-outline-success-avatar-focus-active-font-color); - box-shadow: $chip-success-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-outline-success-active-bg-color, $chip-outline-success-active-border-color, $chip-outline-success-active-font-color, $chip-outline-success-active-font-color, $chip-outline-success-avatar-active-bg-color, $chip-outline-success-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-outline-success-pressed-bg-color, $chip-outline-success-pressed-border-color, $chip-outline-success-pressed-font-color, $chip-outline-success-pressed-font-color, $chip-outline-success-avatar-pressed-bg-color, $chip-outline-success-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-outline-success-disabled-bg-color, $chip-outline-success-disabled-border-color, $chip-outline-success-disabled-font-color, $chip-outline-success-disabled-font-color, $chip-outline-success-avatar-disabled-bg-color, $chip-outline-success-avatar-disabled-font-color); - } - } - } - - &.sf-info { - @include chip-color($chip-info-bg-color, $chip-info-border-color, $chip-info-font-color, $chip-info-font-color, $chip-info-avatar-bg-color, $chip-info-avatar-font-color, $chip-info-close-icon-font-color); - @include chip-dlt-btn-color($chip-info-close-icon-hover-font-color, $chip-info-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-info-hover-bg-color, $chip-info-hover-border-color, $chip-info-hover-font-color, $chip-info-hover-font-color, $chip-info-avatar-hover-bg-color, $chip-info-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-info-focus-bg-color, $chip-info-focus-border-color, $chip-info-focus-font-color, $chip-info-focus-font-color, $chip-info-avatar-focus-bg-color, $chip-info-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-info-focus-active-bg-color, $chip-info-focus-active-border-color, $chip-info-focus-active-font-color, $chip-info-focus-active-font-color, $chip-info-avatar-focus-active-bg-color, $chip-info-avatar-focus-active-font-color); - box-shadow: $chip-info-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-info-active-bg-color, $chip-info-active-border-color, $chip-info-active-font-color, $chip-info-active-font-color, $chip-info-avatar-active-bg-color, $chip-info-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-info-pressed-bg-color, $chip-info-pressed-border-color, $chip-info-pressed-font-color, $chip-info-pressed-font-color, $chip-info-avatar-pressed-bg-color, $chip-info-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-info-disabled-bg-color, $chip-info-disabled-border-color, $chip-info-disabled-font-color, $chip-info-disabled-font-color, $chip-info-avatar-disabled-bg-color, $chip-info-avatar-disabled-font-color); - } - - &.sf-outline { - @include chip-color($chip-outline-info-bg-color, $chip-outline-info-border-color, $chip-outline-info-font-color, $chip-outline-info-font-color, $chip-outline-info-avatar-bg-color, $chip-outline-info-avatar-font-color, $chip-outline-info-close-icon-font-color); - @include chip-dlt-btn-color($chip-outline-info-close-icon-hover-font-color, $chip-outline-info-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-outline-info-hover-bg-color, $chip-outline-info-hover-border-color, $chip-outline-info-hover-font-color, $chip-outline-info-hover-font-color, $chip-outline-info-avatar-hover-bg-color, $chip-outline-info-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-outline-info-focus-bg-color, $chip-outline-info-focus-border-color, $chip-outline-info-focus-font-color, $chip-outline-info-focus-font-color, $chip-outline-info-avatar-focus-bg-color, $chip-outline-info-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-outline-info-focus-active-bg-color, $chip-outline-info-focus-active-border-color, $chip-outline-info-focus-active-font-color, $chip-outline-info-focus-active-font-color, $chip-outline-info-avatar-focus-active-bg-color, $chip-outline-info-avatar-focus-active-font-color); - box-shadow: $chip-info-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-outline-info-active-bg-color, $chip-outline-info-active-border-color, $chip-outline-info-active-font-color, $chip-outline-info-active-font-color, $chip-outline-info-avatar-active-bg-color, $chip-outline-info-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-outline-info-pressed-bg-color, $chip-outline-info-pressed-border-color, $chip-outline-info-pressed-font-color, $chip-outline-info-pressed-font-color, $chip-outline-info-avatar-pressed-bg-color, $chip-outline-info-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-outline-info-disabled-bg-color, $chip-outline-info-disabled-border-color, $chip-outline-info-disabled-font-color, $chip-outline-info-disabled-font-color, $chip-outline-info-avatar-disabled-bg-color, $chip-outline-info-avatar-disabled-font-color); - } - } - } - - &.sf-warning { - @include chip-color($chip-warning-bg-color, $chip-warning-border-color, $chip-warning-font-color, $chip-warning-font-color, $chip-warning-avatar-bg-color, $chip-warning-avatar-font-color, $chip-warning-close-icon-font-color); - @include chip-dlt-btn-color($chip-warning-close-icon-hover-font-color, $chip-warning-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-warning-hover-bg-color, $chip-warning-hover-border-color, $chip-warning-hover-font-color, $chip-warning-hover-font-color, $chip-warning-avatar-hover-bg-color, $chip-warning-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-warning-focus-bg-color, $chip-warning-focus-border-color, $chip-warning-focus-font-color, $chip-warning-focus-font-color, $chip-warning-avatar-focus-bg-color, $chip-warning-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-warning-focus-active-bg-color, $chip-warning-focus-active-border-color, $chip-warning-focus-active-font-color, $chip-warning-focus-active-font-color, $chip-warning-avatar-focus-active-bg-color, $chip-warning-avatar-focus-active-font-color); - box-shadow: $chip-warning-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-warning-active-bg-color, $chip-warning-active-border-color, $chip-warning-active-font-color, $chip-warning-active-font-color, $chip-warning-avatar-active-bg-color, $chip-warning-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-warning-pressed-bg-color, $chip-warning-pressed-border-color, $chip-warning-pressed-font-color, $chip-warning-pressed-font-color, $chip-warning-avatar-pressed-bg-color, $chip-warning-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-warning-disabled-bg-color, $chip-warning-disabled-border-color, $chip-warning-disabled-font-color, $chip-warning-disabled-font-color, $chip-warning-avatar-disabled-bg-color, $chip-warning-avatar-disabled-font-color); - } - - &.sf-outline { - @include chip-color($chip-outline-warning-bg-color, $chip-outline-warning-border-color, $chip-outline-warning-font-color, $chip-outline-warning-font-color, $chip-outline-warning-avatar-bg-color, $chip-outline-warning-avatar-font-color, $chip-outline-warning-close-icon-font-color); - @include chip-dlt-btn-color($chip-outline-warning-close-icon-hover-font-color, $chip-outline-warning-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-outline-warning-hover-bg-color, $chip-outline-warning-hover-border-color, $chip-outline-warning-hover-font-color, $chip-outline-warning-hover-font-color, $chip-outline-warning-avatar-hover-bg-color, $chip-outline-warning-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-outline-warning-focus-bg-color, $chip-outline-warning-focus-border-color, $chip-outline-warning-focus-font-color, $chip-outline-warning-focus-font-color, $chip-outline-warning-avatar-focus-bg-color, $chip-outline-warning-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-outline-warning-focus-active-bg-color, $chip-outline-warning-focus-active-border-color, $chip-outline-warning-focus-active-font-color, $chip-outline-warning-focus-active-font-color, $chip-outline-warning-avatar-focus-active-bg-color, $chip-outline-warning-avatar-focus-active-font-color); - box-shadow: $chip-warning-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-outline-warning-active-bg-color, $chip-outline-warning-active-border-color, $chip-outline-warning-active-font-color, $chip-outline-warning-active-font-color, $chip-outline-warning-avatar-active-bg-color, $chip-outline-warning-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-outline-warning-pressed-bg-color, $chip-outline-warning-pressed-border-color, $chip-outline-warning-pressed-font-color, $chip-outline-warning-pressed-font-color, $chip-outline-warning-avatar-pressed-bg-color, $chip-outline-warning-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-outline-warning-disabled-bg-color, $chip-outline-warning-disabled-border-color, $chip-outline-warning-disabled-font-color, $chip-outline-warning-disabled-font-color, $chip-outline-warning-avatar-disabled-bg-color, $chip-outline-warning-avatar-disabled-font-color); - } - } - } - - &.sf-danger { - @include chip-color($chip-danger-bg-color, $chip-danger-border-color, $chip-danger-font-color, $chip-danger-font-color, $chip-danger-avatar-bg-color, $chip-danger-avatar-font-color, $chip-danger-close-icon-font-color); - @include chip-dlt-btn-color($chip-danger-close-icon-hover-font-color, $chip-danger-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-danger-hover-bg-color, $chip-danger-hover-border-color, $chip-danger-hover-font-color, $chip-danger-hover-font-color, $chip-danger-avatar-hover-bg-color, $chip-danger-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-danger-focus-bg-color, $chip-danger-focus-border-color, $chip-danger-focus-font-color, $chip-danger-focus-font-color, $chip-danger-avatar-focus-bg-color, $chip-danger-avatar-focus-font-color); - - &.sf-active { - @include chip-color($chip-danger-focus-active-bg-color, $chip-danger-focus-active-border-color, $chip-danger-focus-active-font-color, $chip-danger-focus-active-font-color, $chip-danger-avatar-focus-active-bg-color, $chip-danger-avatar-focus-active-font-color); - box-shadow: $chip-danger-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-danger-active-bg-color, $chip-danger-active-border-color, $chip-danger-active-font-color, $chip-danger-active-font-color, $chip-danger-avatar-active-bg-color, $chip-danger-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-danger-pressed-bg-color, $chip-danger-pressed-border-color, $chip-danger-pressed-font-color, $chip-danger-pressed-font-color, $chip-danger-avatar-pressed-bg-color, $chip-danger-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-danger-disabled-bg-color, $chip-danger-disabled-border-color, $chip-danger-disabled-font-color, $chip-danger-disabled-font-color, $chip-danger-avatar-disabled-bg-color, $chip-danger-avatar-disabled-font-color); - } - - &.sf-outline { - @include chip-color($chip-outline-danger-bg-color, $chip-outline-danger-border-color, $chip-outline-danger-font-color, $chip-outline-danger-font-color, $chip-outline-danger-avatar-bg-color, $chip-outline-danger-avatar-font-color, $chip-outline-danger-close-icon-font-color); - @include chip-dlt-btn-color($chip-outline-danger-close-icon-hover-font-color, $chip-outline-danger-close-icon-pressed-font-color); - - &:hover { - @include chip-color($chip-outline-danger-hover-bg-color, $chip-outline-danger-hover-border-color, $chip-outline-danger-hover-font-color, $chip-outline-danger-hover-font-color, $chip-outline-danger-avatar-hover-bg-color, $chip-outline-danger-avatar-hover-font-color); - } - - &.sf-focused { - @include chip-color($chip-outline-danger-focus-bg-color, $chip-outline-danger-focus-border-color, $chip-outline-danger-focus-font-color, $chip-outline-danger-focus-font-color, $chip-outline-danger-avatar-focus-bg-color, $chip-outline-danger-avatar-focus-font-color); - - &.sf-focused.sf-active { - @include chip-color($chip-outline-danger-focus-active-bg-color, $chip-outline-danger-focus-active-border-color, $chip-outline-danger-focus-active-font-color, $chip-outline-danger-focus-active-font-color, $chip-outline-danger-avatar-focus-active-bg-color, $chip-outline-danger-avatar-focus-active-font-color); - box-shadow: $chip-danger-focus-active-box-shadow; - } - } - - &.sf-active { - @include chip-color($chip-outline-danger-active-bg-color, $chip-outline-danger-active-border-color, $chip-outline-danger-active-font-color, $chip-outline-danger-active-font-color, $chip-outline-danger-avatar-active-bg-color, $chip-outline-danger-avatar-active-font-color); - } - - &:active { - @include chip-color($chip-outline-danger-pressed-bg-color, $chip-outline-danger-pressed-border-color, $chip-outline-danger-pressed-font-color, $chip-outline-danger-pressed-font-color, $chip-outline-danger-avatar-pressed-bg-color, $chip-outline-danger-avatar-pressed-font-color); - } - - &.sf-disabled { - @include chip-color($chip-outline-danger-disabled-bg-color, $chip-outline-danger-disabled-border-color, $chip-outline-danger-disabled-font-color, $chip-outline-danger-disabled-font-color, $chip-outline-danger-avatar-disabled-bg-color, $chip-outline-danger-avatar-disabled-font-color); - } - } - } - } - } -} diff --git a/components/buttons/styles/chips/material3-dark.scss b/components/buttons/styles/chips/material3-dark.scss deleted file mode 100644 index c14a867..0000000 --- a/components/buttons/styles/chips/material3-dark.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'material3-dark-definition.scss'; -@import 'all.scss'; diff --git a/components/buttons/styles/chips/material3.scss b/components/buttons/styles/chips/material3.scss deleted file mode 100644 index 9eeb62e..0000000 --- a/components/buttons/styles/chips/material3.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'material3-definition.scss'; -@import 'all.scss'; diff --git a/components/buttons/styles/floating-action-button/_layout.scss b/components/buttons/styles/floating-action-button/_layout.scss deleted file mode 100644 index e9b9219..0000000 --- a/components/buttons/styles/floating-action-button/_layout.scss +++ /dev/null @@ -1,140 +0,0 @@ -@mixin fab-button-styles($border-radius, $min-height, $min-width, $padding, $icon-font-size) { - border-radius: $border-radius; - min-height: $min-height; - min-width: $min-width; - padding: $padding; - - &.sf-icon-btn { - padding: 0; - } - - .sf-btn-icon { - font-size: $icon-font-size; - } -} - -@include export-module('floating-action-button-layout') { - .sf-fab.sf-btn { - align-items: center; - border-radius: $fab-border-radius; - display: inline-flex; - min-height: $fab-min-height; - min-width: $fab-min-width; - padding: $fab-padding; - position: absolute; - z-index: 100000; - - .sf-btn-icon { - margin-top: 0; - font-size: $fab-icon-font-size; - } - - .sf-btn-icon.sf-icon-left { - width: $btn-icon-width; - } - - &.sf-fab-fixed { - position: fixed; - } - - &.sf-fab-top { - top: $fab-offset; - &.sf-fab-middle { - top: 50%; - transform: translateY(-50%); - &.sf-fab-left.sf-fab-center { - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - } - } - } - - &.sf-fab-bottom { - bottom: $fab-offset; - } - - &.sf-fab-left { - left: $fab-offset; - &.sf-fab-center { - left: 50%; - transform: translateX(-50%); - } - } - - &.sf-fab-right { - right: $fab-offset; - } - } - - .sf-rtl { - &.sf-fab.sf-btn { - &.sf-fab-top { - top: $fab-offset; - &.sf-fab-middle { - top: 50%; - transform: translateY(-50%); - &.sf-fab-right.sf-fab-center { - right: 50%; - top: 50%; - transform: translate(50%, -50%); - } - } - } - - &.sf-fab-bottom { - bottom: $fab-offset; - } - - &.sf-fab-right { - right: $fab-offset; - &.sf-fab-center { - right: 50%; - transform: translateX(50%); - } - } - - &.sf-fab-left { - left: $fab-offset; - } - } - } - - .sf-fab-hidden { - visibility: hidden; - } - - .sf-small.sf-fab.sf-btn, - .sf-small .sf-fab.sf-btn { - @include fab-button-styles($fab-small-border-radius, $fab-small-min-height, $fab-small-min-width, $fab-small-padding, $fab-small-icon-font-size); - } -} - - -@mixin fab-button-styles($border-radius, $min-height, $min-width, $padding, $icon-font-size) { - border-radius: $border-radius; - min-height: $min-height; - min-width: $min-width; - padding: $padding; - - &.sf-icon-btn { - padding: 0; - } - - .sf-btn-icon { - font-size: $icon-font-size; - } -} - -@include export-module('floating-action-button-bigger') { - .sf-large.sf-fab.sf-btn, - .sf-large .sf-fab.sf-btn { - @include fab-button-styles($fab-bigger-border-radius, $fab-bigger-min-height, $fab-bigger-min-width, $fab-bigger-padding, $fab-bigger-icon-font-size); - } - .sf-large.sf-small.sf-fab.sf-btn, - .sf-large.sf-small .sf-fab.sf-btn, - .sf-large .sf-small.sf-fab.sf-btn, - .sf-small .sf-large.sf-fab.sf-btn { - @include fab-button-styles($fab-bigger-small-border-radius, $fab-bigger-small-min-height, $fab-bigger-small-min-width, $fab-bigger-small-padding, $fab-bigger-small-icon-font-size); - } -} diff --git a/components/buttons/styles/floating-action-button/_material3-dark-definition.scss b/components/buttons/styles/floating-action-button/_material3-dark-definition.scss deleted file mode 100644 index 356e259..0000000 --- a/components/buttons/styles/floating-action-button/_material3-dark-definition.scss +++ /dev/null @@ -1 +0,0 @@ -@import './material3-definition.scss'; diff --git a/components/buttons/styles/floating-action-button/_material3-definition.scss b/components/buttons/styles/floating-action-button/_material3-definition.scss deleted file mode 100644 index be763a6..0000000 --- a/components/buttons/styles/floating-action-button/_material3-definition.scss +++ /dev/null @@ -1,27 +0,0 @@ -$fab-offset: 16px !default; - -$fab-border-radius: 12px !default; -$fab-min-height: 40px !default; -$fab-min-width: 40px !default; -$fab-padding: 0 16px !default; -$fab-icon-font-size: 14px !default; -$btn-icon-width: 2em !default; -$fab-small-border-radius: 8px !default; -$fab-small-min-height: 32px !default; -$fab-small-min-width: 32px !default; -$fab-small-padding: 0 12px !default; -$fab-small-icon-font-size: 12px !default; - -$fab-bigger-border-radius: 16px !default; -$fab-bigger-min-height: 56px !default; -$fab-bigger-min-width: 56px !default; -$fab-bigger-padding: 0 19px !default; -$fab-bigger-icon-font-size: 14px !default; - -$fab-bigger-small-border-radius: $fab-bigger-border-radius !default; -$fab-bigger-small-min-height: 48px !default; -$fab-bigger-small-min-width: 48px !default; -$fab-bigger-small-padding: 0 19px !default; -$fab-bigger-small-icon-font-size: 14px !default; - -$fab-box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px rgba(0, 0, 0, .14), 0 1px 18px rgba(0, 0, 0, .12) !default; diff --git a/components/buttons/styles/floating-action-button/_mixin.scss b/components/buttons/styles/floating-action-button/_mixin.scss new file mode 100644 index 0000000..92dad0c --- /dev/null +++ b/components/buttons/styles/floating-action-button/_mixin.scss @@ -0,0 +1,6 @@ +@mixin fab-button-styles($border-radius, $min-height, $min-width, $padding) { + border-radius: $border-radius; + min-height: $min-height; + min-width: $min-width; + padding: $padding; +} \ No newline at end of file diff --git a/components/buttons/styles/floating-action-button/_theme.scss b/components/buttons/styles/floating-action-button/_theme.scss deleted file mode 100644 index 4af4f67..0000000 --- a/components/buttons/styles/floating-action-button/_theme.scss +++ /dev/null @@ -1,24 +0,0 @@ -@include export-module('floating-action-button-theme') { - .sf-fab.sf-btn { - box-shadow: $fab-box-shadow; - - &:hover:not(:focus), - &:active, - &.sf-active, - &:disabled { - box-shadow: $fab-box-shadow; - } - - &:focus { - @if ($skin-name != 'tailwind3' and $skin-name != 'bootstrap5.3') { - box-shadow: $fab-box-shadow; - } - } - } -} - -@if $skin-name == 'tailwind3' { - .sf-fab.sf-btn:focus-visible { - box-shadow: $fab-focus-box-shadow !important; /* stylelint-disable-line declaration-no-important */ - } -} diff --git a/components/buttons/styles/floating-action-button/material3-dark.scss b/components/buttons/styles/floating-action-button/material3-dark.scss deleted file mode 100644 index c14a867..0000000 --- a/components/buttons/styles/floating-action-button/material3-dark.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'material3-dark-definition.scss'; -@import 'all.scss'; diff --git a/components/buttons/styles/material3-dark.scss b/components/buttons/styles/material3-dark.scss deleted file mode 100644 index b1883fe..0000000 --- a/components/buttons/styles/material3-dark.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'button/material3-dark-definition.scss'; -@import 'button/all.scss'; -@import 'check-box/material3-dark-definition.scss'; -@import 'check-box/all.scss'; -@import 'radio-button/material3-dark-definition.scss'; -@import 'radio-button/all.scss'; -@import 'chips/material3-dark-definition.scss'; -@import 'chips/all.scss'; -@import 'floating-action-button/material3-dark-definition.scss'; -@import 'floating-action-button/all.scss'; diff --git a/components/buttons/styles/material3.scss b/components/buttons/styles/material3.scss deleted file mode 100644 index 730f78f..0000000 --- a/components/buttons/styles/material3.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'button/material3-definition.scss'; -@import 'button/all.scss'; -@import 'check-box/material3-definition.scss'; -@import 'check-box/all.scss'; -@import 'radio-button/material3-definition.scss'; -@import 'radio-button/all.scss'; -@import 'chips/material3-definition.scss'; -@import 'chips/all.scss'; -@import 'floating-action-button/material3-definition.scss'; -@import 'floating-action-button/all.scss'; diff --git a/components/buttons/styles/radio-button/_layout.scss b/components/buttons/styles/radio-button/_layout.scss deleted file mode 100644 index b714fa8..0000000 --- a/components/buttons/styles/radio-button/_layout.scss +++ /dev/null @@ -1,838 +0,0 @@ - -@include export-module('radiobutton-layout') { - - .sf-radio-wrapper { - display: inline-block; - line-height: 1; - position: relative; - } - - /* stylelint-disable property-no-vendor-prefix */ - .sf-radio { - -webkit-appearance: none; - height: 1px; - opacity: 0; - position: absolute; - width: 1px; - - &:not(:checked):not(:disabled):hover { - +label.sf-rtl, - +label.sf-right { - &::after { - left: auto; - } - } - } - - &:focus-visible { - +label { - @if $skin-name == 'fluent2' { - box-shadow: $switch-box-shadow; - border-radius: 1px; - } - } - } - - +label { - -webkit-tap-highlight-color: transparent; - cursor: pointer; - display: inline-block; - margin: 0; - position: relative; - user-select: none; - vertical-align: middle; - white-space: nowrap; - @if $skin-name == 'fluent2' { - margin: 7.6px; - } - - &.sf-bottom { - .sf-label { - padding-top: 22px; - padding-left: 0; - } - } - - & .sf-label { - color: $radio-btn-font-color; - display: inline-block; - font-family: $font-family; - font-size: $radio-btn-font-size; - font-weight: normal; - line-height: $radio-btn-line-height; - padding-left: $radio-btn-padding-left; - vertical-align: text-top; - white-space: normal; - @if $skin-name == 'tailwind3' { - font-weight: $font-weight-medium; - } - } - - &:focus, - &.sf-focus { - & .sf-ripple-container { - background-color: $radio-btn-focus-ripple-bgcolor; - } - } - - & .sf-ripple-element { - background-color: $radio-btn-checked-ripple-bgcolor; - } - - &::before { - border: $radio-btn-border; - border-radius: 50%; - box-sizing: border-box; - content: ''; - height: $radio-btn-height; - left: 0; - position: absolute; - width: $radio-btn-width; - background-color: $radio-btn-background-color; - border-color: $radio-btn-border-color; - } - - &:focus { - &::before { - box-shadow: $radio-btn-focussed-box-shadow; - @if $skin-name == 'bootstrap5.3' { - border-color: $border-selected; - } - } - } - - &:active { - &::before { - @if $skin-name == 'bootstrap5.3' { - box-shadow: $radio-btn-focussed-box-shadow; - border-color: $border-selected !important; /* stylelint-disable-line declaration-no-important */ - background-color: $content-bg-color-pressed; - } - @if $skin-name == 'tailwind3' { - box-shadow: $radio-btn-focussed-box-shadow; - } - } - - & .sf-ripple-element { - background-color: $radio-btn-ripple-bgcolor; - } - } - - &::after { - border: 1px solid; - border-radius: 50%; - box-sizing: border-box; - content: ''; - height: $radio-btn-icon-height; - left: $radio-btn-icon-left; - position: absolute; - top: $radio-btn-icon-top; - transform: scale(0); - width: $radio-btn-icon-width; - } - - & .sf-ripple-container { - border-radius: 50%; - height: $radio-btn-ripple-size; - left: $radio-btn-ripple-left-position; - position: absolute; - top: $radio-btn-ripple-top-position; - width: $radio-btn-ripple-size; - z-index: 1; - - & .sf-ripple-element { - @if $skin-name == 'Material3' { - border-radius: 50%; - } - } - } - - &.sf-right, - &.sf-rtl { - & .sf-label { - padding-left: 0; - padding-right: $radio-btn-padding-left; - } - - &::before { - left: auto; - right: 0; - } - - &::after { - left: auto; - right: $radio-btn-icon-right; - } - - & .sf-ripple-container { - left: auto; - right: $radio-btn-ripple-position; - } - } - - &.sf-right { - &.sf-rtl { - & .sf-label { - padding-left: $radio-btn-padding-left; - padding-right: 0; - } - - &::before { - left: 0; - right: auto; - } - - &::after { - left: $radio-btn-icon-right; - right: auto; - } - - & .sf-ripple-container { - left: -8px; - right: auto; - } - } - } - - &.sf-small { - - & .sf-label { - line-height: $radio-btn-small-line-height; - padding-left: $radio-btn-small-padding; - } - - &::before { - height: $radio-btn-small-height; - width: $radio-btn-small-width; - } - - &::after { - height: $radio-btn-small-icon-height; - left: $radio-btn-small-icon-left; - top: $radio-btn-small-icon-top; - width: $radio-btn-small-icon-width; - } - - & .sf-ripple-container { - left: $radio-btn-small-ripple-position; - top: $radio-btn-small-ripple-position; - } - - &.sf-right, - &.sf-rtl { - & .sf-label { - padding-left: 0; - padding-right: $radio-btn-small-padding; - } - - &::after { - left: auto; - right: $radio-btn-small-icon-right; - } - - & .sf-ripple-container { - left: auto; - right: $radio-btn-small-ripple-position; - } - } - - &.sf-right { - &.sf-rtl { - & .sf-label { - padding-left: $radio-btn-small-padding; - padding-right: 0; - } - - &::after { - left: $radio-btn-small-icon-right; - right: auto; - } - - & .sf-ripple-container { - left: -10px; - right: auto; - } - } - } - } - } - - - &:focus { - +label { - &::before { - border-color: $radio-btn-focus-check-border-color; - box-shadow: $radio-btn-focussed-box-shadow; - } - } - } - - &:focus { - +label { - &::before { - border-color: $radio-btn-hover-border-color; - box-shadow: $radio-btn-focussed-box-shadow; - } - } - } - - &:checked { - +label { - &::after { - transform: scale(1); - transition: $radio-btn-check-transition; - } - } - } - - &:hover { - +label { - & .sf-ripple-container { - @if $skin-name == 'Material3' { - background: $radio-btn-ripple-bgcolor; - } - } - } - - +label { - &::before { - border-color: $radio-btn-hover-border-color; - } - } - } - - &:checked { - +label { - &::before { - background-color: $radio-btn-checked-background-color; - border-color: $radio-btn-checked-border-color; - } - - &::after { - background-color: $radio-btn-checked-color; - color: $radio-btn-checked-color; - } - - &:active { - & .sf-ripple-element { - background-color: $radio-btn-checked-ripple-bgcolor; - } - } - } - - +.sf-focus { - & .sf-ripple-container { - background-color: $radio-btn-checked-ripple-bgcolor; - } - - &::before { - outline: $radio-btn-focus-outline; - outline-offset: $radio-btn-focus-outline-offset; - } - } - } - - &:checked { - &:focus { - +label { - &::before { - border-color: $radio-btn-focus-check-border-color; - } - - &::after { - background-color: $radio-btn-focus-check-bg-color; - @if $skin-name == 'fluent2' { - color: $radio-btn-focus-check-bg-color; - } - } - } - } - - +label { - &:hover { - & .sf-ripple-container { - @if $skin-name == 'Material3' { - background: $radio-btn-checked-ripple-bgcolor; - } - } - &::before { - border-color: $radio-btn-hover-check-border-color; - } - - &::after { - background-color: $radio-btn-hover-check-bg-color; - @if $skin-name == 'fluent2' { - color: $radio-btn-hover-check-bg-color; - } - } - } - } - } - - &:disabled { - +label { - cursor: default; - pointer-events: none; - @if $skin-name == 'bootstrap5.3' { - opacity: .5; - } - - &::before { - @if $skin-name == 'bootstrap5.3' or $skin-name == 'tailwind3' { - background-color: $radio-btn-disabled-not-checked-bg-color; - } - @else { - background-color: $radio-btn-disabled-background-color; - } - border-color: $radio-btn-disabled-border-color; - cursor: default; - } - - & .sf-ripple-container { - background-color: transparent; - - &::after { - background-color: transparent; - cursor: default; - } - } - - & .sf-label { - color: $radio-btn-disabled-color; - } - } - - &:checked { - +label { - &::before { - background-color: $radio-btn-disabled-background-color; - border-color: $radio-btn-disabled-checked-border-color; - } - - &::after { - background-color: $radio-btn-disabled-checked-color; - border-color: $radio-btn-disabled-checked-color; - cursor: default; - } - - & .sf-ripple-container, - & .sf-ripple-container::after { - background-color: transparent; - } - } - } - } - } - - .sf-small .sf-radio + label, - .sf-radio + label.sf-small { - @if $skin-name == 'fluent2' { - margin: 5.6px; - } - & .sf-label { - line-height: $radio-btn-small-line-height; - padding-left: $radio-btn-small-padding; - font-size: 12px; - @if $skin-name == 'fluent2' or $skin-name == 'tailwind3' { - font-size: 12px; - } - } - - &::before { - height: $radio-btn-small-height; - width: $radio-btn-small-width; - } - - &.sf-bottom { - .sf-label { - padding-top: 22px; - padding-left: 0; - } - } - - &::after { - height: $radio-btn-small-icon-height; - left: $radio-btn-small-icon-left; - top: $radio-btn-small-icon-top; - width: $radio-btn-small-icon-width; - } - - & .sf-ripple-container { - width: $radio-btn-small-ripple-Width; - height: $radio-btn-small-ripple-height; - left: $radio-btn-small-ripple-position; - top: $radio-btn-small-ripple-position; - } - - &.sf-right, - &.sf-rtl { - & .sf-label { - padding-left: 0; - padding-right: $radio-btn-small-padding; - } - - &::after { - left: auto; - right: $radio-btn-small-icon-right; - } - - & .sf-ripple-container { - left: auto; - right: $radio-btn-small-ripple-position; - } - } - - &.sf-right { - &.sf-rtl { - & .sf-label { - padding-left: $radio-btn-small-padding; - padding-right: 0; - } - - &::after { - left: $radio-btn-small-icon-right; - right: auto; - } - - & .sf-ripple-container { - left: -10px; - right: auto; - } - } - } - } - - - .sf-large.sf-small .sf-radio + label, - .sf-radio + label.sf-large.sf-small { - @if $skin-name == 'fluent2' { - margin: 8.4px; - } - & .sf-label { - line-height: $radio-btn-bigger-small-line-height; - padding-left: $radio-btn-bigger-small-padding; - } - - &::before { - height: $radio-btn-bigger-small-height; - width: $radio-btn-bigger-small-width; - } - - &::after { - height: $radio-btn-bigger-small-icon-height; - left: $radio-btn-bigger-small-icon-left; - top: $radio-btn-bigger-small-icon-top; - width: $radio-btn-bigger-small-icon-width; - } - - & .sf-ripple-container { - height: $radio-btn-bigger-small-ripple-size; - left: $radio-btn-bigger-ripple-position; - top: $radio-btn-bigger-ripple-position; - width: $radio-btn-bigger-small-ripple-size; - } - - &.sf-right, - &.sf-rtl { - & .sf-label { - padding-left: 0; - padding-right: $radio-btn-bigger-small-padding; - } - - &::after { - left: auto; - right: $radio-btn-bigger-small-icon-right; - } - - & .sf-ripple-container { - left: auto; - right: $radio-btn-small-ripple-position; - } - } - - &.sf-right { - &.sf-rtl { - & .sf-label { - padding-left: $radio-btn-bigger-small-padding; - padding-right: 0; - } - - &::after { - left: $radio-btn-bigger-small-icon-right; - right: auto; - } - - & .sf-ripple-container { - left: -10px; - right: auto; - } - } - } - } - - .sf-large .sf-radio { - &:not(:checked):hover { - +label, - +label.sf-rtl, - +label.sf-right { - @if $skin-name == 'FluentUI' { - &::after { - background-color: $radio-btn-uncheck-background-color; - border: 1px solid; - border-radius: 50%; - box-sizing: border-box; - color: $radio-btn-uncheck-color; - content: ''; - height: $radio-btn-bigger-icon-height; - left: $radio-btn-bigger-icon-left; - position: absolute; - top: $radio-btn-bigger-icon-top; - transform: scale(1); - width: $radio-btn-bigger-icon-width; - } - } - } - - +label.sf-rtl, - +label.sf-right { - &::after { - left: auto; - } - } - } - - } - - .sf-large .sf-radio + label { - @if $skin-name == 'FluentUI' { - border: 1px solid transparent; - height: 28px; - } - } - - .sf-large .sf-radio + label, - .sf-radio + label.sf-large { - @if $skin-name == 'fluent2' { - margin: 10.4px; - } - - & .sf-label { - font-size: $radio-btn-bigger-font-size; - line-height: $radio-btn-bigger-line-height; - padding-left: $radio-btn-bigger-padding; - } - - &::before { - height: $radio-btn-bigger-height; - width: $radio-btn-bigger-width; - } - - &.sf-bottom { - .sf-label { - padding-top: 22px; - padding-left: 0; - } - } - - &::after { - height: $radio-btn-bigger-icon-height; - left: $radio-btn-bigger-icon-left; - top: $radio-btn-bigger-icon-top; - width: $radio-btn-bigger-icon-width; - } - - & .sf-ripple-container { - height: $radio-btn-bigger-ripple-size; - left: $radio-btn-bigger-ripple-left-position; - top: $radio-btn-bigger-ripple-top-position; - width: $radio-btn-bigger-ripple-size; - } - - &.sf-right, - &.sf-rtl { - & .sf-label { - padding-left: 0; - padding-right: $radio-btn-bigger-padding; - } - - &::after { - left: auto; - right: $radio-btn-bigger-icon-right; - } - - & .sf-ripple-container { - left: auto; - right: $radio-btn-bigger-ripple-position; - } - } - - &.sf-right { - &.sf-rtl { - & .sf-label { - padding-left: $radio-btn-bigger-padding; - padding-right: 0; - } - - &::after { - left: $radio-btn-bigger-icon-right; - right: auto; - } - - & .sf-ripple-container { - left: -12px; - right: auto; - } - } - } - - &.sf-small { - & .sf-label { - line-height: $radio-btn-bigger-small-line-height; - padding-left: $radio-btn-bigger-small-padding; - } - - &::before { - height: $radio-btn-bigger-small-height; - width: $radio-btn-bigger-small-width; - } - - &::after { - height: $radio-btn-bigger-small-icon-height; - left: $radio-btn-bigger-small-icon-left; - top: $radio-btn-bigger-small-icon-top; - width: $radio-btn-bigger-small-icon-width; - } - - & .sf-ripple-container { - height: $radio-btn-bigger-small-ripple-size; - left: $radio-btn-bigger-ripple-position; - top: $radio-btn-bigger-ripple-position; - width: $radio-btn-bigger-small-ripple-size; - } - - &.sf-right, - &.sf-rtl { - & .sf-label { - padding-left: 0; - padding-right: $radio-btn-bigger-small-padding; - } - - &::after { - left: auto; - right: $radio-btn-bigger-small-icon-right; - } - - & .sf-ripple-container { - left: auto; - right: $radio-btn-small-ripple-position; - } - } - - &.sf-right { - &.sf-rtl { - & .sf-label { - padding-left: $radio-btn-bigger-small-padding; - padding-right: 0; - } - - &::after { - left: $radio-btn-bigger-small-icon-right; - right: auto; - } - - & .sf-ripple-container { - left: -10px; - right: auto; - } - } - } - } - } - - .sf-success .sf-radio:checked + label::after { /* csslint allow: adjoining-classes */ - background-color: #689f38; - border-color: #689f38; - } - - .sf-success .sf-radio:checked:focus + label::after, .sf-success .sf-radio:checked + label:hover::after { /* csslint allow: adjoining-classes */ - background-color: #449d44; - border-color: #449d44; - } - - .sf-success .sf-radio:checked + label::before { - border-color: #689f38; - } - - .sf-success .sf-radio:checked:focus + label::before, .sf-success .sf-radio:checked + label:hover::before { /* csslint allow: adjoining-classes */ - border-color: #449d44; - } - - .sf-success .sf-radio +label:hover::before { - border-color: #b1afaf - } - - .sf-info .sf-radio:checked + label::after { /* csslint allow: adjoining-classes */ - background-color: #2196f3; - border-color: #2196f3; - } - - .sf-info .sf-radio:checked:focus + label::after, .sf-info .sf-radio:checked + label:hover::after { /* csslint allow: adjoining-classes */ - background-color: #0b7dda; - border-color: #0b7dda; - } - - .sf-info .sf-radio:checked + label::before { - border-color: #2196f3; - } - - .sf-info .sf-radio:checked:focus + label::before, .sf-info .sf-radio:checked + label:hover::before { - border-color: #0b7dda; - } - - .sf-info .sf-radio + label:hover::before { - border-color: #b1afaf - } - - .sf-warning .sf-radio:checked + label::after { /* csslint allow: adjoining-classes */ - background-color: #ef6c00; - border-color: #ef6c00; - } - - .sf-warning .sf-radio:checked:focus + label::after, .sf-warning .sf-radio:checked + label:hover::after { /* csslint allow: adjoining-classes */ - background-color: #cc5c00; - } - - .sf-radio:checked + .sf-warning::before { - border-color: #ef6c00; - } - - .sf-warning .sf-radio:checked:focus + label::before, .sf-warning .sf-radio:checked + label:hover::before { - border-color: #cc5c00; - } - - .sf-warning .sf-radio + label:hover::before { - border-color: #b1afaf - } - - .sf-danger .sf-radio:checked + label::after { /* csslint allow: adjoining-classes */ - background-color: #d84315; - border-color: #d84315; - } - - .sf-danger .sf-radio:checked:focus + label::after, .sf-danger .sf-radio:checked + label:hover::after { /* csslint allow: adjoining-classes */ - background-color: #ba330a; - border-color: #ba330a; - } - - .sf-danger .sf-radio:checked + label::before { - border-color: #d84315; - } - - .sf-danger .sf-radio:checked:focus + label::before, .sf-danger .sf-radio:checked + label:hover::before { - border-color: #ba330a; - } - - .sf-danger .sf-radio + label:hover::before { - border-color: #b1afaf - } -} diff --git a/components/buttons/styles/radio-button/_material3-dark-definition.scss b/components/buttons/styles/radio-button/_material3-dark-definition.scss deleted file mode 100644 index 356e259..0000000 --- a/components/buttons/styles/radio-button/_material3-dark-definition.scss +++ /dev/null @@ -1 +0,0 @@ -@import './material3-definition.scss'; diff --git a/components/buttons/styles/radio-button/_material3-definition.scss b/components/buttons/styles/radio-button/_material3-definition.scss deleted file mode 100644 index bafe04a..0000000 --- a/components/buttons/styles/radio-button/_material3-definition.scss +++ /dev/null @@ -1,75 +0,0 @@ -$radio-btn-border: 1px solid !default; -$radio-btn-bigger-font-size: 16px !default; -$radio-btn-bigger-height: 20px !default; -$radio-btn-bigger-line-height: 1.2 !default; -$radio-btn-bigger-padding: 28px !default; -$radio-btn-bigger-small-height: 18px !default; -$radio-btn-bigger-small-line-height: 1 !default; -$radio-btn-bigger-small-padding: 28px !default; -$radio-btn-bigger-small-width: 18px !default; -$radio-btn-bigger-width: 20px !default; -$radio-btn-bigger-ripple-position: -10px !default; -$radio-btn-bigger-ripple-left-position: -9px !default; -$radio-btn-bigger-ripple-top-position: -8px !default; -$radio-btn-bigger-ripple-size: 36px !default; -$radio-btn-bigger-small-ripple-size: 36px !default; -$radio-btn-height: 16px !default; -$radio-btn-width: 16px !default; -$radio-btn-small-height: 14px !default; -$radio-btn-small-width: 14px !default; -$radio-btn-icon-left: 5px !default; -$radio-btn-icon-top: 5px !default; -$radio-btn-icon-right: 5px !default; -$radio-btn-ripple-position: -8px !default; -$radio-btn-ripple-left-position: -9px !default; -$radio-btn-ripple-top-position: -8px !default; -$radio-btn-ripple-size: 32px !default; -$radio-btn-small-icon-left: 4px !default; -$radio-btn-small-icon-top: 4px !default; -$radio-btn-small-icon-right: 4px !default; -$radio-btn-small-ripple-position: -4px !default; -$radio-btn-icon-height: 6px !default; -$radio-btn-icon-width: 6px !default; -$radio-btn-small-icon-height: 6px !default; -$radio-btn-small-icon-width: 6px !default; -$radio-btn-line-height: 1 !default; -$radio-btn-padding-left: 22px !default; -$radio-btn-small-line-height: 1 !default; -$radio-btn-small-padding: 22px !default; -$radio-btn-focus-outline-offset: 0 !default; -$radio-btn-font-size: 14px !default; -$radio-btn-background-color: $transparent !default; -$radio-btn-border-color: rgba($content-text-color-alt1) !default; -$radio-btn-checked-border-color: rgba($primary) !default; -$radio-btn-checked-color: rgba($primary) !default; -$radio-btn-checked-background-color: $transparent !default; -$radio-btn-checked-ripple-bgcolor: rgba($primary, .08) !default; -$radio-btn-check-transition: none !default; -$radio-btn-disabled-border-color: rgba($content-text-color, .38) !default; -$radio-btn-disabled-checked-border-color: rgba($content-text-color, .38) !default; -$radio-btn-disabled-background-color: transparent !default; -$radio-btn-disabled-color: $content-text-color-disabled !default; -$radio-btn-disabled-checked-color: rgba($content-text-color, .38) !default; -$radio-btn-font-color: rgba($content-text-color) !default; -$radio-btn-focus-ripple-bgcolor: rgba($content-text-color, .12) !default; -$radio-btn-focussed-box-shadow: none !default; -$radio-btn-hover-bgcolor: $transparent !default; -$radio-btn-hover-border-color: rgba($content-text-color) !default; -$radio-btn-hover-check-bg-color: rgba($primary) !default; -$radio-btn-hover-check-border-color: rgba($primary) !default; -$radio-btn-ripple-bgcolor: rgba($content-text-color, .08) !default; -$radio-btn-focus-check-bg-color: rgba($primary) !default; -$radio-btn-focus-check-border-color: rgba($primary) !default; -$radio-btn-focus-outline: $radio-btn-background-color 0 solid !default; -$radio-btn-bigger-small-icon-height: 8px !default; -$radio-btn-bigger-small-icon-width: 8px !default; -$radio-btn-bigger-small-icon-left: 4px !default; -$radio-btn-bigger-small-icon-top: 4px !default; -$radio-btn-bigger-small-icon-right: 4px !default; -$radio-btn-bigger-icon-height: 8px !default; -$radio-btn-bigger-icon-width: 8px !default; -$radio-btn-bigger-icon-left: 6px !default; -$radio-btn-bigger-icon-top: 6px !default; -$radio-btn-bigger-icon-right: 4px !default; -$radio-btn-small-ripple-Width: 22px !default; -$radio-btn-small-ripple-height: 22px !default; \ No newline at end of file diff --git a/components/buttons/styles/radio-button/material3-dark.scss b/components/buttons/styles/radio-button/material3-dark.scss deleted file mode 100644 index c14a867..0000000 --- a/components/buttons/styles/radio-button/material3-dark.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'material3-dark-definition.scss'; -@import 'all.scss'; diff --git a/components/buttons/styles/radio-button/material3.scss b/components/buttons/styles/radio-button/material3.scss deleted file mode 100644 index 9eeb62e..0000000 --- a/components/buttons/styles/radio-button/material3.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'material3-definition.scss'; -@import 'all.scss'; diff --git a/components/base/CHANGELOG.md b/components/calendars/CHANGELOG.md similarity index 100% rename from components/base/CHANGELOG.md rename to components/calendars/CHANGELOG.md diff --git a/components/calendars/README.md b/components/calendars/README.md new file mode 100644 index 0000000..e8828e6 --- /dev/null +++ b/components/calendars/README.md @@ -0,0 +1,74 @@ +# React Calendars Components + +## What's Included in the React Calendar Package + +The React Calendar package includes the following list of components. + +### React Calendar + +The Calendar component provides a versatile and interactive date selection interface with support for multiple views, customizations, and selection modes. It is designed to handle a wide range of scheduling and planning use cases. + +Explore the demo [here](https://react.syncfusion.com/calendar). + +**Key features** + +- **View Modes:** Switch between month, year, and decade views using the start and depth properties to control the initial view and navigation depth. + +- **Date Range Control:** Restrict selectable dates using minDate and maxDate to ensure users can only choose valid dates within a defined range. + +- **Custom Cell Templates:** Use the cellTemplate property to customize the appearance of specific dates, including disabling dates to prevent selection. + +- **Week Number Display:** Enable the weekNumber property to show week numbers alongside calendar dates for better context in scheduling. + +- **Multi-Date Selection:** Activate the multiSelect property to allow users to select multiple non-consecutive dates, ideal for marking events or selecting custom date ranges. + +### React DatePicker + +The DatePicker component offers a streamlined and customizable interface for selecting dates, supporting various formats, view modes, and styling options. It is ideal for forms, scheduling tools, and any application requiring precise date input. + +Explore the demo [here](https://react.syncfusion.com/date-picker). + +**Key features** + +- **Custom Date Formats:** Display and receive date values in formats that suit your application using the format property, which supports standard date format patterns. + +- **Read-Only Mode:** Prevent user edits while displaying a selected date by enabling the readOnly property—useful for forms or scenarios where the date is system-generated. + +- **View Modes:** Navigate through Month, Year, and Decade views to provide flexible date selection experiences. + +- **Custom Cell Templates:** Highlight important dates, events, or special occasions using the cellTemplate property to apply custom styling, icons, or indicators. + +

+Trusted by the world's leading companies + + Syncfusion logo + +

+ +## Setup + +To install `Calendars` and its dependent packages, use the following command, + +```sh +npm install @syncfusion/react-calendars +``` + +## Support + +Product support is available through following mediums. + +* [Support ticket](https://support.syncfusion.com/support/tickets/create) - Guaranteed Response in 24 hours | Unlimited tickets | Holiday support +* Live chat + +## Changelog +Check the changelog [here](https://github.com/syncfusion/react-ui-components/blob/master/components/calendars/CHANGELOG.md). Get minor improvements and bug fixes every week to stay up to date with frequent updates. + +## License and copyright + +> This is a commercial product and requires a paid license for possession or use. Syncfusion’s licensed software, including this component, is subject to the terms and conditions of Syncfusion's [EULA](https://www.syncfusion.com/eula/es/). To acquire a license for [React UI components](https://www.syncfusion.com/react-components), you can [purchase](https://www.syncfusion.com/sales/products) or [start a free 30-day trial](https://www.syncfusion.com/account/manage-trials/start-trials). + +> A [free community license](https://www.syncfusion.com/products/communitylicense) is also available for companies and individuals whose organizations have less than $1 million USD in annual gross revenue and five or fewer developers. + +See [LICENSE FILE](https://github.com/syncfusion/react-ui-components/blob/master/license?utm_source=npm&utm_campaign=notification) for more info. + +© Copyright 2025 Syncfusion, Inc. All Rights Reserved. The Syncfusion Essential Studio license and copyright applies to this distribution. diff --git a/components/base/gulpfile.js b/components/calendars/gulpfile.js similarity index 100% rename from components/base/gulpfile.js rename to components/calendars/gulpfile.js diff --git a/components/buttons/license b/components/calendars/license similarity index 100% rename from components/buttons/license rename to components/calendars/license diff --git a/components/navigations/package.json b/components/calendars/package.json similarity index 56% rename from components/navigations/package.json rename to components/calendars/package.json index aa50a92..e6c0a9d 100644 --- a/components/navigations/package.json +++ b/components/calendars/package.json @@ -1,7 +1,7 @@ { - "name": "@syncfusion/react-navigations", - "version": "30.1.37", - "description": "A package of Pure React navigation components such as Toolbar and Context-menu which is used to navigate from one page to another", + "name": "@syncfusion/react-calendars", + "version": "31.1.17", + "description": "A complete package of date or time components with built-in features such as date formatting, inline editing, multiple (range) selection, range restriction, month and year selection, strict mode, and globalization.", "author": "Syncfusion Inc.", "license": "SEE LICENSE IN license", "keywords": [ @@ -9,9 +9,14 @@ "web-components", "react", "syncfusion-react", - "react-navigations", - "toolbar", - "context-menu" + "react-calendars", + "calendar", + "date", + "culture", + "month", + "year", + "decade", + "time" ], "repository": { "type": "git", @@ -21,11 +26,11 @@ "module": "./index.js", "readme": "README.md", "dependencies": { - "@syncfusion/react-base": "~30.1.37", - "@syncfusion/react-icons": "~30.1.37", - "@syncfusion/react-buttons": "~30.1.37", - "@syncfusion/react-inputs": "~30.1.37", - "@syncfusion/react-popups": "~30.1.37" + "@syncfusion/react-base": "~31.1.17", + "@syncfusion/react-buttons": "~31.1.17", + "@syncfusion/react-inputs": "~31.1.17", + "@syncfusion/react-popups": "~31.1.17", + "@syncfusion/react-icons": "~31.1.17" }, "devDependencies": { "gulp": "4.0.2", diff --git a/components/calendars/src/calendar/calendar-cell.tsx b/components/calendars/src/calendar/calendar-cell.tsx new file mode 100644 index 0000000..59814bf --- /dev/null +++ b/components/calendars/src/calendar/calendar-cell.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import { forwardRef } from 'react'; + +/** + * Defines the props for the CalendarCell component. + * + */ +export interface CalendarCellProps { + /** + * Sets a custom CSS class to the calendar cell. + * + * @default string + */ + className?: string; + + /** + * Determines if the calendar cell is disabled and non-interactive. + * + * @default false + */ + isDisabled?: boolean; + + /** + * Specifies if the cell is out of current view range (like previous or next month dates in month view). + * + */ + isOutOfRange?: boolean; + + /** + * Specifies if the current cell represents today's date. + * + * @default false + */ + isToday?: boolean; + + /** + * Specifies if the current cell is selected. + * + * @default false + */ + isSelected?: boolean; + + /** + * Specifies if the current cell has focus. + * + * @default false + */ + isFocused?: boolean; + + /** + * Specifies if the cell represents a weekend day. + * + * @default false + */ + isWeekend?: boolean; + + /** + * The date value represented by the cell. + * + */ + date: Date; + + /** + * The content to be displayed inside the cell. + * + * @default null + */ + children?: React.ReactNode; + + /** + * Callback function triggered when a cell is clicked. + * + * @default null + */ + onClick?: (event: React.MouseEvent, date: Date) => void; + + /** + * The id attribute for the cell. + * + * @default - + */ + id?: string; + + /** + * Optional title attribute for the cell. + * + * @default - + */ + title?: string; +} + +/** + * The CalendarCell component is internally used for rendering the items in the current view. + * It can also be used as a custom cell of the Calendar. + */ +export const CalendarCell: React.ForwardRefExoticComponent> = +forwardRef((props: CalendarCellProps, ref: React.Ref) => { + const { + className, + isDisabled = false, + isOutOfRange = false, + isToday = false, + isSelected = false, + isFocused = false, + isWeekend = false, + date, + children, + onClick, + id, + title + } = props; + + const baseClasses: string[] = [ + 'sf-cell', + className || '', + isOutOfRange ? 'sf-other-month' : '', + isWeekend ? 'sf-weekend' : '', + (isDisabled) ? 'sf-disabled sf-overlay' : '', + isToday ? 'sf-today' : '', + (isToday && isFocused) ? 'sf-focused-date' : '', + (isFocused) ? 'sf-focused-date' : '', + isSelected ? 'sf-selected' : '' + ].filter(Boolean); + + const handleClick: (e: React.MouseEvent) => void = (e: React.MouseEvent) => { + if (!isDisabled && onClick) { + onClick(e, date); + } + }; + + return ( + + {children} + + ); +}); + +export default CalendarCell; diff --git a/components/calendars/src/calendar/calendar.tsx b/components/calendars/src/calendar/calendar.tsx new file mode 100644 index 0000000..88ad1f1 --- /dev/null +++ b/components/calendars/src/calendar/calendar.tsx @@ -0,0 +1,1588 @@ +import { useEffect, useRef, useState, useImperativeHandle, forwardRef, isValidElement, JSX, Ref, useCallback } from 'react'; +import { IL10n, L10n, formatDate, parseDate, useProviderContext } from '@syncfusion/react-base'; +import { getValue, getDefaultDateObject, isNullOrUndefined, cldrData, preRender } from '@syncfusion/react-base'; +import * as React from 'react'; +import { Button, Color, Variant } from '@syncfusion/react-buttons'; +import { addMonths, addYears, getWeekNumber } from '../../src/utils/calendar-util'; +import { ChevronLeftIcon, ChevronRightIcon, CloseIcon } from '@syncfusion/react-icons'; +import { CalendarCell, CalendarCellProps } from './calendar-cell'; + +/** + * Specifies the view options for the calendar. + * + * @enum {string} + */ +export enum CalendarView { + /** + * Displays the calendar by month. + * + */ + Month = 'Month', + + /** + * Displays the calendar by year. + * + */ + Year = 'Year', + + /** + * Displays the calendar by decade. + * + */ + Decade = 'Decade' +} + +/** + * Defines the event arguments for the Change event. + */ +export interface ChangeEvent { + /** + * The selected time value. + * + */ + value: Date| Date[] | null; + /** + * The original event object. + * + */ + event?: React.MouseEvent | React.FocusEvent | React.KeyboardEvent; +} + +/** + * Specifies the Type of the calendar. + * + */ +export type CalendarType = 'Gregorian'; + +/** + * Specifies the format of the day to be displayed in the header. + * + * @enum {string} + */ +export enum WeekDaysFormats { + /** + * Short format, typically a single letter. + * + */ + Short = 'Short', + + /** + * Narrow format, usually a minimal abbreviation. + * + */ + Narrow = 'Narrow', + + /** + * Abbreviated format, a shortened form of the day name. + * + */ + Abbreviated = 'Abbreviated', + + /** + * Wide format, the full name of the day. + * + */ + Wide = 'Wide' +} + +/** + * Specifies the rules used to determine which week is considered + * the first week of a calendar year. + * + */ +export enum WeekRule { + /** + * The first week begins on January first, regardless of + * which day of the week it falls on. + * + */ + FirstDay = 'FirstDay', + + /** + * The first week begins on the first occurrence of the + * designated weekday on or after January first. + * + */ + FirstFullWeek = 'FirstFullWeek', + + /** + * The first week must contain at least four days of the new year, + * following the ISO standard definition. + * + */ + FirstFourDayWeek = 'FirstFourDayWeek' +} + +export interface ViewChangeEvent { + /** + * Specifies the current view of the Calendar. + * + * @default null + */ + view?: string + + /** + * Specifies the focused date in a view. + * + * @default null + */ + date?: Date + + /** + * Specifies the original event arguments. + * + * @default null + */ + event?: React.MouseEvent | React.KeyboardEvent +} + +export interface CalendarProps { + /** + * Specifies the selected date of the Calendar. + * + * @default null + */ + value?: Date | Date[] | null; + + /** + * Specifies the default selected date of the Calendar for uncontrolled mode. + * + * @default null + */ + defaultValue?: Date | Date[] | null; + + /** + * Specifies the option to enable the multiple dates selection of the calendar. + * + * @default false + */ + multiSelect?: boolean; + + /** + * Specifies the minimum date that can be selected in the Calendar. + * + * @default new Date(1900, 00, 01) + */ + minDate?: Date; + + /** + * Specifies the maximum date that can be selected in the Calendar. + * + * @default new Date(2099, 11, 31) + */ + maxDate?: Date; + + /** + * Specifies the first day of the week for the calendar. By default, the first day of the week will be determined by the current culture. + * + * @default 0 + */ + firstDayOfWeek?: number; + + /** + * Specifies the initial view of the Calendar when it is opened. + * + * @default Month + */ + start?: CalendarView; + + /** + * Sets the maximum level of view such as month, year, and decade in the Calendar. + * Depth view should be smaller than the start view to restrict its view navigation. + * + * @default Month + */ + depth?: CalendarView; + + /** + * Specifies whether the week number of the year is to be displayed in the calendar or not. + * + * @default false + */ + weekNumber?: boolean; + + /** + * Specifies the component to be disabled or not. + * + * @default true + */ + disabled?: boolean; + + /** + * Specifies whether the Calendar should be in read-only mode. + * + * @default false + */ + readOnly?: boolean; + + /** + * Specifies the rule for defining the first week of the year. + * + * @default FirstDay + */ + weekRule?: WeekRule; + + /** + * Specifies whether the today button is to be displayed or not. + * + * @default true + */ + showTodayButton?: boolean; + + /** + * Specifies the format of the day that to be displayed in header. By default, the format is 'short'. + * Possible formats are: + * * `Short` - Sets the short format of day name (like Su ) in day header. + * * `Narrow` - Sets the single character of day name (like S ) in day header. + * * `Abbreviated` - Sets the min format of day name (like Sun ) in day header. + * * `Wide` - Sets the long format of day name (like Sunday ) in day header. + * + * @default Short + */ + weekDaysFormat?: WeekDaysFormats; + + /** + * Provides a template for rendering custom content for each day cell in the calendar. + * Can be a function that accepts CalendarCellProps, a function that accepts date parameters, + * or a React element. + * + * @default null + */ + cellTemplate?: Function | React.ReactNode; + + /** + * Specifies whether the calendar is displayed in full screen mode. + * Used by DatePicker for mobile view. + * + * @default false + */ + fullScreenMode?: boolean; + + /** + * Triggers when the Calendar value is changed. + * + * @event onChange + */ + onChange?: ((args: ChangeEvent) => void); + + /** + * Triggers when the Calendar is navigated to another level or within the same level of view. + * + * @event onViewChange + */ + onViewChange?: (args: ViewChangeEvent) => void; +} +export interface ICalendar extends CalendarProps { + /** + * The content to be rendered inside the component. + * + * @private + * @default null + */ + element?: HTMLDivElement | null; +} + +type ICalendarProps = ICalendar & Omit, 'defaultValue' | 'value' | 'onChange'>; + +export const Calendar: React.ForwardRefExoticComponent> = + forwardRef((props: ICalendarProps, ref: Ref) => { + const { + className = '', + minDate = new Date(1900, 0, 1), + maxDate = new Date(2099, 11, 31), + depth = CalendarView.Month, + start = CalendarView.Month, + firstDayOfWeek: firstDayOfWeekProp = 0, + value, + defaultValue, + readOnly = false, + weekNumber = false, + weekRule = WeekRule.FirstDay, + showTodayButton = true, + weekDaysFormat = WeekDaysFormats.Short, + disabled = false, + multiSelect = false, + onViewChange, + cellTemplate = null, + onChange, + fullScreenMode = false, + ...otherProps + } = props; + const isControlled: boolean = value !== undefined; + const { locale } = useProviderContext(); + const localeStrings: object = { + today: 'Today' + }; + const [currentView, setCurrentView] = useState(start); + const [isImproperDateRange, setIsImproperDateRange] = useState(false); + const [isTodayDisabled, setIsTodayDisabled] = useState(false); + const [isPreviousDisabled, setIsPreviousDisabled] = useState(false); + const [isNextDisabled, setIsNextDisabled] = useState(false); + const [focusedActiveDecendent, setFocusedActiveDecendent] = useState(''); + const [onSelection, setOnSelection] = useState(false); + const [headerTitle, setheaderTitle] = useState(''); + const firstDayOfWeek: number = firstDayOfWeekProp !== null ? (firstDayOfWeekProp > 6 ? 0 : firstDayOfWeekProp) : 0; + const calendarElement: React.RefObject = useRef(null); + const headerTitleElement: React.RefObject = useRef(null); + const focusedElementRef: React.RefObject = useRef(null); + const selectedElementRef: React.RefObject = useRef(null); + const l10nInstance: IL10n = L10n('calendar', localeStrings, locale || 'en-US'); + const [isHeaderFocused, setIsHeaderFocused] = useState(false); + let startDecadeHdYr: string = ''; + let endDecadeHdYr: string = ''; + const { dir } = useProviderContext(); + const [internalValue, setInternalValue] = useState(() => { + if (isControlled) { + return null; + } + if (defaultValue !== undefined) { + return defaultValue; + } + if (multiSelect) { + return []; + } + return null; + }); + const currentValue: Date | Date[] | null | undefined = isControlled ? value : internalValue; + const normalizedDates: Date[] = React.useMemo(() => { + if (multiSelect && Array.isArray(currentValue)) { + return currentValue.filter( + (d: Date) => + (!minDate || d >= minDate) && + (!maxDate || d <= maxDate) + ); + } + if (!multiSelect && currentValue && !Array.isArray(currentValue)) { + if (minDate && currentValue < minDate) { + return [minDate]; + } + if (maxDate && currentValue > maxDate) { + return [maxDate]; + } + if ( + (!minDate || currentValue >= minDate) && + (!maxDate || currentValue <= maxDate) + ) { + return [currentValue]; + } + } + return []; + }, [currentValue, minDate, maxDate, multiSelect]); + + const [currentDate, setCurrentDate] = useState(() => { + if (normalizedDates.length > 0) { + if (multiSelect) { + return normalizedDates[normalizedDates.length - 1]; + } else { + return normalizedDates[0]; + } + } + if (!isControlled && defaultValue) { + if (Array.isArray(defaultValue) && defaultValue.length > 0) { + return defaultValue[defaultValue.length - 1]; + } else if (!Array.isArray(defaultValue)) { + return defaultValue; + } + } + const today: Date = new Date(); + return today < minDate ? new Date(minDate) : today > maxDate ? new Date(maxDate) : today; + }); + + useEffect(() => { + setCurrentView(start); + }, [start]); + + const updateValue: ( + newValue: Date | Date[] | null, + event?: React.MouseEvent | React.FocusEvent | React.KeyboardEvent + ) => void = useCallback(( + newValue: Date | Date[] | null, + event?: React.MouseEvent | React.FocusEvent | React.KeyboardEvent + ): void => { + if (!isControlled) { + setInternalValue(newValue); + } + if (onChange) { + onChange({ value: newValue, event }); + } + }, [isControlled, onChange]); + + const publicAPI: Partial = { + value: currentValue, + multiSelect, + minDate, + maxDate, + firstDayOfWeek, + start, + depth, + weekNumber, + disabled, + weekRule, + showTodayButton, + weekDaysFormat, + cellTemplate + }; + + useEffect(() => { + preRender('calendar'); + }, []); + + useEffect(() => { + if (currentView === CalendarView.Month) { + titleUpdate(currentDate, 'days'); + } else if (currentView === CalendarView.Year) { + titleUpdate(currentDate, 'months'); + } else { + setheaderTitle(`${startDecadeHdYr} - ${endDecadeHdYr}`); + } + setIsImproperDateRange(minDate > maxDate); + const today: Date = new Date(); + today.setHours(0, 0, 0, 0); + setIsTodayDisabled(today < minDate || today > maxDate); + iconHandler(); + }, [currentDate, currentView, minDate, maxDate, onSelection]); + + useEffect(() => { + if (focusedElementRef.current) { + setFocusedActiveDecendent(focusedElementRef.current.id); + } + }, [focusedElementRef.current]); + + const iconHandler: () => void = (): void => { + const current: Date = new Date(currentDate); + current.setDate(1); + switch (currentView) { + case CalendarView.Month: + setIsPreviousDisabled(compareMonth(currentDate, minDate) < 1); + setIsNextDisabled(compareMonth(currentDate, maxDate) > -1); + break; + case CalendarView.Year: + setIsPreviousDisabled(compare(currentDate, minDate, 0) < 1); + setIsNextDisabled(compare(currentDate, maxDate, 0) > -1); + break; + case CalendarView.Decade: + setIsPreviousDisabled(compare(currentDate, minDate, 10) < 1); + setIsNextDisabled(compare(currentDate, maxDate, 10) > -1); + } + }; + + const previous: () => void = (): void => { + switch (currentView) { + case CalendarView.Month: + setCurrentDate(addMonths(currentDate, -1)); + break; + case CalendarView.Year: + setCurrentDate(addYears(currentDate, -1)); + break; + case CalendarView.Decade: + setCurrentDate(addYears(currentDate, -10)); + break; + } + }; + + const next: () => void = (): void => { + switch (currentView) { + case CalendarView.Month: + setCurrentDate(addMonths(currentDate, 1)); + break; + case CalendarView.Year: + setCurrentDate(addYears(currentDate, 1)); + break; + case CalendarView.Decade: + setCurrentDate(addYears(currentDate, 10)); + break; + } + }; + + const compareMonth: (start: Date, end: Date) => number = (start: Date, end: Date): number => { + let result: number; + if (start.getFullYear() > end.getFullYear()) { + result = 1; + } else if (start.getFullYear() < end.getFullYear()) { + result = -1; + } else { + result = start.getMonth() === end.getMonth() ? 0 : start.getMonth() > end.getMonth() ? 1 : -1; + } + return result; + }; + + const compare: ( + startDate: Date, endDate: Date, modifier: number + ) => number = (startDate: Date, endDate: Date, modifier: number): number => { + let start: number = endDate.getFullYear(); + let end: number = start; + let result: number = 0; + if (modifier) { + start = start - (start % modifier); + end = start - (start % modifier) + modifier - 1; + } + if (startDate.getFullYear() > end) { + result = 1; + } else if (startDate.getFullYear() < start) { + result = -1; + } + return result; + }; + + const addContentFocus: () => void = (): void => { + if (selectedElementRef.current) { + selectedElementRef.current.classList.add('sf-focused-cell'); + } else if (focusedElementRef.current) { + focusedElementRef.current.classList.add('sf-focused-cell'); + } + }; + + const removeContentFocus: () => void = (): void => { + if (selectedElementRef.current) { + selectedElementRef.current.classList.remove('sf-focused-cell'); + } else if (focusedElementRef.current) { + focusedElementRef.current.classList.remove('sf-focused-cell'); + } + }; + + const navigatedTo: ( + e: React.MouseEvent | React.KeyboardEvent, + view: CalendarView, date?: Date + ) => void = (e: React.MouseEvent | React.KeyboardEvent, view: CalendarView, date?: Date): void => { + e.preventDefault(); + if (date && +date >= +minDate && +date <= +maxDate) { + setCurrentDate(date); + } + if (date && +date <= +minDate) { + setCurrentDate(new Date(minDate)); + } + if (date && +date >= +maxDate) { + setCurrentDate(new Date(maxDate)); + } + if (onViewChange) { + onViewChange({ + event: e, + view: view, + date: currentDate || date + }); + } + setCurrentView(view); + }; + + const minMaxDate: (localDate: Date) => Date = (localDate: Date): Date => { + const currentDate: Date = new Date(new Date(+localDate).setHours(0, 0, 0, 0)); + const min: Date = new Date(new Date(+minDate).setHours(0, 0, 0, 0)); + const max: Date = new Date(new Date(+maxDate).setHours(0, 0, 0, 0)); + if (+currentDate === +min || +currentDate === +max) { + if (+localDate < +minDate) { + return new Date(+minDate); + } + if (+localDate > +maxDate) { + return new Date(+maxDate); + } + } + return localDate; + }; + + const renderMonths: (date: Date) => JSX.Element = (date: Date): JSX.Element => { + const monthPerRow: number = weekNumber ? 8 : 7; + const tdEles: React.ReactNode[] = renderDays(date); + return ( + <> + {createContentHeader()} + {renderTemplate(tdEles, monthPerRow, 'sf-month')} + + ); + }; + + const todayButtonClick: ( + e: React.MouseEvent | React.KeyboardEvent + ) => void = (e: React.MouseEvent | React.KeyboardEvent): void => { + if (showTodayButton) { + e.preventDefault(); + const tempValue: Date = new Date(new Date().setHours(0, 0, 0, 0)); + const dateValue: Date = new Date(+tempValue.getTime()); + if (isControlled) { + if (onChange) { + onChange({ value: multiSelect ? [tempValue] : tempValue, ...e }); + } + } else { + if (multiSelect) { + const currentValues: Date[] = Array.isArray(currentValue) ? currentValue : []; + const newValues: Date[] = [...currentValues, tempValue]; + updateValue(newValues, e); + } else { + updateValue(tempValue, e); + } + } + if (depth !== CalendarView.Month) { + navigatedTo(e, depth, new Date(dateValue)); + } else { + if (getViewNumber(start) >= getViewNumber(depth)) { + navigatedTo(e, depth, new Date(dateValue)); + } else { + navigatedTo(e, CalendarView.Month, new Date(dateValue)); + } + } + } + }; + + const renderDays: (currentDate: Date, isTodayDate?: boolean) => React.ReactNode[] = (currentDate: Date, isTodayDate?: boolean) => { + const tdEles: React.ReactNode[] = []; + const totalDaysInGrid: number = 42; + const todayDate: Date = isTodayDate ? new Date(+currentDate) : new Date(); + let localDate: Date = new Date(currentDate); + const currentMonth: number = localDate.getMonth(); + let minMaxDates: Date; + localDate = new Date( + localDate.getFullYear(), + localDate.getMonth(), + 0, + localDate.getHours(), + localDate.getMinutes(), + localDate.getSeconds(), + localDate.getMilliseconds() + ); + while (localDate.getDay() !== firstDayOfWeek) { + setStartDate(localDate, -1 * 86400000); + } + for (let day: number = 0; day < totalDaysInGrid; ++day) { + const isWeekNumber: boolean | null = day % 7 === 0 && weekNumber; + minMaxDates = new Date(+localDate); + localDate = minMaxDate(localDate); + const dateFormatOptions: object = {locale: locale as string, type: 'dateTime', skeleton: 'full' }; + const date: Date = parseDate( + formatDate( localDate, dateFormatOptions), + dateFormatOptions + ); + const isOtherMonth: boolean = date.getMonth() !== currentMonth; + const isToday: boolean = date && todayDate && + date.getFullYear() === todayDate.getFullYear() && + date.getMonth() === todayDate.getMonth() && + date.getDate() === todayDate.getDate(); + const isSelected: boolean = multiSelect + ? normalizedDates.some((selectedDate: Date) => selectedDate && date.toDateString() === selectedDate.toDateString()) + : normalizedDates.length > 0 && date.toDateString() === normalizedDates[0].toDateString(); + const isDisabled: boolean = (minDate > date) || (maxDate < date); + const isWeekend: boolean = date.getDay() === 0 || date.getDay() === 6; + let isFocused: boolean = false; + if (currentDate.getDate() === localDate.getDate() && !isOtherMonth && !isDisabled) { + isFocused = true; + } else { + if (currentDate >= maxDate && parseInt(`${date.valueOf()}`, 10) === +maxDate && !isOtherMonth && !isDisabled) { + isFocused = true; + } + if (currentDate <= minDate && parseInt(`${date.valueOf()}`, 10) === +minDate && !isOtherMonth && !isDisabled) { + isFocused = true; + } + } + + const baseClasses: string = `${isOtherMonth ? 'sf-other-month' : ''} ${isWeekend ? 'sf-weekend' : ''} ${isDisabled ? 'sf-disabled sf-overlay' : ''}`; + const disabledClass: string = disabled ? 'sf-disabled sf-overlay' : ''; + const className: string = `sf-cell ${baseClasses} ${disabledClass} ${isToday ? 'sf-today' : ''} ${isToday && isFocused && !disabled ? 'sf-focused-date' : ''} ${isFocused && !onSelection && !disabled ? 'sf-focused-date' : ''} ${isSelected ? 'sf-selected' : ''}`.trim(); + + if (isWeekNumber) { + tdEles.push( + + + {(() => { + const numberOfDays: number = weekRule === 'FirstDay' ? 6 : (weekRule === 'FirstFourDayWeek' ? 3 : 0); + const finalDate: Date = new Date( + localDate.getFullYear(), + localDate.getMonth(), + (localDate.getDate() + numberOfDays) + ); + return getWeekNumber(finalDate); + })()} + + + ); + } + + const cellProps: CalendarCellProps = { + id: `${localDate.valueOf()}`, + className: className, + isDisabled: isDisabled || (disabled ? true : false), + isOutOfRange: isOtherMonth, + isToday: isToday, + isSelected: isSelected, + isFocused: isFocused && !onSelection, + isWeekend: isWeekend, + date: new Date(date), + onClick: (e: React.MouseEvent) => { + if (disabled) { + return; + } + clickHandler(e, date); + }, + title: formatDate(date, {locale: locale as string, type: 'date', skeleton: 'full' }) + }; + if (typeof cellTemplate === 'function') { + try { + const wrappedCellProps: CalendarCellProps & { ref: (el: HTMLTableCellElement | null) => void } = { + ...cellProps, + ref: (el: HTMLTableCellElement | null) => { + if (isFocused && el) { focusedElementRef.current = el; } + if (isSelected && el) { selectedElementRef.current = el; } + } + }; + const CustomCell: ( + props: CalendarCellProps + ) => React.ReactNode = cellTemplate as (props: CalendarCellProps) => React.ReactNode; + tdEles.push(CustomCell(wrappedCellProps)); + } catch (error) { + tdEles.push( + { + if (isFocused && el) { focusedElementRef.current = el; } + if (isSelected && el) { selectedElementRef.current = el; } + }} + > + {renderDayCells(localDate, isDisabled, isOtherMonth)} + + ); + } + } else { + tdEles.push( + { + if (isFocused && el) { focusedElementRef.current = el; } + if (isSelected && el) { selectedElementRef.current = el; } + }} + > + {renderDayCells(localDate, isDisabled, isOtherMonth)} + + ); + } + localDate = new Date(+minMaxDates); + localDate.setDate(localDate.getDate() + 1); + } + return tdEles; + }; + + const setStartDate: (date: Date, time: number) => void = (date: Date, time: number) => { + const tzOffset: number = date.getTimezoneOffset(); + const d: Date = new Date(date.getTime() + time); + const tzOffsetDiff: number = d.getTimezoneOffset() - tzOffset; + const minutesMilliSeconds: number = 60000; + date.setTime(d.getTime() + tzOffsetDiff * minutesMilliSeconds); + }; + + const renderDayCells: ( + date: Date, isDisabled: boolean, isOtherMonth: boolean) => React.ReactNode = ( + date: Date, isDisabled: boolean, isOtherMonth: boolean + ) => { + const title: string = formatDate(date, {locale: locale as string, type: 'date', skeleton: 'full' }); + const dayText: string = formatDate(date, {locale: locale as string, format: 'd', type: 'date', skeleton: 'yMd' }); + const isWeekend: boolean = date.getDay() === 0 || date.getDay() === 6; + if (isValidElement(cellTemplate)) { + return cellTemplate; + } else if (typeof cellTemplate === 'function') { + const isCalendarCellComponent: boolean = (cellTemplate).length === 1; + if (isCalendarCellComponent) { + return null; + } else { + const customContent: React.ReactNode = cellTemplate(date, isWeekend, currentView, currentDate); + if (customContent) { + return customContent; + } + } + } + return ( + + {dayText} + + ); + }; + + const createContentHeader: () => JSX.Element = () => { + const effectiveFirstDayOfWeek: number = firstDayOfWeek; + const shortNames: string[] = !isNullOrUndefined(weekDaysFormat) ? getCultureValues().length > 0 + ? shiftArray(getCultureValues(), effectiveFirstDayOfWeek) + : ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] : []; + return ( + + + {weekNumber && } + {shortNames.slice(0, 7).map((day: string, index: number) => ( + {toCapitalize(day)} + ))} + + + ); + }; + + const getCultureValues: () => string[] = () => { + const culShortNames: string[] = []; + let cldrObj: string[]; + const dayFormat: string = 'days.stand-alone.' + weekDaysFormat?.toLowerCase(); + if ((locale === 'en' || locale === 'en-US') && !isNullOrUndefined(dayFormat)) { + cldrObj = getValue(dayFormat, getDefaultDateObject()) as string[]; + } else { + cldrObj = getCultureObjects(cldrData, (locale || 'en-US')) as string[]; + } + if (!isNullOrUndefined(cldrObj)) { + for (const obj of Object.keys(cldrObj)) { + culShortNames.push(getValue(obj, cldrObj)); + } + } + return culShortNames; + }; + + const getCultureObjects: (ld: object, c: string) => object = (ld: object, c: string) => { + const gregorianFormat: string = weekDaysFormat + ? `.dates.calendars.gregorian.days.format.${weekDaysFormat.toLowerCase()}` + : ''; + const mainVal: string = 'main.'; + return getValue(`${mainVal}${c}${gregorianFormat}`, ld); + }; + + const shiftArray: (array: string[], places: number) => string[] = (array: string[], places: number) => { + return array.slice(places).concat(array.slice(0, places)); + }; + + const renderTemplate: ( + elements: React.ReactNode[], count: number, classNm: string + ) => React.ReactNode = (elements: React.ReactNode[], count: number, classNm: string) => { + const rows: React.ReactNode[] = []; + for (let i: number = 0; i < elements.length; i += count) { + const trElements: React.ReactNode[] = elements.slice(i, i + count); + const hasOtherMonth: boolean = trElements.some((element: React.ReactNode) => + isValidElement>(element) && + 'className' in element.props && + typeof element.props.className === 'string' && + element.props.className.includes('sf-other-month') + ); + const isOtherMonthRow: boolean = hasOtherMonth && trElements.every((element: React.ReactNode) => + isValidElement>(element) && + 'className' in element.props && + typeof element.props.className === 'string' && + ( + element.props.className.includes('sf-other-month') || + element.props.className.includes('sf-week-number') + ) + ); + rows.push( + + {trElements} + + ); + } + return ( + + {rows} + + ); + }; + + const renderYears: () => React.ReactNode = () => { + const monthPerRow: number = 3; + const tdEles: React.ReactNode[] = []; + const curDate: Date = new Date(currentDate); + const curYear: number = curDate.getFullYear(); + const curMonth: number = curDate.getMonth(); + const min: Date = new Date(minDate); + const max: Date = new Date(maxDate); + const minYear: number = min.getFullYear(); + const minMonth: number = min.getMonth(); + const maxYear: number = max.getFullYear(); + const maxMonth: number = max.getMonth(); + const selectedDate: Date | null = multiSelect + ? (Array.isArray(currentValue) && currentValue.length > 0 ? currentValue[currentValue.length - 1] : null) + : (currentValue && !Array.isArray(currentValue) ? currentValue : null); + const selectedMonth: number | undefined = selectedDate?.getMonth(); + const selectedYear: number | undefined = selectedDate?.getFullYear(); + for (let month: number = 0; month < 12; ++month) { + const monthDate: Date = new Date(curYear, month, 1); + const isFocusedDate: boolean = month === curMonth; + const isSelected: boolean = + selectedMonth === month && selectedYear === curYear; + const isDisabled: boolean = + (minDate && (curYear < minYear || (curYear === minYear && month < minMonth))) || + (maxDate && (curYear > maxYear || (curYear === maxYear && month > maxMonth))); + const className: string = + 'sf-cell' + + (isSelected ? ' sf-selected' : isFocusedDate ? ' sf-focused-date' : '') + + (isDisabled ? ' sf-disabled' : ''); + tdEles.push( + ) => handleCellClick(e, monthDate, currentView)} + ref={(el: HTMLTableCellElement | null) => { + if (isFocusedDate && el) { focusedElementRef.current = el; } + if (isSelected && el) { selectedElementRef.current = el; } + }} + aria-selected={isSelected || false} + > + {renderYearCell(monthDate)} + + ); + } + + return <>{renderTemplate(tdEles, monthPerRow, 'sf-zoomin')}; + }; + + const renderYearCell: (localDate: Date) => React.ReactNode = (localDate: Date) => { + const title: string = formatDate(localDate, {locale: locale as string, type: 'date', format: 'MMM y' }); + const content: string = toCapitalize( + formatDate(localDate, {locale: locale as string, + format: undefined, + type: 'dateTime', + skeleton: 'MMM' + }) + ); + return ( + + {content} + + ); + }; + + const renderDecades: () => JSX.Element = () => { + const yearPerRow: number = 3; + const yearCell: number = 12; + const tdEles: React.ReactNode[] = []; + const curDate: Date = new Date(currentDate); + const curYear: number = curDate.getFullYear(); + const localDate: Date = new Date(curDate); + localDate.setMonth(0); + const baseDecadeStartYear: number = curYear - (curYear % 10); + const startYr: Date = new Date(localDate.setFullYear(baseDecadeStartYear)); + const endYr: Date = new Date(localDate.setFullYear(baseDecadeStartYear + 9)); + const startFullYr: number = startYr.getFullYear(); + const endFullYr: number = endYr.getFullYear(); + startDecadeHdYr = formatDate(startYr, {locale: locale as string, + format: undefined, type: 'dateTime', skeleton: 'y' + }); + endDecadeHdYr = formatDate(endYr, {locale: locale as string, + format: undefined, type: 'dateTime', skeleton: 'y' + }); + const minYear: number = new Date(minDate).getFullYear(); + const maxYear: number = new Date(maxDate).getFullYear(); + const selectedDate: Date | null = multiSelect + ? (Array.isArray(currentValue) && currentValue.length > 0 ? currentValue[currentValue.length - 1] : null) + : (currentValue && !Array.isArray(currentValue) ? currentValue : null); + const startYear: number = baseDecadeStartYear - 1; + const selectedYear: number | null = selectedDate?.getFullYear() || null; + for (let i: number = 0; i < yearCell; ++i) { + const year: number = startYear + i; + localDate.setFullYear(year); + const fullYear: number = localDate.getFullYear(); + const isOutOfRange: boolean = fullYear < minYear || fullYear > maxYear; + const isOtherDecade: boolean = fullYear < startFullYr || fullYear > endFullYr; + let className: string = 'sf-cell'; + let isSelected: boolean = false; + let isFocused: boolean = false; + if (isOutOfRange) { + className += ' sf-disabled'; + } + if (isOtherDecade) { + className += ' sf-other-year'; + } + if (selectedYear === fullYear) { + className += ' sf-selected'; + isSelected = true; + } else if (fullYear === curYear) { + className += ' sf-focused-date'; + isFocused = true; + } + const decadeDate: Date = new Date(localDate); + tdEles.push( + ) => handleCellClick(e, decadeDate, currentView)} + ref={(el: HTMLTableCellElement | null) => { + if (isFocused && el) { focusedElementRef.current = el; } + if (isSelected && el) { selectedElementRef.current = el; } + }} + aria-selected={isSelected || false} + > + {renderDecadeCell(decadeDate)} + + ); + } + + return <>{renderTemplate(tdEles, yearPerRow, 'sf-zoomin')}; + }; + + const renderDecadeCell: (localDate: Date) => React.ReactNode = (localDate: Date) => { + const content: string = formatDate( localDate, {locale: locale as string, + format: undefined, type: 'dateTime', skeleton: 'y' + }); + return ( + + {content} + + ); + }; + + const handleCellClick: ( + e: React.MouseEvent | React.KeyboardEvent, date: Date, + view: string) => void = useCallback(( + e: React.MouseEvent | React.KeyboardEvent, date: Date, view: string): + void => { + setCurrentDate(date); + if (view === CalendarView.Year) { + const year: number = date.getFullYear(); + const month: number = date.getMonth(); + const firstDay: Date = new Date(year, month, 1); + const lastDay: Date = new Date(year, month + 1, 0); + const firstValidDay: Date = new Date(firstDay); + while (firstValidDay <= lastDay) { + if (firstValidDay >= minDate && firstValidDay <= maxDate) { + break; + } + firstValidDay.setDate(firstValidDay.getDate() + 1); + } + if (firstValidDay > lastDay) { + return; + } + if (depth === CalendarView.Year) { + updateValue(firstValidDay, e); + } else { + navigatedTo(e, CalendarView.Month, firstValidDay); + } + return; + } + if (depth === CalendarView.Decade) { + updateValue(date, e); + } else { + navigatedTo(e, CalendarView.Year, date); + } + }, [minDate, maxDate, depth, updateValue, navigatedTo]); + + const titleUpdate: (date: Date, view: string) => void = (date: Date, view: string) => { + const dayFormatOptions: string = formatDate(date, {locale: locale as string, + type: 'dateTime', skeleton: 'yMMMM', calendar: 'gregorian' + }); + const monthFormatOptions: string = formatDate( date, {locale: locale as string, + format: undefined, type: 'dateTime', skeleton: 'y', calendar: 'gregorian' + }); + switch (view) { + case 'days': + if (headerTitleElement.current) { + setheaderTitle(toCapitalize(dayFormatOptions)); + } + break; + case 'months': + if (headerTitleElement.current) { + setheaderTitle(monthFormatOptions); + } + break; + } + }; + + const clickHandler: ( + e: React.MouseEvent, value: Date + ) => void = (e: React.MouseEvent, value: Date) => { + if (readOnly || disabled || isImproperDateRange) { + return; + } + const isOtherMonth: boolean = currentView === CalendarView.Month && + value.getMonth() !== currentDate.getMonth(); + if (isOtherMonth) { + const newDate: Date = new Date(value); + setCurrentDate(newDate); + return; + } + const storeView: CalendarView = currentView; + selectDate(e, getIdValue(e, null)); + if (multiSelect && currentDate !== value && (storeView === CalendarView.Year || storeView === CalendarView.Month)) { + if (focusedElementRef.current) { + focusedElementRef.current.classList.remove('sf-focused-date'); + } + } + if (calendarElement.current) { + calendarElement.current.focus(); + } + }; + + const selectDate: ( + e: React.MouseEvent | React.FocusEvent | React.KeyboardEvent, + date: Date) => void = ( + e: React.MouseEvent | React.FocusEvent | React.KeyboardEvent, date: Date + ) => { + if (isControlled) { + if (onChange) { + let newValue: Date | Date[]; + if (multiSelect) { + const currentValues: Date[] = Array.isArray(currentValue) ? currentValue : []; + const dateExists: boolean = currentValues.some((v: Date) => v.toDateString() === date.toDateString()); + if (dateExists) { + newValue = currentValues.filter((v: Date) => v.toDateString() !== date.toDateString()); + } else { + newValue = [...currentValues, date]; + } + } else { + newValue = date; + } + onChange({ value: newValue, ...e }); + } + } else { + if (multiSelect) { + const currentValues: Date[] = Array.isArray(currentValue) ? currentValue : []; + const dateExists: boolean = currentValues.some((v: Date) => v.toDateString() === date.toDateString()); + let newValues: Date[]; + if (dateExists) { + newValues = currentValues.filter((v: Date) => v.toDateString() !== date.toDateString()); + } else { + newValues = [...currentValues, date]; + } + updateValue(newValues, e); + } else { + updateValue(date, e); + } + } + setOnSelection(true); + setCurrentDate(date); + }; + + const getIdValue: ( + e: React.MouseEvent | React.TouchEvent | React.KeyboardEvent | null, + element: Element | null) => Date = ( + e: React.MouseEvent | React.TouchEvent | React.KeyboardEvent | null, + element: Element | null + ): Date => { + let eve: Element; + if (e) { + eve = e.currentTarget; + } else { + eve = element!; + } + const dateFormatOptions: object = { locale: locale as string, type: 'dateTime', skeleton: 'full', calendar: 'gregorian' }; + const dateString: string = formatDate( + new Date(parseInt('' + eve.getAttribute('id'), 10)), + dateFormatOptions + ); + const date: Date = parseDate(dateString, dateFormatOptions); + const value: number = date.valueOf() - date.valueOf() % 1000; + return new Date(value); + }; + + const handleHeaderTitleClick: ( + e: React.MouseEvent | React.KeyboardEvent, + view: string + ) => void = (e: React.MouseEvent | React.KeyboardEvent, view: string) => { + switch (view) { + case CalendarView.Month: + navigatedTo(e, CalendarView.Year); + break; + case CalendarView.Year: + navigatedTo(e, CalendarView.Decade); + break; + default: + break; + } + }; + + const toCapitalize: (text: string) => string = (text: string) => { + return text.charAt(0).toUpperCase() + text.slice(1); + }; + + const getViewNumber: (view: CalendarView) => number = (view: CalendarView): number => { + switch (view) { + case CalendarView.Month: + return 0; + case CalendarView.Year: + return 1; + case CalendarView.Decade: + return 2; + default: + return 0; + } + }; + + const keyActionHandle: (e: React.KeyboardEvent) => void = (e: React.KeyboardEvent) => { + if (calendarElement.current === null || e.key === 'Escape' || readOnly || disabled || isImproperDateRange || isHeaderFocused) { + return; + } + e.stopPropagation(); + setOnSelection(false); + const selectedDate: Element | null = multiSelect ? selectedElementRef.current : focusedElementRef.current; + const view: number = getViewNumber(currentView as CalendarView); + const depthValue: number = getViewNumber(depth); + const levelRestrict: boolean = (view === depthValue && getViewNumber(start) >= depthValue); + const element: Element | null = focusedElementRef.current || selectedDate; + switch (e.key) { + case 'ArrowLeft': + keyboardNavigate(-1, view); + e.preventDefault(); + break; + case 'ArrowRight': + keyboardNavigate(1, view); + e.preventDefault(); + break; + case 'ArrowUp': + keyboardNavigate(view === 0 ? -7 : -4, view); + e.preventDefault(); + break; + case 'ArrowDown': + keyboardNavigate(view === 0 ? 7 : 4, view); + e.preventDefault(); + break; + case 'Enter': + if (element) { + if (levelRestrict) { + const d: Date = new Date(parseInt(element.id, 10)); + selectDate(e, d); + } else if (!(e.currentTarget as HTMLElement).className.includes('sf-disabled')) { + handleCellClick(e, currentDate, currentView); + } + } + break; + case 'Home': + setCurrentDate(firstDay(currentDate)); + e.preventDefault(); + break; + case 'End': + setCurrentDate(lastDay(currentDate, view)); + e.preventDefault(); + break; + case 'PageUp': + if (e.shiftKey) { + setCurrentDate(addYears(currentDate, -1)); + } else { + setCurrentDate(addMonths(currentDate, -1)); + } + e.preventDefault(); + break; + case 'PageDown': + if (e.shiftKey) { + setCurrentDate(addYears(currentDate, 1)); + } else { + setCurrentDate(addMonths(currentDate, 1)); + } + e.preventDefault(); + break; + } + if (e.ctrlKey) { + switch (e.key) { + case 'ArrowUp': + if (view < 2 && view >= getViewNumber(depth)) { + const nextView: CalendarView = view === 0 ? CalendarView.Year : CalendarView.Decade; + navigatedTo(e, nextView); + } + e.preventDefault(); + break; + case 'ArrowDown': + if (view > 0 && view > depthValue) { + const nextView: CalendarView = view === 2 ? CalendarView.Year : CalendarView.Month; + navigatedTo(e, nextView, currentDate); + } + e.preventDefault(); + break; + case 'Home': + navigatedTo(e, CalendarView.Month, new Date(currentDate.getFullYear(), 0, 1)); + e.preventDefault(); + break; + case 'End': + navigatedTo(e, CalendarView.Month, new Date(currentDate.getFullYear(), 11, 31)); + e.preventDefault(); + break; + } + } + }; + + const firstDay: (date: Date) => Date = (date: Date) => { + const view: number = getViewNumber(currentView as CalendarView); + const visibleDates: Date[] = getVisibleDates(date, view); + for (const d of visibleDates) { + if (!isDateDisabled(d)) { + return d; + } + } + return date; + }; + + const lastDay: (date: Date, view: number) => Date = (date: Date, view: number) => { + const lastDate: Date = new Date(date.getFullYear(), date.getMonth() + 1, 0); + if (view !== 2) { + const timeOffset: number = Math.abs(lastDate.getTimezoneOffset() - firstDay(date).getTimezoneOffset()); + if (timeOffset) { + lastDate.setHours(firstDay(date).getHours() + (timeOffset / 60)); + } + return findLastDay(lastDate, view); + } else { + return findLastDay(firstDay(lastDate), view); + } + }; + + const findLastDay: (date: Date, view: number) => Date = (date: Date, view: number) => { + const visibleDates: Date[] = getVisibleDates(date, view).reverse(); + for (const d of visibleDates) { + if (!isDateDisabled(d)) { + return d; + } + } + return date; + }; + + const isDateDisabled: (date: Date) => boolean = (date: Date) => { + return (minDate && date < minDate) || (maxDate && date > maxDate); + }; + + const getVisibleDates: (baseDate: Date, view: number) => Date[] = (baseDate: Date, view: number) => { + const result: Date[] = []; + if (view === 0) { + const start: Date = new Date(baseDate.getFullYear(), baseDate.getMonth(), 1); + const end: Date = new Date(baseDate.getFullYear(), baseDate.getMonth() + 1, 0); + for (let d: Date = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + result.push(new Date(d)); + } + } else if (view === 1) { + for (let m: number = 0; m < 12; m++) { + result.push(new Date(baseDate.getFullYear(), m, 1)); + } + } else if (view === 2) { + const decadeStart: number = baseDate.getFullYear() - (baseDate.getFullYear() % 10) - 1; + for (let y: number = decadeStart; y < decadeStart + 12; y++) { + result.push(new Date(y, 0, 1)); + } + } + return result; + }; + + const keyboardNavigate: (num: number, view: number) => void = (num: number, view: number) => { + const date: Date = new Date(currentDate); + switch (view) { + case 2: + if (isMonthYearRange(date)) { + setCurrentDate(addYears(currentDate, num)); + } else { + setCurrentDate(currentDate); + } + break; + case 1: + if (isMonthYearRange(date)) { + setCurrentDate(addMonths(currentDate, num)); + } else { + setCurrentDate(currentDate); + } + break; + case 0: + date.setDate(date.getDate() + num); + if (+date >= +minDate && +date <= +maxDate) { + setCurrentDate(date); + } else { + setCurrentDate(currentDate); + } + break; + } + }; + + const isMonthYearRange: (date: Date) => boolean = (date: Date) => { + return date.getMonth() >= minDate.getMonth() + && date.getFullYear() >= minDate.getFullYear() + && date.getMonth() <= maxDate.getMonth() + && date.getFullYear() <= maxDate.getFullYear(); + }; + + const renderModelHeader: () => React.ReactNode | null = () => { + if (!fullScreenMode) { + return null; + } + return ( +
+
+ +
+ {showTodayButton && ( + + )} +
+ + {formatDate(currentValue && !Array.isArray(currentValue) ? currentValue : new Date(), {locale: locale || 'en-US', format: 'E', type: 'date' })} + + + {formatDate(currentValue && !Array.isArray(currentValue) ? currentValue : new Date(), { locale: locale || 'en-US', format: 'MMM d', type: 'date' })} + +
+
+ ); + }; + + useImperativeHandle(ref, () => ({ + ...publicAPI as ICalendar, + element: calendarElement.current + }), [publicAPI]); + + const classNames: string = [ + 'sf-calendar', + className, + 'sf-control', + dir === 'rtl' ? 'sf-rtl' : '', + (isImproperDateRange || disabled) ? 'sf-overlay' : '', + weekDaysFormat === WeekDaysFormats.Wide ? 'sf-calendar-day-header-lg' : '', + weekNumber ? 'sf-week-number' : '', + readOnly ? 'sf-readonly' : '' + ].filter(Boolean).join(' '); + + return ( +
{ + if (calendarElement.current) { + calendarElement.current.focus(); + } + }} + {...otherProps}> + {fullScreenMode && renderModelHeader()} +
+
) => { + if (!disabled && !isImproperDateRange) { + handleHeaderTitleClick(e, currentView); + } + }} + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (!disabled && !isImproperDateRange) { + handleHeaderTitleClick(e, currentView); + } + } + }} + aria-atomic="true" + aria-live="assertive" + aria-label="title" + tabIndex={disabled || isImproperDateRange ? -1 : 0} + onFocus={() => setIsHeaderFocused(true)} + onMouseUp={(e: React.MouseEvent) => { + (e.currentTarget as HTMLElement).blur(); + }} + onBlur={() => { + setIsHeaderFocused(false); + if (focusedElementRef.current) { + focusedElementRef.current.focus(); + } + }} + > + { + (() => { + if (typeof cellTemplate === 'function') { + const customTitle: React.ReactNode = cellTemplate(currentView, true); + if (customTitle) { + return customTitle; + } + } + return headerTitle; + })() + } +
+
+ + +
+
+
+ + {(() => { + switch (getViewNumber(currentView)) { + case 0: + return renderMonths(currentDate); + case 1: + return renderYears(); + case 2: + return renderDecades(); + default: + return null; + } + })() || null} +
+
+ {showTodayButton && ( +
+ +
+ )} +
+ ); + }); +export default React.memo(Calendar); diff --git a/components/calendars/src/calendar/index.ts b/components/calendars/src/calendar/index.ts new file mode 100644 index 0000000..68cf706 --- /dev/null +++ b/components/calendars/src/calendar/index.ts @@ -0,0 +1,9 @@ +/** + * Calendar modules + */ +export * from './calendar'; + +/** + * Calendar cell modules + */ +export * from './calendar-cell'; diff --git a/components/calendars/src/datepicker/datepicker.tsx b/components/calendars/src/datepicker/datepicker.tsx new file mode 100644 index 0000000..77b1852 --- /dev/null +++ b/components/calendars/src/datepicker/datepicker.tsx @@ -0,0 +1,971 @@ +import * as React from 'react'; +import { + useState, useRef, useEffect, useCallback, forwardRef, useImperativeHandle, + useMemo +} from 'react'; +import { Calendar, CalendarView, WeekRule, WeekDaysFormats, ViewChangeEvent, ICalendar, ChangeEvent } from '../calendar/calendar'; +import { CalendarCellProps } from '../calendar/calendar-cell'; +import { InputBase, renderFloatLabelElement } from '@syncfusion/react-inputs'; +import { Browser, formatDate, getDatePattern, parseDate, preRender, LabelMode } from '@syncfusion/react-base'; +import { useProviderContext } from '@syncfusion/react-base'; +import { CollisionType, getZindexPartial, IPopup, Popup } from '@syncfusion/react-popups'; +import { CloseIcon, TimelineDayIcon } from '@syncfusion/react-icons'; +import { createPortal } from 'react-dom'; +export { LabelMode }; + +export interface DatePickerProps { + /** + * Specifies the placeholder text to display in the input box when no value is set. + * + * @default - + */ + placeholder?: string; + + /** + * Specifies whether the component is disabled or not. + * + * @default false + */ + disabled?: boolean; + + /** + * Specifies whether the component is in read-only mode. + * When enabled, users cannot change input value or open the picker. + * + * @default false + */ + readOnly?: boolean; + + /** + * Specifies the float label behavior. + * Possible values: + * * `Never` - The label will never float. + * * `Auto` - The label floats when the input has focus, value, or placeholder. + * * `Always` - The label always floats. + * + * @default 'Never' + */ + labelMode?: LabelMode; + + /** + * Specifies the date format string for displaying and parsing date values. + * Examples: 'MM/dd/yyyy', 'yyyy-MM-dd', etc. + * + * @default 'M/d/yyyy' + */ + format?: string; + + /** + * Specifies an array of acceptable date input formats for parsing user input. + * Can be an array of strings or FormatObject. + * + * @default - + */ + inputFormats?: string[] | FormatObject[]; + + /** + * Specifies whether to show the clear button within the input field. + * + * @default true + */ + clearButton?: boolean; + + /** + * Enables strict date validation mode. + * When enabled, invalid values are prevented or auto-corrected. + * + * @default false + */ + strictMode?: boolean; + + /** + * When true, should open the calendar popup on input focus. + * + * @default false + */ + openOnFocus?: boolean; + + /** + * Specifies the selected date of the DatePicker for controlled usage. + * + * @default null + * + */ + value?: Date | null; + + /** + * Specifies the default selected date of the DatePicker for uncontrolled mode. + * + * @default null + * + */ + defaultValue?: Date; + + /** + * Specifies the minimum date that can be selected in the DatePicker. + * + * @default new Date(1900, 0, 1) + */ + minDate?: Date; + + /** + * Specifies the maximum date that can be selected in the DatePicker. + * + * @default new Date(2099, 11, 31) + */ + maxDate?: Date; + + /** + * Specifies the initial view of the Calendar when it is opened. + * + * @default Month + */ + start?: CalendarView; + + /** + * Sets the maximum level of view such as month, year, and decade. + * Depth view should be smaller than the start view to restrict its view navigation. + * + * @default Month + */ + depth?: CalendarView; + + /** + * Specifies whether the calendar popup is open or closed. + * + * @default false + */ + open?: boolean; + + /** + * Specifies the first day of the week for the calendar. + * + * @default 0 + */ + firstDayOfWeek?: number; + + /** + * Specifies whether the week number of the year is to be displayed in the calendar or not. + * + * @default false + */ + weekNumber?: boolean; + + /** + * Specifies the rule for defining the first week of the year. + * Used only if `weekNumber` is enabled. + * + * @default FirstDay + */ + weekRule?: WeekRule; + + /** + * Specifies whether the today button is to be displayed or not. + * + * @default true + */ + showTodayButton?: boolean; + + /** + * Specifies the format of the day that to be displayed in header. + * Possible formats are: + * * `Short` - Sets the short format of day name (like Su) in day header. + * * `Narrow` - Sets the single character of day name (like S) in day header. + * * `Abbreviated` - Sets the min format of day name (like Sun) in day header. + * * `Wide` - Sets the long format of day name (like Sunday) in day header. + * + * @default Short + */ + weekDaysFormat?: WeekDaysFormats; + + /** + * Provides a template for rendering custom content for each day cell in the calendar. + * + * @default null + */ + cellTemplate?: Function | React.ReactNode; + + /** + * Specifies whether the input field can be edited directly. + * When false, only allows selection via calendar. + * + * @default true + */ + editable?: boolean; + + /** + * Sets the z-index value for the dropdown popup, controlling its stacking order relative to other elements on the page. + * + * @default 1000 + */ + zIndex?: number; + + /** + * Specifies whether the component popup should display in full screen mode on mobile devices. + * + * @default false + */ + fullScreenMode?: boolean; + + + /** + * Specifies whether the DatePicker is a required field in a form. + * When set to true, the component will be marked as required. + * + * @default false + */ + required?: boolean; + + /** + * Overrides the validity state of the component. + * If valid is set, the required property will be ignored. + * + * @default false + */ + valid?: boolean; + + /** + * Controls the form error message of the component. + * + * @default - + */ + validationMessage?: string; + + /** + * If set to false, no visual representation of the invalid state of the component will be applied. + * + * @default true + */ + validityStyles?: boolean; + + /** + * Triggers when the DatePicker value is changed. + * + * @event onChange + */ + onChange?: ((args: ChangeEvent) => void); + + /** + * Triggers when the calendar popup opens. + * + * @event onOpen + */ + onOpen?: (args: { popup: IPopup }) => void; + + /** + * Triggers when the calendar popup closes. + * + * @event onClose + */ + onClose?: (args: { popup: IPopup }) => void; + + /** + * Triggers when the Calendar is navigated to another level or within the same level of view. + * + * @event onViewChange + */ + onViewChange?: (args: ViewChangeEvent) => void; +} + +export interface FormatObject { + /** + * Specifies the format skeleton to use for formatting dates. + * + */ + skeleton?: string; +} + +export interface IDatePicker extends DatePickerProps { + /** + * The content to be rendered inside the component. + * + * @private + * @default null + */ + element?: HTMLSpanElement | null; +} + +type IDatePickerProps = IDatePicker & Omit, 'defaultValue' | 'value'>; + +export const DatePicker: React.ForwardRefExoticComponent> = + forwardRef((props: IDatePickerProps, ref: React.Ref) => { + const { + className = '', + placeholder = 'Choose a date', + disabled = false, + readOnly = false, + labelMode = 'Never', + format = 'M/d/yyyy', + clearButton = true, + strictMode = false, + value, + defaultValue, + minDate = new Date(1900, 0, 1), + maxDate = new Date(2099, 11, 31), + start = CalendarView.Month, + depth = CalendarView.Month, + firstDayOfWeek = 0, + weekNumber = false, + weekRule, + showTodayButton = true, + weekDaysFormat, + open, + inputFormats, + fullScreenMode = false, + zIndex = 1000, + cellTemplate, + onChange, + onOpen, + onClose, + openOnFocus = false, + editable = true, + required = false, + valid, + validationMessage = '', + validityStyles = true, + ...otherProps + } = props; + + const { locale, dir } = useProviderContext(); + const [isOpen, setIsOpen] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [selectedDate, setSelectedDate] = useState(value ?? defaultValue ?? null); + const [isFocused, setIsFocused] = useState(false); + const containerRef: React.RefObject = useRef(null); + const inputRef: React.RefObject = useRef(null); + const popupRef: React.RefObject = useRef(null); + const calendarRef: React.RefObject = useRef(null); + const iconRef: React.RefObject = useRef(null); + const isControlled: boolean = value !== undefined; + const currentValue: Date | null | undefined = isControlled ? value : selectedDate; + const isOpenControlled: boolean = open !== undefined; + const currentOpenState: boolean | undefined = isOpenControlled ? open : isOpen; + const [inputFormatsString, setInputFormatsString] = useState([]); + const [isInputValid, setIsInputValid] = useState(valid !== undefined ? valid : (required ? selectedDate !== null : true)); + const [isIconActive, setIsIconActive] = useState(false); + const isFullScreenMode: boolean = fullScreenMode && Browser.isDevice; + const [shouldFocusOnClose, setShouldFocusOnClose] = useState(false); + + useEffect(() => { + if (inputFormats && inputFormats.length > 0) { + const formats: string[] = inputFormats.map((format: string | FormatObject) => { + if (typeof format === 'string') { + return format; + } else if (format.skeleton) { + return getDatePattern( {locale: locale || 'en-US', skeleton: format.skeleton, type: 'date' }); + } + return ''; + }).filter(Boolean); + setInputFormatsString(formats); + } else { + setInputFormatsString([]); + } + }, [inputFormats, locale]); + + useEffect(() => { + if (!inputRef.current) { return; } + const isValid: boolean = valid !== undefined ? valid : (required ? currentValue !== null : true); + setIsInputValid(isValid); + const message: string = isValid ? '' : validationMessage || ''; + inputRef.current.setCustomValidity(message); + }, [valid, validationMessage, required, currentValue]); + + const formatDateValue: (date: Date) => string = useCallback((date: Date) => { + try { + return formatDate(date, {locale: locale || 'en-US', format: format || 'M/d/yyyy', type: 'date' }); + } catch { + return date.toLocaleDateString(locale || 'en-US'); + } + }, [locale, format]); + + useEffect(() => { + if (currentValue) { + const formattedValue: string = formatDateValue(currentValue); + if (inputValue !== formattedValue) { + setInputValue(formattedValue); + } + } else { + if (inputValue !== '') { + setInputValue(''); + } + } + }, [currentValue, format, locale]); + + const showPopup: () => void = useCallback(() => { + if (disabled || readOnly) { + return; + } + if (!currentOpenState) { + if (!isOpenControlled) { + setIsOpen(true); + } + } + }, [disabled, readOnly, currentOpenState, isOpenControlled]); + + const hidePopup: () => void = useCallback(() => { + if (currentOpenState) { + if (!isOpenControlled) { + setIsOpen(false); + } + if (onClose && popupRef.current) { + onClose({ + popup: popupRef.current as IPopup + }); + } + } + }, [currentOpenState, isOpenControlled, onClose]); + + + const togglePopup: () => void = useCallback(() => { + if (currentOpenState) { + hidePopup(); + setIsIconActive(false); + } else { + showPopup(); + } + }, [currentOpenState, hidePopup, showPopup]); + + const handlePopupInteraction: (e: Event) => void = useCallback((e: Event) => { + const target: HTMLElement = e.target as HTMLElement; + if (isFullScreenMode && target.closest('.sf-popup-close')) { + hidePopup(); + setIsIconActive(false); + return; + } + if (!isFullScreenMode && e.type === 'mousedown') { + const targetNode: Node = e.target as Node; + if (!containerRef.current?.contains(targetNode) && !popupRef.current?.element?.contains(targetNode)) { + hidePopup(); + setIsIconActive(false); + } + } + }, [isFullScreenMode, hidePopup]); + + useEffect(() => { + if (!currentOpenState) { + if (shouldFocusOnClose) { + if (inputRef.current) { + inputRef.current.focus(); + } + setShouldFocusOnClose(false); + } + return; + } + const handleGlobalKeyDown: (e: KeyboardEvent) => void = (e: KeyboardEvent) => { + if (e.altKey && e.key === 'ArrowUp' && currentOpenState) { + e.preventDefault(); + e.stopPropagation(); + const inputElement: HTMLInputElement | null = inputRef.current; + + hidePopup(); + setIsIconActive(false); + + if (inputElement) { + inputElement.focus(); + } + } + if (e.key === 'Escape' && currentOpenState) { + e.preventDefault(); + e.stopPropagation(); + hidePopup(); + setIsIconActive(false); + if (inputRef.current) { + inputRef.current.focus(); + } + } + }; + document.addEventListener('mousedown', handlePopupInteraction); + document.addEventListener('keydown', handleGlobalKeyDown, true); + return () => { + document.removeEventListener('mousedown', handlePopupInteraction); + document.removeEventListener('keydown', handleGlobalKeyDown, true); + }; + }, [currentOpenState, handlePopupInteraction, hidePopup]); + + const handleKeyDown: (e: React.KeyboardEvent) => void = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown' && e.altKey) { + showPopup(); + } else if (e.key === 'Tab' && currentOpenState) { + if (isIconActive || e.target === iconRef.current || e.target === inputRef.current) { + if (isIconActive || e.target === iconRef.current) { + setIsIconActive(false); + } + } + } + if ((e.key === 'Enter' || e.key === ' ') && e.target !== inputRef.current) { + togglePopup(); + } + }; + + const parseInputValue: (inputVal: string) => Date | null = useCallback((inputVal: string) => { + if (!inputVal || !inputVal.trim()) { + return null; + } + const normalizedInput: string = inputVal.replace(/(am|pm|Am|aM|pM|Pm)/g, (match: string) => match.toUpperCase()); + if (inputFormatsString && inputFormatsString.length > 0) { + for (const formatStr of inputFormatsString) { + try { + const parsedDate: Date = parseDate(normalizedInput, {locale: locale || 'en-US', format: formatStr, type: 'date' }); + if (parsedDate && !isNaN(parsedDate.getTime())) { + return parsedDate; + } + } catch (e) { + continue; + } + } + } + try { + const parsedDate: Date = parseDate(normalizedInput, { locale: locale || 'en-US', format: format, type: 'date' }); + if (parsedDate && !isNaN(parsedDate.getTime())) { + return parsedDate; + } + } catch (e) { + return null; + } + return null; + }, [locale, format, inputFormatsString]); + + + const updateValue: ( + newValue: Date | null, + event?: React.MouseEvent | React.FocusEvent | React.KeyboardEvent + ) => void = useCallback(( + newValue: Date | null, + event?: React.MouseEvent | React.FocusEvent | React.KeyboardEvent + ) => { + if (!isControlled) { + setSelectedDate(newValue); + } + const isValid: boolean = valid !== undefined ? valid : (required ? newValue !== null : true); + setIsInputValid(isValid); + if (onChange) { + onChange({ value: newValue, event }); + } + }, [isControlled, onChange, required, valid]); + + const handleClear: () => void = useCallback(() => { + setInputValue(''); + updateValue(null); + hidePopup(); + inputRef.current?.focus(); + }, [updateValue, hidePopup]); + + const handleCalendarChange: (event: ChangeEvent) => void = useCallback((event: ChangeEvent) => { + if (event && event.value) { + if (!isControlled) { + setInputValue(formatDateValue(event.value as Date)); + } + updateValue(event.value as Date, event.event); + setShouldFocusOnClose(true); + hidePopup(); + } + }, [formatDateValue, updateValue, hidePopup, isControlled]); + + const isDateDisabledByCellTemplate: (date: Date) => boolean = (date: Date) => { + if (typeof cellTemplate !== 'function') { + return false; + } + try { + const isWeekend: boolean = date.getDay() === 0 || date.getDay() === 6; + const today: Date = new Date(); + const referenceDate: Date = currentValue && !Array.isArray(currentValue) ? currentValue : today; + const isOtherMonth: boolean = date.getMonth() !== referenceDate.getMonth(); + const isToday: boolean = today.toDateString() === date.toDateString(); + const isSelected: boolean | null | undefined = currentValue && + !Array.isArray(currentValue) && + currentValue.toDateString() === date.toDateString(); + const cellProps: CalendarCellProps = { + date: date, + isWeekend: isWeekend, + isDisabled: (minDate > date) || (maxDate < date), + isOutOfRange: isOtherMonth, + isToday: isToday, + isSelected: isSelected || false, + isFocused: false, + className: '', + id: `${date.valueOf()}` + }; + + const result: React.ReactNode = cellTemplate(cellProps); + if (result && typeof result === 'object' && 'props' in result) { + return (result as any).props.isDisabled === true; + } + if (result === null || result === undefined) { + return true; + } + + return false; + } catch (error) { + return false; + } + }; + + const isValidDate: (date: Date) => boolean = (date: Date) => { + if (!date || isNaN(date.getTime())) { + return false; + } + if (strictMode) { + if (minDate && maxDate && minDate.getTime() === maxDate.getTime()) { + return date.getTime() === minDate.getTime(); + } + if (minDate && date < minDate) { + return false; + } + if (maxDate && date > maxDate) { + return false; + } + } + + const basicValidation: boolean = date >= (minDate || date) && date <= (maxDate || date); + if (!basicValidation) { + return false; + } + + if (isDateDisabledByCellTemplate(date)) { + return false; + } + + return true; + }; + + const handleInputBlur: () => void = useCallback(() => { + setIsFocused(false); + const trimmedValue: string = inputValue.trim(); + if (trimmedValue === '') { + setInputValue(''); + updateValue(null); + setIsInputValid(valid !== undefined ? valid : !required); + return; + } + if (strictMode && inputValue.trim() !== '') { + let parsedDate: Date | null = parseInputValue(inputValue); + if (!parsedDate) { + const twoDigitYearPattern: RegExp = /^(\d{1,2})[/.-](\d{1,2})[/.-](\d{2})$/; + const match: RegExpExecArray | null = twoDigitYearPattern.exec(inputValue.trim()); + if (match) { + const [_, part1, part2, twoDigitYear] = match; + const fullYear: number = 2000 + parseInt(twoDigitYear, 10); + const modifiedInput: string = `${part1}/${part2}/${fullYear}`; + parsedDate = parseInputValue(modifiedInput); + } + } + if (!parsedDate || !isValidDate(parsedDate)) { + setInputValue(currentValue ? formatDateValue(currentValue) : ''); + setIsInputValid(valid !== undefined ? valid : !inputValue.trim()); + if (!currentValue) { + updateValue(null); + } + return; + } else { + setInputValue(formatDateValue(parsedDate)); + updateValue(parsedDate); + return; + } + } + let parsedDate: Date | null = parseInputValue(inputValue); + if (!parsedDate && inputValue.trim() !== '') { + const twoDigitYearPattern: RegExp = /^(\d{1,2})[/.-](\d{1,2})[/.-](\d{2})$/; + const match: RegExpExecArray | null = twoDigitYearPattern.exec(inputValue.trim()); + if (match) { + const [_, part1, part2, twoDigitYear] = match; + const fullYear: number = 2000 + parseInt(twoDigitYear, 10); + const modifiedInput: string = `${part1}/${part2}/${fullYear}`; + parsedDate = parseInputValue(modifiedInput); + } + } + let isValid: boolean = true; + if (inputValue.trim() !== '' && !parsedDate) { + isValid = false; + } + if (parsedDate && !isValidDate(parsedDate)) { + isValid = false; + } + if (valid !== undefined) { + setIsInputValid(valid); + } else if (inputValue.trim() === '') { + setIsInputValid(!required); + } else if (!isValid) { + setIsInputValid(false); + } else if (required) { + setIsInputValid(currentValue !== null); + } else { + setIsInputValid(isValid); + } + if (parsedDate && isValidDate(parsedDate)) { + setInputValue(formatDateValue(parsedDate)); + updateValue(parsedDate); + } else if (inputValue.trim() === '') { + updateValue(null); + } else if (!strictMode) { + updateValue(null); + if (valid === undefined && inputValue.trim() !== '' && !isValid) { + setIsInputValid(false); + } + } else { + updateValue(null); + } + }, [parseInputValue, inputValue, strictMode, formatDateValue, isValidDate, updateValue, currentValue, valid, required]); + + const handleInputChange: (event: React.ChangeEvent) => void = (event: React.ChangeEvent) => { + const newInputValue: string = event.target.value; + setInputValue(newInputValue); + if (valid === undefined) { + if (newInputValue.trim() === '') { + setIsInputValid(!required); + } + else if (!strictMode) { + const parsedDate: Date | null = parseInputValue(newInputValue); + setIsInputValid(parsedDate !== null && isValidDate(parsedDate)); + } + } + }; + + const publicAPI: Partial = { + placeholder, + editable, + inputFormats, + openOnFocus, + format, + labelMode, + disabled, + open, + clearButton, + zIndex, + strictMode, + value: currentValue, + minDate, + maxDate, + firstDayOfWeek, + start, + depth, + weekNumber, + weekRule, + showTodayButton, + weekDaysFormat, + required, + valid, + validationMessage, + validityStyles, + cellTemplate + }; + + const { zIndexPopup } = useMemo(() => { + let baseValue: number = typeof zIndex === 'number' ? zIndex : 1000; + if (baseValue === 1000 && containerRef.current) { + baseValue = getZindexPartial(containerRef.current); + } + return { + zIndexPopup: Math.max(3, baseValue + 1) + }; + }, [zIndex, containerRef.current]); + + useImperativeHandle(ref, () => ({ + ...publicAPI as IDatePicker, + element: containerRef.current + }), [publicAPI]); + + useEffect(() => { + preRender('datepicker'); + }, []); + + return ( + + { + setIsFocused(true); + if (openOnFocus && !disabled && !readOnly) { + showPopup(); + } + }} + onBlur={() => { + setIsFocused(false); + handleInputBlur(); + }} + value={inputValue} + onChange={editable && !readOnly && !disabled ? handleInputChange : undefined} + role="combobox" + aria-haspopup="dialog" + aria-expanded={currentOpenState} + aria-disabled={disabled} + onKeyDown={handleKeyDown} + tabIndex={0} + required={required} + /> + {labelMode !== 'Never' && renderFloatLabelElement( + labelMode, + false, + inputValue, + placeholder, + 'datepicker-input' + )} + + {clearButton && inputValue && (isFocused || currentOpenState) && !readOnly && ( + ) => { + e.preventDefault(); + handleClear(); + }} + > + + + )} + + { + togglePopup(); + setIsIconActive(true); + }} + onKeyDown={handleKeyDown} + > + + + {currentOpenState && createPortal( + <> + {isFullScreenMode && ( +
+
+ { + hidePopup(); + }} + onOpen={() => { + const element: HTMLElement | null = calendarRef.current?.element ?? null; + if (element) { + setTimeout(() => { + element.focus(); + }); + } + if (onOpen) { + onOpen({ + popup: popupRef.current as IPopup + }); + } + }} + relateTo={document.body} + position={{ X: 'center', Y: 'center' }} + collision={{ + X: CollisionType.Fit, + Y: CollisionType.Fit + }} + > + { + if (props.onViewChange) { + props.onViewChange({ + view: args.view, + date: args.date, + event: args.event + }); + } + }} + className={isFullScreenMode ? 'sf-fullscreen-calendar' : ''} + fullScreenMode={isFullScreenMode} + /> + +
+ )} + {!isFullScreenMode && ( + { + hidePopup(); + }} + onOpen={() => { + const element: HTMLElement | null = calendarRef.current?.element ?? null; + if (element) { + setTimeout(() => { + element.focus(); + }); + } + if (onOpen) { + onOpen({ + popup: popupRef.current as IPopup + }); + } + }} + relateTo={containerRef.current as HTMLElement} + position={{ X: 'left', Y: 'bottom' }} + collision={{ + X: CollisionType.Flip, + Y: CollisionType.Flip + }} + > + { + if (props.onViewChange) { + props.onViewChange({ + view: args.view, + date: args.date, + event: args.event + }); + } + }} + /> + + )} + , + document.body + )} +
+ ); + }); + +DatePicker.displayName = 'DatePicker'; +export default DatePicker; diff --git a/components/calendars/src/datepicker/index.ts b/components/calendars/src/datepicker/index.ts new file mode 100644 index 0000000..7503ddd --- /dev/null +++ b/components/calendars/src/datepicker/index.ts @@ -0,0 +1,4 @@ +/** + * DatePicker modules + */ +export * from './datepicker'; diff --git a/components/calendars/src/index.ts b/components/calendars/src/index.ts new file mode 100644 index 0000000..ca7d63d --- /dev/null +++ b/components/calendars/src/index.ts @@ -0,0 +1,9 @@ +/** + * Calendar all modules + */ +export * from './calendar/index'; + +/** + * DatePicker all modules + */ +export * from './datepicker/index'; diff --git a/components/calendars/src/utils/calendar-util.ts b/components/calendars/src/utils/calendar-util.ts new file mode 100644 index 0000000..634658a --- /dev/null +++ b/components/calendars/src/utils/calendar-util.ts @@ -0,0 +1,32 @@ +export const addMonths: (actualDate: Date, i: number) => Date = (actualDate: Date, i: number): Date => { + const date: Date = new Date(actualDate.getTime()); + const day: number = date.getDate(); + date.setDate(1); + date.setMonth(date.getMonth() + i); + date.setDate(Math.min(day, getMaxDays(date))); + return date; +}; + +export const addYears: (date: Date, years: number) => Date = (date: Date, years: number): Date => { + const d: Date = new Date(date); + d.setFullYear(d.getFullYear() + years); + return d; +}; + +export const getWeekNumber: (date: Date) => number = (date: Date): number => { + const currentDate: number = new Date(date).valueOf(); + const firstDayOfYear: number = new Date(date.getFullYear(), 0, 1).valueOf(); + return Math.ceil((((currentDate - firstDayOfYear) + 86400000) / 86400000) / 7); +}; + +const getMaxDays: (d: Date) => number = (d: Date) => { + let date: number = 28; + const tmpDate: Date = new Date(d); + const month: number = tmpDate.getMonth(); + while (tmpDate.getMonth() === month) { + ++date; + tmpDate.setDate(date); + } + return date - 1; +}; + diff --git a/components/calendars/src/utils/index.ts b/components/calendars/src/utils/index.ts new file mode 100644 index 0000000..890e7dc --- /dev/null +++ b/components/calendars/src/utils/index.ts @@ -0,0 +1 @@ +export * from './calendar-util'; diff --git a/components/buttons/styles/button/_all.scss b/components/calendars/styles/calendar/_all.scss similarity index 100% rename from components/buttons/styles/button/_all.scss rename to components/calendars/styles/calendar/_all.scss diff --git a/components/calendars/styles/calendar/_layout.scss b/components/calendars/styles/calendar/_layout.scss new file mode 100644 index 0000000..ebf562a --- /dev/null +++ b/components/calendars/styles/calendar/_layout.scss @@ -0,0 +1,401 @@ +@include export-module('calendar-layout') { + /*! calendar layout */ + + + + .sf-calendar.sf-disabled { + .sf-header { + + .sf-prev, + .sf-next { + cursor: $calendar-cursor-default-style; + pointer-events: $calendar-none-style; + touch-action: $calendar-none-style; + } + + .sf-title { + cursor: $calendar-cursor-default-style; + pointer-events: $calendar-none-style; + touch-action: $calendar-none-style; + } + } + + .sf-content td { + pointer-events: $calendar-none-style; + touch-action: $calendar-none-style; + } + + .sf-btn.sf-today { + pointer-events: $calendar-none-style; + touch-action: $calendar-none-style; + } + } + + .sf-calendar { + border-radius: $calendar-wrapper-border-radius; + display: $calendar-block-style; + overflow: auto; + user-select: $calendar-none-style; + + .sf-rtl .sf-header .sf-title { + float: $calendar-float-right-style; + text-align: $calendar-float-right-style; + } + + .sf-rtl .sf-header .sf-icon-container { + float: $calendar-float-left-style; + } + + .sf-header { + background: $calendar-none-style; + display: flex; + align-items: center; + justify-content: space-between; + font-weight: $calendar-spanicon-font-weight-style; + position: relative; + text-align: center; + width: $calendar-full-width; + + button { + background: transparent; + border: 0; + margin-right: $calendar-icon-button-margin; + padding: 0; + text-decoration: $calendar-none-style; + } + + span { + cursor: $calendar-cursor-pointer-style; + display: inline-block; + font-size: $calendar-icon-font-size-style; + font-weight: $calendar-spanicon-font-weight-style; + line-height: $calendar-icon-line-height; + vertical-align: middle; + + .sf-disabled { + cursor: $calendar-cursor-default-style; + } + } + } + + .sf-week-header { + padding: $calendar-thead-padding; + } + + th { + cursor: $calendar-cursor-default-style; + font-size: $calendar-header-font-size; + font-weight: normal; + text-align: center; + } + + .sf-content { + + .sf-selected, + .sf-state-hover { + border-radius: 0; + } + + span.sf-day { + border-radius: 0; + cursor: $calendar-cursor-pointer-style; + display: $calendar-inline-block-style; + font-size: $calendar-date-font-size; + overflow: hidden; + padding: 0; + text-align: center; + text-decoration: $calendar-none-style; + vertical-align: middle; + } + + th, + td { + box-sizing: border-box; + } + + td.sf-disabled { + opacity: $calendar-disable-opacity; + pointer-events: $calendar-none-style; + touch-action: $calendar-none-style; + } + + td.sf-disabled.sf-today { + opacity: 1; + span.sf-day { + box-shadow: $calendar-disabled-today-box-shadow; + color: $calendar-disabled-today-font-color; + } + } + + td { + cursor: pointer; + padding: $calendar-yeardeacde-span-padding; + text-align: center; + + .sf-week-number { + color: $calendar-week-number-font-color; + font-size: $calendar-week-number-font-size-style; + font-style: $calendar-week-number-font-style; + font-weight: $calendar-week-number-font-weight; + } + + .sf-overlay { + background: $calendar-none-style; + width: initial; + } + } + + table { + border-collapse: separate; + border-spacing: 0; + border-width: 0; + float: $calendar-float-left-style; + margin: 0; + outline: 0; + table-layout: fixed; + width: $calendar-full-width; + } + + td.sf-other-month>span.sf-day, + td.sf-other-year>span.sf-day { + display: $calendar-other-month-display-style; + font-weight: $calendar-link-font-weight-style; + } + + tr.sf-month-hide { + display: $calendar-other-month-row-display-style; + font-weight: $calendar-link-font-weight-style; + } + + tr.sf-month-hide, + td.sf-other-month, + td.sf-other-year { + pointer-events: $calendar-pointer-events; + touch-action: $calendar-pointer-events; + } + + tr.sf-month-hide, + td.sf-other-month.sf-disabled, + td.sf-other-year.sf-disabled { + pointer-events: $calendar-none-style; + touch-action: $calendar-none-style; + } + + td.sf-disabled.sf-overlay { + background: $calendar-none-style; + width: initial; + } + + td.sf-week-number:hover span.sf-day, + td.sf-week-number:hover { + background: $calendar-bg-color; + cursor: $calendar-cursor-default-style; + } + + &.sf-month { + table { + padding: $calendar-table-padding; + } + } + + &.sf-decade, + &.sf-year { + table { + min-height: 200px; + } + } + } + + .sf-header { + + &.sf-decade, + &.sf-year { + .sf-title { + padding: $calendar-header-padding; + } + } + + .sf-title { + cursor: $calendar-cursor-pointer-style; + display: $calendar-inline-block-style; + font-size: $calendar-title-font-size; + font-weight: $calendar-title-font-weight-style; + text-align: $calendar-float-left-style; + padding: $calendar-header-title-padding; + border: $calendar-none-style; + border-radius: $calendar-title-border-radius; + } + + .sf-prev:hover, + .sf-next:hover { + cursor: $calendar-cursor-pointer-style; + } + + .sf-prev.sf-overlay, + .sf-next.sf-overlay { + background: $calendar-none-style; + } + } + + .sf-header.sf-decade .sf-title, + .sf-header.sf-year .sf-title { + margin-left: $calendar-decade-title-left-margin-style; + } + + .sf-header.sf-decade .sf-title { + cursor: $calendar-cursor-default-style; + } + + .sf-header .sf-icon-container { + display: flex; + align-items: center; + gap: 5px; + float: $calendar-float-right-style; + padding-top: $calendar-icon-padding-top; + + .sf-btn.sf-round { + .sf-btn-icon { + line-height: 0px; + } + } + } + + .sf-footer-container { + text-transform: uppercase; + } + } + + //normal style + #{&}.sf-calendar { + border-spacing: 0; + width: $calendar-normal-width; + height: $calendar-normal-height; + padding: $calendar-popup-padding; + + .sf-calendar-day-header-lg { + max-width: $calendar-lg-day-header-format-max-width; + min-width: $calendar-lg-day-header-format-min-width; + } + + &.sf-week-number { + min-width: $calendar-weeknumber-min-width; + } + + .sf-week { + max-width: $calendar-week-normal-max-width; + min-width: $calendar-week-normal-min-width; + } + + .sf-rtl .sf-header .sf-title { + text-align: $calendar-float-right-style; + text-indent: $calendar-rtl-text-indent; + } + + .sf-header { + &.sf-month { + padding: $calendar-normal-header-padding; + } + + &.sf-year, + &.sf-decade { + padding: $calendar-yeardecade-header-padding; + } + } + + th { + font-weight: $calendar-normal-day-header-font-weight; + height: $calendar-normal-day-header-height; + text-transform: $calendar-normal-day-header-text; + } + + .sf-content { + + height: 0; + + .sf-selected, + .sf-state-hover { + border-radius: 0; + } + + span.sf-day { + border: $calendar-default-border-color; + font-size: $calendar-date-font-size; + font-weight: $calendar-link-font-weight-style; + height: $calendar-normal-month-view-height; + line-height: $calendar-normal-month-view-height; + width: $calendar-normal-month-view-width; + } + } + + .sf-content.sf-month td.sf-today span.sf-day { + line-height: $calendar-normal-month-view-line-height; + } + + .sf-content.sf-year table, + .sf-content.sf-decade table { + border-spacing: 0; + padding: $calendar-yeardecade-padding; + } + + .sf-content.sf-month td { + height: $calendar-normal-month-view-height; + padding: $calendar-normal-month-cell-padding; + } + + .sf-content .tfooter>tr>td { + height: $calendar-normal-today-button-height; + line-height: $calendar-normal-today-button-height; + } + + .sf-content.sf-year td, + .sf-content.sf-decade td { + height: $calendar-normal-year-decade-height; + width: $calendar-normal-year-decade-width; + } + + .sf-content.sf-year td>span.sf-day, + .sf-content.sf-decade td>span.sf-day { + font-weight: $calendar-yeardecade-font-weight; + height: $calendar-normal-year-decade-height-inside; + line-height: $calendar-normal-year-decade-height-inside; + width: $calendar-normal-year-decade-width; + } + + .sf-footer-container { + background: $calendar-footer-background; + cursor: $calendar-cursor-default-style; + display: $calendar-display-style; + flex-direction: row; + justify-content: flex-end; + padding: $calendar-footer-container-padding; + width: $calendar-full-width; + } + + .sf-content td { + border: unset; + } + } + + .sf-calendar { + .sf-btn.sf-today.sf-flat.sf-disabled, + .sf-btn.sf-today.sf-flat.sf-disabled:hover, + .sf-btn.sf-today.sf-flat.sf-disabled:active, + .sf-btn.sf-today.sf-flat.sf-disabled:focus, + .sf-btn.sf-today.sf-flat.sf-disabled:hover:active { + background: $calendar-today-disabled-background-style; + border-color: $calendar-today-disabled-border-style; + box-shadow: $calendar-today-disabled-box-shadow; + color: $calendar-today-disabled-color; + cursor: $calendar-cursor-default-style; + opacity: $calendar-disable-opacity; + outline: $calendar-none-style; + pointer-events: $calendar-none-style; + touch-action: $calendar-none-style; + } + } + + .sf-content-placeholder.sf-calendar.sf-placeholder-calendar { + background-size: 250px 336px; + min-height: 336px; + } +} \ No newline at end of file diff --git a/components/calendars/styles/calendar/_material3-definition.scss b/components/calendars/styles/calendar/_material3-definition.scss new file mode 100644 index 0000000..182ddd5 --- /dev/null +++ b/components/calendars/styles/calendar/_material3-definition.scss @@ -0,0 +1,153 @@ +$calendar-title-hover-color: rgba($content-text-color) !default; +$calendar-icon-hover-color: rgba($icon-color) !default; +$calendar-default-border-color: none !default; +$calendar-icon-hover-border-color: none !default; +$calendar-light-font: rgba($content-text-color) !default; +$calendar-bg-color: transparent !default; +$calendar-active-font-color: rgba($primary-text-color) !default; +$calendar-active-hover-font-color: rgba($primary) !default; +$calendar-active-today-font-color: rgba($primary-text-color) !default; +$calendar-active-today-hover-font-color: rgba($primary-text-color) !default; +$calendar-active-bg-color: rgba($primary) !default; +$calendar-active-bg-border-color: 1px solid rgba($primary) !default; +$calendar-active-bg-box-shadow: none !default; +$calendar-hover-color: $flyout-bg-color-hover !default; +$calendar-header-font-color: rgba($content-text-color) !default; +$calendar-header-icon-color: rgba($icon-color) !default; +$calendar-week-header-font-color: rgba($content-text-color-alt1) !default; +$calendar-title-font-color: rgba($content-text-color) !default; +$calendar-title-decoration-style: none !default; +$calendar-icon-font-color: rgba($icon-color) !default; +$calendar-active-icon-color: $calendar-icon-font-color !default; +$calendar-active-state-icon-bg-color: rgba($content-text-color, .12) !default; +$calendar-icon-hover-bg-color: rgba($content-text-color, .08) !default; +$calendar-device-icon-hover-bg-color: none !default; +$calendar-focus-date-color: rgba($flyout-text-color) !default; +$calendar-other-month-date: rgba($content-text-color-alt1, .84) !default; +$calendar-text-color: rgba($content-text-color) !default; +$calendar-hover-text: $calendar-text-color !default; +$calendar-text-color-focus: rgba($primary-text-color) !default; +$calendar-focus-bg-color: rgba($primary) !default; +$calendar-border-style: 1px solid rgba($border-light) !default; +$calendar-box-shadow: none !default; +$calendar-none-style: none !default; +$calendar-cursor-default-style: default !default; +$calendar-cursor-pointer-style: pointer !default; +$calendar-block-style: block !default; +$calendar-inline-block-style: inline-block !default; +$calendar-display-style: flex !default; +$calendar-wrapper-border-radius: 2px !default; +$calendar-full-width: 100% !default; +$calendar-lg-day-header-format-max-width: 100% !default; +$calendar-lg-day-header-format-min-width: 540px !default; +$calendar-header-font-size: 13px !default; +$calendar-title-font-size: 14px !default; +$calendar-focused-state-color: rgba($primary) !default; +$calendar-icon-line-height: 16px !default; +$calendar-bigger-max-width: 296px !default; +$calendar-bigger-min-width: 296px !default; +$calendar-normal-header-padding: 8px 12px 8px 12px !default; +$calendar-normal-width: 320px !default; +$calendar-normal-height: 420px !default; +$calendar-normal-max-width: 320px !default; +$calendar-normal-max-height: 420px !default; +$calendar-normal-min-width: 240px !default; +$calendar-weeknumber-min-width: 360px !default; +$calendar-weeknumber-bigger-width: 320px !default; +$calendar-normal-month-view-height: 40px !default; +$calendar-normal-month-view-line-height: 40px !default; +$calendar-normal-day-header-height: $calendar-normal-month-view-height !default; +$calendar-normal-day-header-text: none !default; +$calendar-normal-year-decade-height: 72px !default; +$calendar-normal-header-height: 40px !default; +$calendar-normal-icon-size: 36px !default; +$calendar-normal-today-button-height: 36px !default; +$calendar-border-radius: 36px !default; +$calendar-pointer-events: initial !default; +$calendar-float-right-style: right !default; +$calendar-icon-padding-top: 0 !default; +$calendar-float-left-style: left !default; +$calendar-table-padding: 0 8px 0 8px !default; +$calendar-zero-value: 0 !default; +$calendar-next-prev-icon-size: 16px !default; +$calendar-prev-icon: '\e910' !default; +$calendar-next-icon: '\e916' !default; +$calendar-date-font-size: 13px !default; +$calendar-yeardecade-bg-color: transparent !default; +$calendar-yeardecade-hover-color: rgba($content-bg-color) !default; +$calendar-title-font-weight-style: $font-weight-medium !default; +$calendar-spanicon-font-weight-style: 500 !default; +$calendar-title-margin-left-style: 5px !default; +$calendar-decade-title-left-margin-style: 5px !default; +$calendar-normal-year-decade-padding: 1px !default; +$calendar-icon-font-size-style: 15px !default; +$calendar-link-font-weight-style: normal !default; +$calendar-yeardeacde-span-padding: 2px !default; +$calendar-yeardecade-padding: 0 12px 0 12px !default; +$calendar-focused-date-bg-style: rgba($content-text-color, .16) !default; +$calendar-focused-today-bg-style: rgba($primary-bg-color, .08) !default; +$calendar-focused-today-border-style: 1px solid rgba($primary-bg-color) !default; +$calendar-focused-today-box-shadow: none !default; +$calendar-yeardecade-header-padding: 10px 10px 0 10px !default; +$calendar-week-header-bg-style: none !default; +$calendar-today-bg-style: none !default; +$calendar-disable-font-weight-style: normal !default; +$calendar-other-month-display-style: inline-block !default; +$calendar-other-month-row-display-style: none !default; +$calendar-week-number-font-style: italic !default; +$calendar-week-number-font-color: rgba($content-text-color-alt1) !default; +$calendar-week-number-color-style: rgba($content-text-color-alt1) !default; +$calendar-week-number-font-size-style: 12px !default; +$calendar-normal-month-cell-padding: 2px !default; +$calendar-yeardecade-hover-bg: $calendar-hover-color !default; +$calendar-normal-day-header-font-weight: normal !default; +$calendar-other-month-date-hover-bg: rgba($flyout-text-color-hover) !default; +$calendar-yeardecade-selected-hover-bg: $calendar-active-bg-color !default; +$calendar-yeardecade-font-weight: normal !default; +$calendar-today-color: rgba($primary) !default; +$calendar-today-focused-font-color: rgba($primary-bg-color) !default; +$calendar-today-focus-color: rgba($primary-bg-color) !default; +$calendar-today-border-color: 1px solid rgba($primary-bg-color) !default; +$calendar-today-box-shadow: none !default; +$calendar-disabled-today-box-shadow: none !default; +$calendar-disabled-today-font-color: $primary-text-disabled !default; +$calendar-focus-border-color: none !default; +$calendar-focus-box-shadow: none !default; +$calendar-hover-border-color: none !default; +$calendar-normal-month-view-width: $calendar-normal-month-view-height !default; +$calendar-normal-year-decade-width: $calendar-normal-year-decade-height !default; +$calendar-normal-year-decade-height-inside: 72px !default; +$calendar-popup-padding: 0 !default; +$calendar-popup-bigger-padding: 0 !default; +$calendar-thead-padding: 0 !default; +$calendar-week-normal-max-width: $calendar-normal-max-width !default; +$calendar-week-normal-min-width: $calendar-normal-min-width !default; +$calendar-week-bigger-max-width: $calendar-bigger-max-width !default; +$calendar-week-bigger-min-width: $calendar-bigger-min-width !default; +$calendar-today-bg-hover-color: rgba($primary-bg-color, .08) !default; +$calendar-disable-font-color: $calendar-light-font !default; +$calendar-disable-opacity: .35 !default; +$calendar-active-hover-bg-color: rgba($primary) !default; +$calendar-other-decade-cell-color: $calendar-other-month-date !default; +$calendar-icon-button-margin: 0 !default; +$calendar-selected-box-shadow: inset 0 0 0 2px $flyout-bg-color !default; +$calendar-selected-border-color: none !default; +$calendar-week-number-font-weight: 500 !default; +$calendar-normar-year-decade-width: 56px !default; +$calendar-header-title-padding: 5px !default; +$calendar-header-padding: 1 5px !default; +$calendar-title-focus-box-shadow: 0 0 0 1px rgba($primary) !default; +$calendar-title-focus-background: none !default; +$calendar-title-active-background: rgba($content-text-color, .12) !default; +$calendar-icon-focus-box-shadow: 0 0 0 1px rgba($primary) !default; +$calendar-title-border-radius: 4px !default; +$calendar-title-hover-bg-color: rgba($content-text-color, .08) !default; +$calendar-focused-cell-box-shadow: 0 0 0 1px rgba($primary) !default; +$calendar-footer-container-padding: 8px 12px 8px 12px !default; +$calendar-footer-border: none !default; +$calendar-footer-background: $calendar-bg-color !default; +$calendar-today-disabled-background-style: $transparent !default; +$calendar-today-disabled-border-style: $transparent !default; +$calendar-today-disabled-box-shadow: none !default; +$calendar-today-disabled-color: $primary-text-disabled !default; +$calendar-rtl-text-indent: 4px !default; diff --git a/components/calendars/styles/calendar/_theme.scss b/components/calendars/styles/calendar/_theme.scss new file mode 100644 index 0000000..620640b --- /dev/null +++ b/components/calendars/styles/calendar/_theme.scss @@ -0,0 +1,295 @@ +@include export-module('calendar-theme') { + #{&}.sf-calendar { + background: $calendar-bg-color; + border-radius: 8px; + border: $calendar-border-style; + box-shadow: $calendar-box-shadow; + + .sf-date-icon-prev, + .sf-date-icon-next { + color: $calendar-header-icon-color; + } + + th { + border-bottom: 0; + color: $calendar-week-header-font-color; + } + + @at-root { + .sf-header { + border-bottom: 0; + + a { + span { + border: $calendar-default-border-color; + color: $calendar-icon-font-color; + } + } + + .sf-title { + color: $calendar-title-font-color; + } + + .sf-title:hover { + background: $calendar-title-hover-bg-color; + cursor: pointer; + text-decoration: $calendar-title-decoration-style; + } + + .sf-title:focus { + box-shadow: $calendar-title-focus-box-shadow; + background: $calendar-title-focus-background; + text-decoration: $calendar-title-decoration-style; + } + + .sf-title:active { + box-shadow: $calendar-box-shadow; + background: $calendar-title-active-background; + text-decoration: $calendar-title-decoration-style; + } + + .sf-prev:hover>span, + .sf-next:hover>span { + border: $calendar-icon-hover-border-color; + cursor: pointer; + } + + .sf-prev:hover, + .sf-next:hover { + background: $calendar-icon-hover-bg-color; + } + + .sf-prev:focus, + .sf-next:focus { + box-shadow: $calendar-icon-focus-box-shadow; + } + + .sf-btn:focus { + background: none; + } + + .sf-prev:active, + .sf-next:active { + background: $calendar-active-state-icon-bg-color; + color: $calendar-active-font-color; + } + + button.sf-prev:active span, + button.sf-next:active span { + border: $calendar-selected-border-color; + color: $calendar-active-icon-color; + } + + .sf-decade .sf-title { + color: $calendar-light-font; + cursor: default; + } + + .sf-next.sf-disabled span, + .sf-prev.sf-disabled span { + color: $calendar-disable-font-color; + font-weight: $calendar-disable-font-weight-style; + } + + .sf-next.sf-disabled, + .sf-prev.sf-disabled { + opacity: $calendar-disable-opacity; + } + } + + .sf-content { + + &.sf-decade tr:first-child .sf-cell:first-child span.sf-day, + &.sf-decade tr:last-child .sf-cell:last-child span.sf-day { + color: $calendar-other-decade-cell-color; + } + + &.sf-decade tr:first-child .sf-cell:first-child.sf-selected span.sf-day, + &.sf-decade tr:last-child .sf-cell:last-child.sf-selected span.sf-day { + color: $calendar-active-font-color; + } + + &.sf-decade tr:first-child .sf-cell.sf-disabled:first-child span.sf-day, + &.sf-decade tr:last-child .sf-cell.sf-disabled:last-child span.sf-day { + color: $calendar-disable-font-color; + } + + &.sf-year td:hover span.sf-day, + &.sf-decade td:hover span.sf-day { + background: $calendar-hover-color; + } + + &.sf-year td.sf-selected:hover span.sf-day, + &.sf-decade td.sf-selected:hover span.sf-day { + background: $calendar-yeardecade-selected-hover-bg; + } + + &.sf-year td>span.sf-day, + &.sf-decade td>span.sf-day { + background: $calendar-yeardecade-bg-color; + } + + .sf-week-number { + color: $calendar-other-month-date; + } + + td.sf-focused-date span.sf-day, + td.sf-focused-date:hover span.sf-day, + td.sf-focused-date:focus span.sf-day { + background: $calendar-focused-date-bg-style; + border: $calendar-focus-border-color; + border-radius: $calendar-border-radius; + box-shadow: $calendar-focus-box-shadow; + } + + td.sf-focused-date:hover span.sf-day { + background: $calendar-hover-color; + border: $calendar-hover-border-color; + border-radius: $calendar-border-radius; + color: $calendar-text-color; + } + + td.sf-today span.sf-day, + td.sf-focused-date.sf-today span.sf-day { + background: $calendar-today-bg-style; + border: $calendar-today-border-color; + border-radius: $calendar-border-radius; + box-shadow: $calendar-today-box-shadow; + color: $calendar-today-color; + } + + td.sf-focused-date.sf-today span.sf-day { + background: $calendar-focused-today-bg-style; + border: $calendar-focused-today-border-style; + box-shadow: $calendar-focused-today-box-shadow; + color: $calendar-today-focused-font-color; + } + + td.sf-today:focus span.sf-day, + td.sf-focused-date.sf-today:focus span.sf-day { + background: $calendar-focus-bg-color; + border: $calendar-focus-border-color; + border-radius: $calendar-border-radius; + color: $calendar-today-focus-color; + } + + td.sf-today:hover span.sf-day, + td.sf-focused-date.sf-today:hover span.sf-day, + td.sf-focused-date.sf-today:focus span.sf-day { + background: $calendar-hover-color; + border: $calendar-today-border-color; + color: $calendar-today-focus-color; + } + + td.sf-today.sf-selected span.sf-day { + background: $calendar-active-bg-color; + border: $calendar-active-bg-border-color; + box-shadow: $calendar-selected-box-shadow; + color: $calendar-active-today-font-color; + } + + td.sf-today.sf-selected:hover span.sf-day, + td.sf-selected:hover span.sf-day, + td.sf-selected.sf-focused-date span.sf-day { + background: $calendar-active-hover-bg-color; + color: $calendar-active-today-hover-font-color; + } + + .sf-disabled span.sf-day:hover { + background: $calendar-none-style; + border: 0; + color: $calendar-disable-font-color; + } + + .sf-other-month:hover span.sf-day { + color: $calendar-other-month-date-hover-bg; + } + + .sf-other-month span.sf-day, + .sf-other-month.sf-today span.sf-day { + color: $calendar-other-month-date; + } + + .sf-other-month.sf-today:hover span.sf-day { + background: $calendar-hover-color; + color: $calendar-other-month-date; + } + + thead { + background: $calendar-week-header-bg-style; + border-bottom: 0; + } + + td:hover span.sf-day, + td:focus span.sf-day { + background: $calendar-hover-color; + border: $calendar-hover-border-color; + border-radius: $calendar-border-radius; + color: $calendar-hover-text; + } + + td:focus span.sf-day { + background: $calendar-focus-bg-color; + color: $calendar-text-color-focus; + border: $calendar-focus-border-color; + border-radius: $calendar-border-radius; + } + + td.sf-disabled span.sf-day, + td.sf-disabled:hover span.sf-day, + td.sf-disabled:focus span.sf-day { + background: $calendar-none-style; + border: $calendar-none-style; + color: $calendar-disable-font-color; + } + + td.sf-selected span.sf-day { + background: $calendar-active-bg-color; + color: $calendar-active-font-color; + border: $calendar-selected-border-color; + border-radius: $calendar-border-radius; + } + + .sf-footer { + color: $calendar-active-bg-color; + } + } + } + + .sf-device { + + .sf-prev:hover, + .sf-next:hover, + .sf-prev:active, + .sf-next:active, + .sf-prev:focus, + .sf-next:focus { + background: $calendar-device-icon-hover-bg-color; + } + + button.sf-prev:active span, + button.sf-next:active span { + color: $calendar-header-icon-color; + } + } + } + + .sf-calendar .sf-zoomin { + animation: animatezoom .3s; + } + + @keyframes animatezoom { + from { + transform: scale(0); + } + + to { + transform: scale(1); + } + } + + .sf-calendar .sf-calendar-content-table .sf-cell.sf-focused-cell span.sf-day { + box-shadow: $calendar-focused-cell-box-shadow; + border-radius: $calendar-border-radius; + } +} diff --git a/components/calendars/styles/calendar/material3.scss b/components/calendars/styles/calendar/material3.scss new file mode 100644 index 0000000..ddbe045 --- /dev/null +++ b/components/calendars/styles/calendar/material3.scss @@ -0,0 +1,4 @@ +@import '../../base/themes/material3.scss'; +@import '../../buttons/button/material3-definition.scss'; +@import 'material3-definition.scss'; +@import 'all.scss'; diff --git a/components/notifications/styles/toast/_all.scss b/components/calendars/styles/datepicker/_all.scss similarity index 52% rename from components/notifications/styles/toast/_all.scss rename to components/calendars/styles/datepicker/_all.scss index f4d67e8..b74a392 100644 --- a/components/notifications/styles/toast/_all.scss +++ b/components/calendars/styles/datepicker/_all.scss @@ -1 +1,2 @@ @import 'layout.scss'; +@import 'theme.scss'; \ No newline at end of file diff --git a/components/calendars/styles/datepicker/_layout.scss b/components/calendars/styles/datepicker/_layout.scss new file mode 100644 index 0000000..e3e0fd2 --- /dev/null +++ b/components/calendars/styles/datepicker/_layout.scss @@ -0,0 +1,398 @@ +@include export-module('datepicker-layout') { + + .sf-input-group.sf-control-wrapper.sf-date-wrapper.sf-non-edit.sf-input-focus .sf-input:focus ~ .sf-clear-icon, + .sf-float-input.sf-control-wrapper.sf-input-group.sf-date-wrapper.sf-non-edit.sf-input-focus input:focus ~ .sf-clear-icon { + display: flex; + } + + .sf-date-wrapper:not(.sf-filled) { + .sf-date-icon.sf-icons { + box-sizing: $datepicker-box-sizing; + } + } + + #{&}.sf-datepicker { + .sf-calendar { + + .sf-content table tbody tr.sf-month-hide:last-child { + display: $datepicker-othermonth-row; + } + } + + #{if(&, '&', '*')}.sf-popup-wrapper { + border-radius: $datepicker-popup-border-radius; + overflow-y: hidden; + pointer-events: auto; + } + + #{if(&, '&', '*')}.sf-date-modal { + background: $datepicker-overlay; + height: 100%; + left: 0; + opacity: .5; + pointer-events: auto; + position: fixed; + top: 0; + width: 100%; + z-index: 999; + } + + .sf-model-header { + background: $datepicker-modal-header-bg; + color: $datepicker-modal-header-color; + cursor: default; + display: $datepicker-modal-header-display; + padding: 10px 10px 10px 15px; + + .sf-model-year { + font-size: $modal-year-font-size; + font-weight: $modal-year-font-weight; + line-height: $modal-year-line-height; + margin: 0; + } + } + + .sf-model-month, + .sf-model-day { + font-size: $modal-month-font-size; + font-weight: $modal-month-font-weight; + line-height: $modal-month-line-height; + margin: 0; + } + } +} + +/* stylelint-disable */ +.sf-date-overflow { + overflow: hidden !important; +} + +.sf-datepick-mob-popup-wrap { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + left: 0; + max-height: 100%; + position: fixed; + top: 0; + width: 100%; + z-index: 1002; + + .sf-datepicker.sf-popup-wrapper.sf-lib.sf-popup.sf-control.sf-popup-open { + position: relative; + top: 0 !important; + left: 0 !important; + } + + .sf-datepicker.sf-popup-wrapper.sf-popup-expand.sf-lib.sf-popup.sf-control.sf-popup-open { + min-width: 100%; + min-height: 100%; + } +} + +.sf-content-placeholder.sf-datepicker.sf-placeholder-datepicker { + background-size: 250px 33px; + min-height: 33px; +} + +@media screen and (orientation: landscape) { + .sf-datepick-mob-popup-wrap .sf-datepicker.sf-popup-expand .sf-calendar .sf-content.sf-month td.sf-today span.sf-day { + line-height: $modal-tablet-content-dimension; + } +} + +.sf-datepick-mob-popup-wrap { + .sf-datepicker.sf-popup-expand { + border-radius: 0; + + .sf-model-header.sf-blazor-device { + height: 15vh; + .sf-popup-close { + float: right; + } + } + + .sf-model-header.sf-blazor-device { + height: 15vh; + .sf-popup-close { + float: right; + } + } + + .sf-model-header { + height: $modal-portrait-header-height; + padding: $modal-portrait-header-padding; + + .sf-day-wrapper { + margin: $modal-portrait-header-year-margin; + } + + .sf-popup-close { + color: $modal-portrait-cancel-icon-color; + float: $modal-portrait-cancel-icon-float-style; + font-size: $datepicker-bigger-icon-font-size; + @media (min-device-width: 768px) { + font-size: $modal-tablet-font-size; + } + } + + .sf-btn.sf-flat.sf-popup-close { + background: transparent; + border-color: transparent; + box-shadow: none; + font-weight: 400; + padding: 0; + } + + .sf-today.sf-flat.sf-primary { + color: $modal-today-text-color; + float: $modal-portrait-today-float-style; + @media (min-device-width: 768px) { + font-size: $modal-tablet-font-size; + } + } + } + + .sf-model-month, + .sf-model-day { + font-size: $modal-portrait-month-font-size; + line-height: $modal-header-day-line-height; + } + + .sf-calendar { + min-width: $modal-portrait-calendar-min-width; + min-height: $modal-portrait-calendar-min-height; + height: $modal-portrait-calendar-height; + .sf-header.sf-month, + .sf-header.sf-year, + .sf-header.sf-decade { + height: $modal-portrait-header-month-height; + border-style: $modal-header-border-style; + border-width: $modal-header-border-width; + border-color: $modal-header-border-color; + padding: $modal-portrait-month-header-padding; + line-height: $modal-month-header-line-height; + + .sf-title { + position: $modal-portrait-header-title-position; + line-height: $modal-month-header-title-line-height; + margin-left: $modal-portrait-header-title-margin; + text-align: $modal-portrait-header-title-text; + vertical-align: $modal-portrait-month-header-vertical-align; + width: $modal-portrait-header-title-width; + @media (min-device-width: 768px) { + font-size: $modal-tablet-font-size; + } + } + + .sf-prev { + margin-right: $modal-portrait-prev-icon-margin; + vertical-align: inherit; + height: 35px; + width: 35px; + @media (min-device-width: 768px) { + height: 55px; + width: 55px; + } + + .sf-icons { + vertical-align: inherit; + @media (min-device-width: 768px) { + font-size: $modal-tablet-font-size; + } + } + } + + .sf-next { + margin-right: $modal-portrait-next-icon-margin; + vertical-align: inherit; + height: 35px; + width: 35px; + @media (min-device-width: 768px) { + height: 55px; + width: 55px; + } + + .sf-icons { + vertical-align: inherit; + @media (min-device-width: 768px) { + font-size: $modal-tablet-font-size; + } + } + } + + .sf-icon-container { + float: $modal-portrait-icon-float; + } + } + + th { + @media (min-device-width: 768px) { + font-size: $modal-tablet-font-size; + height: $modal-tablet-table-header-height; + } + } + + .sf-content span.sf-day { + @media (min-device-width: 768px) { + font-size: $modal-tablet-font-size; + height: $modal-tablet-content-dimension; + width: $modal-tablet-content-dimension; + line-height: $modal-tablet-content-dimension; + } + } + + } + + .sf-calendar-cell-container { + height: $modal-portrait-calendar-container-height; + } + + .sf-footer-container + { + display: $modal-portrait-footer-display-style; + } + + .sf-content.sf-month + { + height: $modal-portrait-calendar-content-height; + table + { + height: $modal-portrait-calendar-tabel-height; + } + + } + + } +} + +@media screen and (orientation: landscape) { + .sf-datepick-mob-popup-wrap { + .sf-datepicker.sf-popup-expand { + + .sf-model-header.sf-blazor-device { + height: 25vh; + .sf-popup-close { + float: right; + } + } + + .sf-calendar-cell-container { + + .sf-content.sf-month, + .sf-content.sf-year, + .sf-content.sf-decade { + @media (max-height: 600px) { + height: 50vh; + } + + @media (min-height: 600px) { + height: 60vh; + } + overflow-y: auto; + + table { + @media (min-height: 600px) { + height: 60vh; + } + } + } + .sf-footer-container { + @media (min-height: 600px) { + padding: 10px 0; + height: 10%; + font-size: 24px; + } + } + } + + .sf-model-header { + + @media (max-height: 600px) { + height: $modal-landscape-header-height; + } + + @media (min-height: 600px) { + height: $modal-landscape-header-big-height; + } + width: $datepicker-modal-popup-landscape-max-width; + + .sf-popup-close { + float: $modal-portrait-cancel-icon-float-style; + } + + .sf-day-wrapper { + margin: $modal-landscape-header-year-margin; + } + + .sf-model-month, + .sf-model-day { + font-size: $modal-month-landscape-font-size; + } + + } + + .sf-calendar .sf-header.sf-month, + .sf-calendar .sf-header.sf-year, + .sf-calendar .sf-header.sf-decade { + + .sf-prev { + margin-right: $modal-landscape-prev-icon-margin; + } + + .sf-title { + @media (max-height: 600px) { + line-height: $modal-month-landscape-title-line-height; + } + + @media (min-height: 600px) { + line-height: $modal-month-header-title-line-height; + } + } + } + + .sf-content.sf-month, + .sf-content.sf-year, + .sf-content.sf-decade { + @media (max-height: 600px) { + height: $modal-landscape-calendar-content-height; + } + + @media (min-height: 600px) { + height: $modal-landscape-calendar-content-big-height; + } + overflow-y: auto; + } + + .sf-calendar { + display: $datepicker-modal-header-display; + max-width: $datepicker-modal-popup-landscape-max-width; + overflow: $datepicker-modal-landscape-overflow; + } + + .sf-calendar-cell-container { + height: 70%; + width: 100%; + } + + .sf-calendar.sf-device .sf-month table tbody { + display: table-row-group; + } + + .sf-content.sf-month table , + .sf-content.sf-decade table , + .sf-content.sf-year table { + @media (max-height: 600px) { + height: $modal-portrait-calendar-content-height; + } + + @media (min-height: 600px) { + height: $modal-landscape-calendar-content-table-height; + } + } + } +} +} +/* stylelint-enable */ \ No newline at end of file diff --git a/components/calendars/styles/datepicker/_material3-definition.scss b/components/calendars/styles/datepicker/_material3-definition.scss new file mode 100644 index 0000000..f721e7f --- /dev/null +++ b/components/calendars/styles/datepicker/_material3-definition.scss @@ -0,0 +1,88 @@ +$datepicker-icon-color: rgba($icon-color) !default; +$datepicker-active-icon-color: rgba($content-text-color-alt1) !default; +$datepicker-popup-box-shadow: $shadow-lg !default; +$datepicker-input-border-style: none !default; +$datepicker-calendar-border-style: none !default; +$datepicker-icon: '\e778' !default; +$datepicker-popup-bg-color: $flyout-bg-color !default; +$datepicker-popup-border: none !default; +$datepicker-icon-font-size: 16px !default; +$datepicker-icon-container-min-height: 30px !default; +$datepicker-icon-container-min-width: 30px !default; +$datepicker-small-icon-container-min-width: 22px !default; +$datepicker-small-icon-container-min-height: 22px !default; +$datepicker-small-icon-border-radius: 14px !default; +$datepicker-mouse-icon-border-radius: 16px !default; +$datepicker-bigger-icon-font-size: 20px !default; +$datepicker-overlay: $content-bg-color-alt1 !default; +$datepicker-icon-normal-margin: 0 !default; +$datepicker-small-icon-margin: 0 !default; +$datepicker-icon-hover-color: rgba($content-text-color, .08) !default; +$datepicker-icon-border-radius: 50% !default; +$datepicker-popup-border-radius: 8px !default; +$datepicker-box-sizing: border-box !default; +$datepicker-othermonth-row: none !default; +$datepicker-modal-header-bg: $flyout-bg-color !default; +$datepicker-modal-header-color: rgba($content-text-color) !default; +$datepicker-modal-header-display: block !default; +$datepicker-calendar-tbody-landscape-height: 130px !default; +$datepicker-clearicon: '\e7e7' !default; +$datepicker-icon-active-border: 1px !default; +$datepicker-icon-active-border-radius: 16px !default; +$datepicker-icon-active-bg-color: rgba($content-text-color, .08) !default; +$datepicker-small-icon-font-size: $font-icon-16 !default; +$zero-value: 0 !default; +$modal-year-font-size: 14px !default; +$modal-year-font-weight: 500 !default; +$modal-year-line-height: 32px !default; +$modal-month-font-size: 20px !default; +$modal-month-font-weight: 500 !default; +$modal-month-line-height: 32px !default; +$modal-portrait-year-font-size: 4vw !default; +$modal-portrait-header-year-margin: 12vh 0 0 0 !default; +$modal-portrait-month-font-size: 5vw !default; +$modal-portrait-header-padding: 2vh 6vw !default; +$modal-portrait-month-header-padding: 2vh 2vw !default; +$modal-portrait-month-header-vertical-align: middle !default; +$modal-month-header-line-height: 5vh !default; +$modal-month-header-title-line-height: inherit !default; +$modal-header-border-style: none !default; +$modal-header-border-width: 0 !default; +$modal-header-border-color: rgba($border-light) !default; +$modal-portrait-cancel-icon-float-style: left !default; +$modal-portrait-cancel-icon-padding: 2px !default; +$modal-portrait-prev-icon-margin: 18px !default; +$modal-portrait-next-icon-margin: 0 !default; +$modal-portrait-header-title-margin: 5vw !default; +$modal-portrait-header-title-position: inherit !default; +$modal-portrait-header-title-text: left !default; +$modal-portrait-header-title-width: 60vw !default; +$modal-portrait-cancel-icon-color: inherit !default; +$modal-header-day-line-height: 6vw !default; +$modal-portrait-today-float-style: right !default; +$modal-today-text-color: inherit !default; +$modal-portrait-footer-display-style: none !default; +$modal-portrait-icon-float: right !default; +$modal-portrait-header-height: 20vh !default; +$modal-portrait-header-month-height: 10vh !default; +$modal-portrait-calendar-container-height: 79vh !default; +$modal-portrait-calendar-min-height: 100% !default; +$modal-portrait-calendar-min-width: 100% !default; +$modal-portrait-calendar-height: 100% !default; +$modal-portrait-calendar-content-height: 69vh !default; +$modal-portrait-calendar-tabel-height: 69vh !default; +$datepicker-modal-popup-landscape-max-width: 100% !default; +$modal-landscape-header-height: 30vh !default; +$modal-landscape-header-big-height: 25vh !default; +$modal-landscape-calendar-content-height: 60vh !default; +$modal-landscape-calendar-content-table-height: 65vh !default; +$modal-landscape-calendar-content-big-height: 65vh !default; +$modal-landscape-header-year-margin: 12vh 0 0 0 !default; +$modal-landscape-prev-icon-margin: $modal-portrait-prev-icon-margin !default; +$modal-year-landscape-font-size: 3vw !default; +$modal-month-landscape-font-size: 4vw !default; +$datepicker-modal-landscape-overflow: visible !default; +$modal-month-landscape-title-line-height: 12vh !default; +$modal-tablet-font-size: 18px !default; +$modal-tablet-content-dimension: 64px !default; +$modal-tablet-table-header-height: 48px !default; diff --git a/components/calendars/styles/datepicker/_theme.scss b/components/calendars/styles/datepicker/_theme.scss new file mode 100644 index 0000000..8faa16f --- /dev/null +++ b/components/calendars/styles/datepicker/_theme.scss @@ -0,0 +1,30 @@ +@include export-module('datepicker-theme') { + .sf-datepicker, + .sf-input-group { + .sf-date-wrapper.sf-dateinput-active:active:not(.sf-success):not(.sf-warning):not(.sf-error) { + border: $datepicker-input-border-style; + } + &.sf-popup-wrapper { + border: $datepicker-popup-border; + box-shadow: $datepicker-popup-box-shadow; + + .sf-calendar { + background: $datepicker-popup-bg-color; + border: $datepicker-calendar-border-style; + } + } + } + + .sf-date-wrapper { + span.sf-input-group-icon { + .sf-date-icon.sf-icons.sf-active { + color: $datepicker-icon-color; + } + .sf-date-icon.sf-icons.sf-active { + color: $datepicker-active-icon-color; + border: $datepicker-icon-active-border; + background: $datepicker-icon-active-bg-color; + } + } + } +} diff --git a/components/calendars/styles/datepicker/material3.scss b/components/calendars/styles/datepicker/material3.scss new file mode 100644 index 0000000..a712397 --- /dev/null +++ b/components/calendars/styles/datepicker/material3.scss @@ -0,0 +1,5 @@ +@import '../../base/themes/material3.scss'; +@import '../../inputs/input/material3-definition.scss'; +@import '../../popups/popup/material3-definition.scss'; +@import 'material3-definition.scss'; +@import 'all.scss'; diff --git a/components/calendars/styles/material3.scss b/components/calendars/styles/material3.scss new file mode 100644 index 0000000..e2eac4d --- /dev/null +++ b/components/calendars/styles/material3.scss @@ -0,0 +1,10 @@ +@import '../base/themes/material3.scss'; +@import '../buttons/button/material3-definition.scss'; +@import '../inputs/input/material3-definition.scss'; +@import '../popups/popup/material3-definition.scss'; +@import 'calendar/material3-definition.scss'; +@import 'calendar/all.scss'; +@import 'datepicker/material3-definition.scss'; +@import 'datepicker/all.scss'; +@import 'timepicker/material3-definition.scss'; +@import 'timepicker/all.scss'; diff --git a/components/buttons/styles/chips/_all.scss b/components/calendars/styles/timepicker/_all.scss similarity index 100% rename from components/buttons/styles/chips/_all.scss rename to components/calendars/styles/timepicker/_all.scss diff --git a/components/calendars/styles/timepicker/_layout.scss b/components/calendars/styles/timepicker/_layout.scss new file mode 100644 index 0000000..2b65b91 --- /dev/null +++ b/components/calendars/styles/timepicker/_layout.scss @@ -0,0 +1,187 @@ +@include export-module('timepicker-layout') { + // timepicker layout + .sf-input-group.sf-control-wrapper.sf-time-wrapper.sf-non-edit.sf-input-focus .sf-input:focus ~ .sf-clear-icon, + .sf-float-input.sf-control-wrapper.sf-input-group.sf-time-wrapper.sf-non-edit.sf-input-focus input:focus ~ .sf-clear-icon { + display: flex; + } + + .sf-time-wrapper, + .sf-control-wrapper.sf-time-wrapper { + .sf-time-icon.sf-icons { + font-size: $timepicker-icon-normal-font-size; + min-height: $timepicker-normal-input-min-height; + min-width: $timepicker-normal-input-min-width; + border-radius: $timepicker-normal-icon-border-radius; + margin: $timepicker-icon-margin; + } + + .sf-time-icon.sf-icons.sf-disabled { + pointer-events: none; + } + + span { + cursor: pointer; + } + } + + #{&}.sf-timepicker.sf-time-modal { + background: $timepicker-default-overlay; + height: 100%; + left: 0; + opacity: .5; + pointer-events: auto; + position: fixed; + top: 0; + width: 100%; + z-index: 999; + } + + #{&}.sf-timepicker.sf-popup { + border-style: solid; + border-width: 1px; + overflow: auto; + + .sf-content { + position: relative; + } + + .sf-list-parent.sf-ul { + margin: 0; + padding: $timepicker-list-normal-padding 0; + + .sf-list-item { + cursor: default; + font-size: $timepicker-list-normal-font-size; + overflow: hidden; + position: relative; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + width: 100%; + } + + .sf-list-item.sf-hover { + cursor: pointer; + } + } + } + + #{&}.sf-timepicker.sf-popup { + .sf-list-parent.sf-ul .sf-list-item { + line-height: $timepicker-list-normal-line-height; + text-indent: $timepicker-list-normal-text-indent; + } + } + + .sf-content-placeholder.sf-timepicker.sf-placeholder-timepicker { + background-size: 250px 33px; + min-height: 33px; + } +} + +.sf-time-overflow { + overflow: hidden; +} + +.sf-timepicker-mob-popup-wrap { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + left: 0; + max-height: 100%; + position: fixed; + top: 0; + width: 100%; + z-index: 1002; + + .sf-timepicker.sf-popup.sf-lib.sf-control.sf-popup-open { + left: 0 !important; + position: relative; + top: 0 !important; + } +} + +.sf-timepicker-mob-popup-wrap .sf-popup-expand.sf-timepicker.sf-popup, +.sf-datetimepicker.sf-popup-expand.sf-timepicker.sf-popup, +.sf-timepicker-mob-popup-wrap .sf-popup-expand.sf-datetimepicker.sf-popup, +.sf-datetimepicker.sf-popup-expand.sf-popup { + border-radius: 0; +} + +.sf-timepicker-mob-popup-wrap .sf-popup-expand, +.sf-datetimepicker.sf-popup-expand { + .sf-model-header { + height: $modal-header-height; + padding: $modal-header-padding; + display: $modal-header-display-style; + align-items: $modal-header-content-align; + font-size: $modal-header-portrait-font-size; + border-bottom: $modal-header-border-bottom; + + .sf-popup-close { + float: $modal-close-icon-float; + padding: $modal-portrait-content-padding; + } + + .sf-model-title { + padding: $modal-portrait-content-padding; + text-transform: $modal-header-title-transform; + font-weight: 500; + } + + .sf-btn.sf-popup-close { + font-size: $modal-header-portrait-font-size; + } + } + + .sf-content { + height: $modal-content-height; + overflow: $modal-content-overflow; + + .sf-list-parent.sf-ul .sf-list-item { + padding: $modal-list-item-padding; + line-height: $modal-list-line-height; + + @media (max-device-width: 768px) { + font-size: $modal-mobile-font-size; + } + + @media (min-device-width: 768px) { + font-size: $modal-tablet-font-size; + } + } + } +} + +@media screen and (orientation: landscape) { + .sf-timepicker-mob-popup-wrap .sf-popup-expand, + .sf-datetimepicker.sf-popup-expand { + .sf-model-header { + height: $modal-header-landscape-height; + font-size: $modal-header-landscape-font-size; + + .sf-popup-close { + padding: $modal-landscape-padding; + } + + .sf-model-title { + padding: $modal-landscape-padding; + } + + .sf-btn.sf-popup-close { + font-size: $modal-header-landscape-font-size; + } + } + + .sf-content { + height: $modal-content-landscape-height; + + .sf-list-parent.sf-ul .sf-list-item { + padding: $modal-list-item-padding; + line-height: $modal-landscape-list-line-height; + } + } + } +} \ No newline at end of file diff --git a/components/calendars/styles/timepicker/_material3-definition.scss b/components/calendars/styles/timepicker/_material3-definition.scss new file mode 100644 index 0000000..3ee3c68 --- /dev/null +++ b/components/calendars/styles/timepicker/_material3-definition.scss @@ -0,0 +1,83 @@ +$timepicker-skin-name: 'Material3' !default; +$timepicker-default-text-indent: 16px !default; +$timepicker-list-bigger-line-height: 40px !default; +$timepicker-list-normal-line-height: 32px !default; +$timepicker-list-normal-font-size: 13px !default; +$timepicker-list-bigger-font-size: 14px !default; +$timepicker-list-normal-text-indent: $timepicker-default-text-indent !default; +$timepicker-list-bigger-text-indent: 16px !default; +$timepicker-popup-border-radius: 4px !default; +$timepicker-list-font-weight: normal !default; +$timepicker-popup-shadow: $shadow-md !default; +$timepicker-list-default-font-color: rgba($flyout-text-color) !default; +$timepicker-list-default-border-style: none !default; +$timepicker-list-border-color: none !default; +$timepicker-list-hover-border-color: none !default; +$timepicker-list-bg-color: $flyout-bg-color !default; +$timepicker-list-active-bg-color: rgba($primary) !default; +$timepicker-list-active-font-color: rgba($primary-text-color) !default; +$timepicker-list-active-icon-color: rgba($icon-color) !default; +$timepicker-list-hover-bg-color: $flyout-bg-color-hover !default; +$timepicker-list-hover-font-color: rgba($flyout-text-color-hover) !default; +$timepicker-list-popup-icon-active-color: rgba($icon-color) !default; +$timepicker-list-active-hover-bg-color: rgba($primary) !default; +$timepicker-list-active-hover-font-color: rgba($primary-text-color) !default; +$timepicker-list-normal-padding: 0 !default; +$timepicker-list-bigger-padding: 0 !default; +$timepicker-font-icon: '\e20c' !default; +$timepicker-icon-normal-font-size: $font-icon-16 !default; +$timepicker-icon-bigger-font-size: $font-icon-20 !default; +$timepicker-normal-input-min-height: 30px !default; +$timepicker-normal-input-min-width: 30px !default; +$timepicker-bigger-input-min-height: 38px !default; +$timepicker-bigger-input-min-width: 38px !default; +$timepicker-disable-text: $primary-text-disabled !default; +$timepicker-disable-opacity: 1 !default; +$timepicker-default-overlay: $content-bg-color-alt1 !default; +$timepicker-icon-padding: 4px !default; +$timepicker-normal-icon-border-radius: 16px !default; +$timepicker-bigger-icon-border-radius: 20px !default; +$timepicker-icon-active-border: 1px !default; +$timepicker-icon-active-border-radius: 20px; +$timepicker-icon-active-bg-color: rgba($content-text-color, .12) !default; +$timepicker-bigger-icon-margin: 0 !default; +$timepicker-icon-margin: 0 !default; +$timepicker-time-small-icon-margin: 0 !default; +$timepicker-bigger-small-icon-margin: 0 !default; +$timepicker-list-small-font-size: 12px !default; +$timepicker-list-small-line-height: 26px !default; +$timepicker-list-small-text-indent: 12px !default; +$timepicker-list-small-font-color: rgba($content-text-color) !default; +$timepicker-icon-small-font-size: $font-icon-16 !default; +$timepicker-small-input-min-height: 22px !default; +$timepicker-small-input-min-width: 22px !default; +$timepicker-small-icon-border-radius: 14px !default; +$timepicker-list-bigger-small-font-size: 13px !default; +$timepicker-list-bigger-small-line-height: 36px !default; +$timepicker-list-bigger-small-text-indent: 16px !default; +$timepicker-icon-bigger-small-font-size: $font-icon-20 !default; +$timepicker-bigger-small-input-min-height: 34px !default; +$timepicker-bigger-small-input-min-width: 34px !default; +$timepicker-bigger-small-icon-border-radius: 20px !default; +$modal-header-bg-color: rgba($primary) !default; +$modal-header-text-color: rgba($primary-text-color) !default; +$modal-header-height: 10% !default; +$modal-header-padding: 2.5vh 2.5vw 2.5vh 1.5vw !default; +$modal-header-display-style: flex !default; +$modal-header-content-align: center !default; +$modal-header-portrait-font-size: 2vh !default; +$modal-close-icon-float: left !default; +$modal-portrait-content-padding: 1vh 2vw !default; +$modal-content-height: 90% !default; +$modal-content-overflow: auto !default; +$modal-header-title-transform: uppercase !default; +$modal-header-border-bottom: none !default; +$modal-list-line-height: 5vh !default; +$modal-landscape-list-line-height: 10vh !default; +$modal-header-landscape-height: 15% !default; +$modal-content-landscape-height: 85% !default; +$modal-header-landscape-font-size: 2vw !default; +$modal-landscape-padding: 1vh 1vw !default; +$modal-list-item-padding: 1vh 0 !default; +$modal-mobile-font-size: 14px !default; +$modal-tablet-font-size: 24px !default; diff --git a/components/calendars/styles/timepicker/_theme.scss b/components/calendars/styles/timepicker/_theme.scss new file mode 100644 index 0000000..d89fda3 --- /dev/null +++ b/components/calendars/styles/timepicker/_theme.scss @@ -0,0 +1,69 @@ +@include export-module('timepicker-theme') { + .sf-time-wrapper { + .sf-input-group-icon.sf-icons.sf-active { + color: $timepicker-list-active-icon-color; + } + + .sf-input-group:not(.sf-disabled) .sf-input-group-icon.sf-active:active { + color: $timepicker-list-popup-icon-active-color; + border: $timepicker-icon-active-border; + border-radius: $timepicker-icon-active-border-radius; + background: $timepicker-icon-active-bg-color; + } + } + + #{&}.sf-timepicker.sf-popup { + background-color: $timepicker-list-bg-color; + background: $timepicker-list-bg-color; + border: $timepicker-list-border-color; + border-radius: $timepicker-popup-border-radius; + box-shadow: $timepicker-popup-shadow; + + .sf-list-parent.sf-ul { + background: $timepicker-list-bg-color; + li.sf-list-item { + border: $timepicker-list-default-border-style; + color: $timepicker-list-default-font-color; + } + + .sf-list-item.sf-disabled { + color: $timepicker-disable-text; + opacity: $timepicker-disable-opacity; + pointer-events: none; + touch-action: none; + } + + .sf-list-item.sf-hover, + .sf-list-item.sf-navigation, + .sf-list-item:focus { + background: $timepicker-list-hover-bg-color; + border: $timepicker-list-hover-border-color; + color: $timepicker-list-hover-font-color; + } + + .sf-list-item.sf-active { + background: $timepicker-list-active-bg-color; + color: $timepicker-list-active-font-color; + } + + .sf-list-item.sf-active.sf-hover { + background: $timepicker-list-active-hover-bg-color; + color: $timepicker-list-active-hover-font-color; + } + } + } + + .sf-timepicker-mob-popup-wrap .sf-timepicker.sf-popup-expand, + .sf-datetimepicker.sf-popup-expand { + + .sf-model-header { + background-color: $modal-header-bg-color; + color: $modal-header-text-color; + + .sf-popup-close { + color: $modal-header-text-color; + font-weight: 500; + } + } + } +} diff --git a/components/calendars/styles/timepicker/material3.scss b/components/calendars/styles/timepicker/material3.scss new file mode 100644 index 0000000..a712397 --- /dev/null +++ b/components/calendars/styles/timepicker/material3.scss @@ -0,0 +1,5 @@ +@import '../../base/themes/material3.scss'; +@import '../../inputs/input/material3-definition.scss'; +@import '../../popups/popup/material3-definition.scss'; +@import 'material3-definition.scss'; +@import 'all.scss'; diff --git a/components/buttons/tsconfig.json b/components/calendars/tsconfig.json similarity index 100% rename from components/buttons/tsconfig.json rename to components/calendars/tsconfig.json diff --git a/components/charts/CHANGELOG.md b/components/charts/CHANGELOG.md new file mode 100644 index 0000000..563e074 --- /dev/null +++ b/components/charts/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +## [Unreleased] + +## 31.1.17 (2025-09-05) + +### Chart + +`The Chart component` is used to visualize data with interactivity and offers extensive customization options for configuring data presentation. All chart elements are rendered using `Scalable Vector Graphics (SVG)`, ensuring crisp visuals and smooth performance across devices. Designed for modern React applications, it supports a wide range of chart types and interactive features, making it suitable for dynamic dashboards and data-driven interfaces. + +**Key features** + +- **High Performance:** Optimized to render large datasets with minimal lag, enabling smooth interactions and fast updates. + +- **Comprehensive Chart Types:** Supports 10 essential chart types including line, bar, area, spline, and scatter charts. + +- **Flexible Axis Support:** Offers multiple axis types—numeric, datetime, logarithmic, and categorical—for diverse data plotting needs. + +- **Axis Features:** Supports multiple axes, inverted axis, multiple panes, opposed position, strip lines, and smart labels for enhanced layout control. + +- **Data Labels and Markers:** Supports data labels and markers to annotate and highlight specific data points for better clarity. + +- **Legend:** Displays legends to provide additional context for chart series, with support for paging and customization. + +- **Rich Interactivity:** Includes tooltips, zooming, panning, clickable legends, and smooth animations to enhance engagement. + +- **Animation Support:** Delivers visually appealing transitions and effects that improve data storytelling. + +- **Accessibility & Navigation:** Provides keyboard navigation and screen reader support for inclusive experiences. + +- **Customization Options:** Enables configuration of data points, series styles, and UI behaviors to match specific visualization requirements. diff --git a/components/charts/README.md b/components/charts/README.md new file mode 100644 index 0000000..3bcc73c --- /dev/null +++ b/components/charts/README.md @@ -0,0 +1,63 @@ +# React Chart Components + +## What's Included in the React Chart Package + +The React Chart package includes the following list of components. + +### React Chart + +The Chart component is designed to deliver high-performance, interactive data visualizations with a wide range of chart types and customization options. + +Explore the demo [here](https://react.syncfusion.com/chart/overview). + +**Key features** + +- **High Performance:** Optimized to render large datasets with minimal lag, ensuring smooth interactions and fast updates. + +- **Comprehensive Chart Types:** Supports a wide range of chart types including Line, Column, Area, Bar, StackingColumn, StackingBar, StepLine, SplineArea, Scatter, Spline, and Bubble. + +- **Flexible Axis Support:** Offers multiple axis types—numeric, datetime, logarithmic, and categorical—for diverse data plotting. + +- **Rich Interactivity:** Includes tooltips, zooming, panning, clickable legends, and smooth animations to enhance user engagement. + +- **Animation Support:** Delivers visually appealing transitions and effects that improve data storytelling and user experience. + +- **Accessibility & Navigation:** Provides keyboard navigation and screen reader support for inclusive user experiences. + +- **Customization Options:** Allows developers to tailor data points, series styles, and UI behaviors to meet specific application needs. + + +

+Trusted by the world's leading companies + + Syncfusion logo + +

+ +## Setup + +To install `@syncfusion/react-charts` and its dependent packages, use the following command: + +```sh +npm install @syncfusion/react-charts +``` + +## Support + +Product support is available through following mediums. + +* [Support ticket](https://support.syncfusion.com/support/tickets/create) - Guaranteed Response in 24 hours | Unlimited tickets | Holiday support +* Live chat + +## Changelog +Check the changelog [here](https://github.com/syncfusion/react-ui-components/blob/master/components/chart/CHANGELOG.md). Get minor improvements and bug fixes every week to stay up to date with frequent updates. + +## License and copyright + +> This is a commercial product and requires a paid license for possession or use. Syncfusion’s licensed software, including this component, is subject to the terms and conditions of Syncfusion's [EULA](https://www.syncfusion.com/eula/es/). To acquire a license for [React UI components](https://www.syncfusion.com/react-components), you can [purchase](https://www.syncfusion.com/sales/products) or [start a free 30-day trial](https://www.syncfusion.com/account/manage-trials/start-trials). + +> A [free community license](https://www.syncfusion.com/products/communitylicense) is also available for companies and individuals whose organizations have less than $1 million USD in annual gross revenue and five or fewer developers. + +See [LICENSE FILE](https://github.com/syncfusion/react-ui-components/blob/master/license?utm_source=npm&utm_campaign=notification) for more info. + +© Copyright 2025 Syncfusion, Inc. All Rights Reserved. The Syncfusion Essential Studio license and copyright applies to this distribution. diff --git a/components/buttons/gulpfile.js b/components/charts/gulpfile.js similarity index 100% rename from components/buttons/gulpfile.js rename to components/charts/gulpfile.js diff --git a/components/inputs/license b/components/charts/license similarity index 100% rename from components/inputs/license rename to components/charts/license diff --git a/components/notifications/package.json b/components/charts/package.json similarity index 71% rename from components/notifications/package.json rename to components/charts/package.json index 56cbfbf..6e77aaa 100644 --- a/components/notifications/package.json +++ b/components/charts/package.json @@ -1,7 +1,7 @@ { - "name": "@syncfusion/react-notifications", - "version": "30.1.37", - "description": "A package of Pure React notification components such as Toast and Message which used to notify important information to end-users.", + "name": "@syncfusion/react-charts", + "version": "31.1.17", + "description": "A feature-rich React chart component for visualizing data effectively.", "author": "Syncfusion Inc.", "license": "SEE LICENSE IN license", "keywords": [ @@ -9,7 +9,7 @@ "web-components", "react", "syncfusion-react", - "react-notifications" + "react-charts" ], "repository": { "type": "git", @@ -19,8 +19,9 @@ "module": "./index.js", "readme": "README.md", "dependencies": { - "@syncfusion/react-base": "~30.1.37", - "@syncfusion/react-buttons": "~30.1.37" + "@syncfusion/react-base": "~31.1.17", + "@syncfusion/react-data": "~31.1.17", + "@syncfusion/react-svg-tooltip": "~31.1.17" }, "devDependencies": { "gulp": "4.0.2", diff --git a/components/charts/src/chart/Chart.tsx b/components/charts/src/chart/Chart.tsx new file mode 100644 index 0000000..1b95f27 --- /dev/null +++ b/components/charts/src/chart/Chart.tsx @@ -0,0 +1,116 @@ +import { forwardRef, useEffect, useRef, useState, useImperativeHandle, Ref } from 'react'; +import { ChartProvider } from './layout/ChartProvider'; +import { ChartComponentProps } from './base/interfaces'; +import { stringToNumber } from './utils/helper'; +import { defaultChartConfigs } from './base/default-properties'; +import { preRender, useProviderContext } from '@syncfusion/react-base'; +import { ElementWithSize, ChartSizeProps } from './chart-area/chart-interfaces'; + +/** + * Extends the base chart component properties with optional lifecycle methods. + * + */ +export interface IChart extends ChartComponentProps { + /** + * Optional method to clean up or destroy the chart instance. + * Can be used to release resources or detach event listeners when the chart is no longer needed. + * + * @private + */ + destroy?: () => void; +} + +/** + * **The React Chart component** enables developers to visualize data through a wide range of interactive and customizable chart types. + * It supports real-time updates, responsive layouts, and efficient rendering of large datasets for modern web applications. + * + * ```typescript + * import { Chart, ChartPrimaryXAxis, ChartSeries, ChartSeriesCollection } from '@syncfusion/react-charts'; + * + * + * + * + * + * + * + * ``` + */ +export const Chart: React.ForwardRefExoticComponent> = + forwardRef((props: ChartComponentProps, ref: Ref) => { + + const chartRef: React.RefObject = useRef(null); + const { dir } = useProviderContext(); + const [element, setElement] = useState(null); + const [isDestroyed, setIsDestroyed] = useState(false); + useEffect(() => { + const container: HTMLDivElement = chartRef.current as HTMLDivElement; + container.style.touchAction = 'element'; + container.style.userSelect = 'none'; + container.style.webkitUserSelect = 'none'; + container.style.position = 'relative'; + container.style.display = 'block'; + container.style.height = 'inherit'; + const containerWidth: number = container?.clientWidth || container?.offsetWidth || 600; + const containerHeight: number = container?.clientHeight || 450; + container.id = sanitizeElementId(container.id); + const availableSize: ChartSizeProps = { + width: stringToNumber(props.width, containerWidth) || containerWidth, + height: stringToNumber(props.height, containerHeight) || containerHeight + }; + if (!container.classList.contains('sf-chart-focused')) { + container.classList.add('sf-chart-focused'); + } + setElement({ element: container, availableSize }); + }, [props.height, props.width]); + + useImperativeHandle(ref, () => ({ + destroy: () => { setIsDestroyed(true); } + }), []); + + useEffect(() => { + preRender('chart'); + }, []); + + const chartProps: ChartComponentProps = { ...defaultChartConfigs.chart, ...props }; + chartProps.accessibility = { ...defaultChartConfigs.accessibility, ...props.accessibility }; + return ( + !isDestroyed && ( +
+ {element && ( + + )} +
+ ) + ); + }); + +export default Chart; + +/** + * Sanitizes and returns a valid element ID for a chart. + * + * - If the `elementId` is an empty string, it generates a unique ID based on a static chart ID and + * the current number of `.sf-chart` elements in the DOM. + * + * @param {string} elementId - The input element ID to sanitize or use for generating a unique one. + * @returns {string} A valid and unique element ID string safe for use in the DOM. + * @private + */ +export function sanitizeElementId(elementId: string): string { + if (elementId === '') { + const uniqueSuffix: number = Math.floor(Math.random() * 1000000); + const childElementId: string = `chart_${uniqueSuffix}`; + return childElementId; + } + else { + return elementId; + } +} diff --git a/components/charts/src/chart/base/Legend-base.tsx b/components/charts/src/chart/base/Legend-base.tsx new file mode 100644 index 0000000..d9e9baa --- /dev/null +++ b/components/charts/src/chart/base/Legend-base.tsx @@ -0,0 +1,351 @@ +import { ChartBorderProps, ChartLegendProps, ChartLocationProps } from './interfaces'; +import { ChartSeriesType, ChartMarkerShape, LegendPosition, LegendShape } from './enum'; +import { Chart, PathOptions, Rect, ChartSizeProps, TextOption } from '../chart-area/chart-interfaces'; + +/** + * Interface for the base legend component that defines core functionality and properties. + * + * @interface BaseLegend + * @extends ChartLegendProps + * @private + */ +export interface BaseLegend extends ChartLegendProps { + /** Reference to the parent chart instance for coordinating interactions and layout. */ + chart?: Chart; + /** Comprehensive legend configuration properties inherited from the chart. */ + legend?: ChartLegendProps; + /** Maximum height allowed for an individual legend item in pixels. */ + maxItemHeight?: number; + /** Collection of calculated row heights used for layout positioning. */ + rowHeights?: number[]; + /** Collection of calculated page heights when pagination is enabled for the legend. */ + pageHeights?: number[]; + /** Collection of calculated column heights for multi-column legend layouts. */ + columnHeights?: number[]; + /** Boolean flag indicating whether the legend content requires pagination due to space constraints. */ + isPaging?: boolean; + /** Height of the SVG clipping path applied to limit the visible legend items. */ + clipPathHeight?: number; + /** Total number of pages available when pagination is enabled. */ + totalPages?: number; + /** Boolean flag indicating whether the legend has a vertical orientation layout. */ + isVertical?: boolean; + /** Standard five-pixel measurement used for consistent spacing and alignment. */ + fivePixel?: number; + /** Number of rows calculated for the current legend layout configuration. */ + rowCount?: number; + /** The pixel dimensions of pagination navigation buttons. */ + pageButtonSize?: number; + /** Collection of x-coordinates for positioning pagination elements. */ + pageXCollections?: number[]; + /** Maximum number of columns allowed in a multi-column legend layout. */ + maxColumns?: number; + /** Maximum width constraint for the legend component in pixels. */ + maxWidth?: number; + /** Unique identifier for the legend DOM element used for selection and events. */ + legendID?: string; + /** SVG clipping rectangle definition for limiting visible content area. */ + clipRect?: RectOption; + /** Reference to the SVG group element that contains the translatable legend content. */ + legendTranslateGroup?: Element; + /** Currently active page number in a paginated legend, starting from 0. */ + currentPage?: number; + /** Opacity value for the backward navigation arrow (0-1), controlled by page position. */ + backwardArrowOpacity?: number; + /** Opacity value for the forward navigation arrow (0-1), controlled by page position. */ + forwardArrowOpacity?: number; + /** Text description used for screen readers to improve accessibility. */ + accessbilityText?: string; + /** Width of pagination arrow buttons in pixels. */ + arrowWidth?: number; + /** Height of pagination arrow buttons in pixels. */ + arrowHeight?: number; + /** Position of the legend relative to the chart's plotting area. */ + position?: LegendPosition; + /** Number of rows in the chart's data structure, used for layout calculations. */ + chartRowCount?: number; + /** Calculated bounding rectangle that defines the legend's position and dimensions. */ + legendBounds?: Rect; + /** Collection of all legend items with their configuration and state information. */ + legendCollections?: LegendOptions[]; + /** Collection of title text strings for multiple legend sections. */ + legendTitleCollections?: string[]; + /** Calculated dimensions of the legend title for layout positioning. */ + legendTitleSize?: ChartSizeProps; + /** Boolean flag indicating whether the legend is positioned at the top of the chart. */ + isTop?: boolean; + /** Boolean flag indicating whether the legend includes a title element. */ + isTitle?: boolean; + /** Timer ID reference for managing tooltip clearing operations. */ + clearTooltip?: number; + /** SVG clipping rectangle specifically for paginated legend content. */ + pagingClipRect?: RectOption; + /** The page number currently displayed to the user (1-based index for display). */ + currentPageNumber?: number; + /** Collection of interactive region definitions for legend items. */ + legendRegions?: ILegendRegions[]; + /** Collection of bounding rectangles for pagination interactive elements. */ + pagingRegions?: Rect[]; + /** Total number of pages calculated for pagination display. */ + totalNoOfPages?: number; + /** Boolean flag indicating whether Right-to-Left text direction is enabled. */ + isRtlEnable?: boolean; + /** Boolean flag indicating whether legend items should be displayed in reverse order. */ + isReverse?: boolean; + /** Pixel padding between individual legend items for spacing. */ + itemPadding?: number; + /** SVG transform attribute value for positioning the legend container. */ + transform?: string; + /** Configuration options for the "page up" navigation button. */ + pageUpOption?: PathOptions; + /** Configuration options for the "page down" navigation button. */ + pageDownOption?: PathOptions; + /** Text rendering options for pagination information display. */ + pageTextOption?: TextOption; + /** SVG translate transform value specifically for legend positioning. */ + legendTranslate?: string; + /** Calculated position for the legend title element. */ + legendTitleLoction?: ChartLocationProps +} + +/** + * Interface for legend options that define the appearance and behavior of individual legend items. + * + * @interface LegendOptions + * @private + */ +export interface LegendOptions { + /** Boolean flag determining whether this legend item should be included in rendering. */ + render: boolean; + /** Preserved original text content before any truncation or modification for display. */ + originalText: string; + /** Display text shown in the legend item, possibly truncated or modified for layout. */ + text: string; + /** Color value for the legend marker background. */ + fill: string; + /** Visual shape of the legend marker symbol. */ + shape: LegendShape; + /** Controls visibility state of this legend item without removing it from the DOM. */ + visible: boolean; + /** Classification of the chart series this legend item represents. */ + type: ChartSeriesType; + /** Calculated dimensions of the text element for layout planning. */ + textSize: ChartSizeProps; + /** Coordinates where this legend item is positioned within the legend container. */ + location: ChartLocationProps; + /** URL reference for image-based markers when applicable. */ + url?: string; + /** Zero-based index of the data point this legend item represents in the series. */ + pointIndex?: number; + /** Zero-based index of the series this legend item represents in the chart. */ + seriesIndex?: number; + /** Custom shape for the marker that may differ from the default legend shape. */ + markerShape?: ChartMarkerShape; + /** Controls whether the marker symbol is shown alongside the legend text. */ + markerVisibility?: boolean; + /** Array of text segments for handling multi-line or wrapped legend labels. */ + textCollection?: string[]; + /** SVG dash pattern definition for line series representations (e.g., "5,2"). */ + dashArray?: string; + /** Detailed rendering options for the legend symbol. */ + symbolOption?: PathOptions; + /** Detailed rendering options for the legend marker. */ + markerOption?: PathOptions; + /** Detailed rendering options for the legend text. */ + textOption?: TextOption; +} + +/** + * Interface for defining interactive regions within the legend. + * + * @interface ILegendRegions + * @private + */ +export interface ILegendRegions { + /** Bounding rectangle defining the clickable/interactive area. */ + rect: Rect; + /** Zero-based index corresponding to the associated legend item. */ + index: number; +} + +/** + * Creates a fully configured legend option object with the specified properties. + * + * @param {string} text - Legend text to display, representing the series or point name. + * @param {string} fill - Fill color for the legend marker, matching the series or point color. + * @param {LegendShape} shape - Visual shape of the legend marker (circle, rectangle, triangle, etc.). + * @param {boolean} visible - Initial visibility state of the legend item in the chart. + * @param {ChartSeriesType} type - Type classification of the chart series this legend represents. + * @param {string} [url] - Optional image URL for image-based markers. + * @param {ChartMarkerShape} [markerShape] - Optional custom shape for the marker that may differ from the legend shape. + * @param {boolean} [markerVisibility] - Controls whether the marker symbol appears alongside the text. + * @param {number} [pointIndex] - Zero-based index of the specific data point in multi-point series. + * @param {number} [seriesIndex] - Zero-based index of the series within the chart's collection. + * @param {string} [dashArray] - SVG dash pattern for representing line styles (e.g., "5,2" for dashed lines). + * @param {string} [originalText] - Preserved original text before any modifications for display. + * @returns {LegendOptions} A fully configured legend options object ready for rendering. + * @private + */ +export const createLegendOption: Function = ( + text: string, + fill: string, + shape: LegendShape, + visible: boolean, + type: ChartSeriesType, + url?: string, + markerShape?: ChartMarkerShape, + markerVisibility?: boolean, + pointIndex?: number, + seriesIndex?: number, + dashArray?: string, + originalText?: string +): LegendOptions => { + return { + render: true, + text, + fill, + shape, + url, + visible, + type, + markerVisibility, + markerShape, + pointIndex, + seriesIndex, + dashArray, + originalText: originalText || text, + textSize: { width: 0, height: 0 }, + location: { x: 0, y: 0 }, + textCollection: [] + }; +}; + +/** + * Creates a fully configured path options object for rendering SVG paths in the legend. + * + * @param {string} id - Unique identifier for the path element used in DOM selection and events. + * @param {string} fill - Fill color for the path interior (CSS color string or gradient reference). + * @param {number} strokeWidth - Width of the stroke outline in pixels. + * @param {string} stroke - Stroke color for the path outline (CSS color string). + * @param {number} opacity - Opacity value between 0 (transparent) and 1 (opaque). + * @param {string} strokeDasharray - Pattern for dashed lines (e.g., "5,2" for dashed lines). + * @param {string} d - SVG path data string defining the shape geometry. + * @param {number} [rx] - X-axis radius for rounded corners in rectangular shapes. + * @param {number} [ry] - Y-axis radius for rounded corners in rectangular shapes. + * @param {number} [cx] - X-coordinate of the center point for circular shapes. + * @param {number} [cy] - Y-coordinate of the center point for circular shapes. + * @returns {PathOptions} Fully configured path options object ready for rendering. + * @private + */ +export const createPathOption: Function = ( + id: string, + fill: string, + strokeWidth: number, + stroke: string, + opacity: number, + strokeDasharray: string, + d: string, + rx?: number, + ry?: number, + cx?: number, + cy?: number +): PathOptions => { + return { + id, + fill, + stroke, + strokeWidth, + strokeDasharray, + opacity, + d, + rx, + ry, + cx, + cy + }; +}; + +/** + * Interface that defines properties for SVG rectangle elements used in charts and legends. + * + * @interface RectOption + * @private + */ +export interface RectOption { + /** Unique identifier for the rectangle element used in DOM selection and events. */ + id: string; + /** Fill color for the rectangle interior (CSS color string or gradient reference). */ + fill: string; + /** Stroke color for the rectangle outline (CSS color string). */ + stroke: string; + /** Width of the stroke outline in pixels. */ + strokeWidth: number; + /** Pattern for dashed lines (e.g., "5,2" for dashed borders). */ + strokeDasharray: string; + /** Opacity value between 0 (transparent) and 1 (opaque). */ + opacity: number; + /** SVG path data (preserved for compatibility with path interfaces but not used for rectangles). */ + d: string; + /** X-coordinate of the top-left corner of the rectangle. */ + x: number; + /** Y-coordinate of the top-left corner of the rectangle. */ + y: number; + /** Width of the rectangle in pixels. */ + width: number; + /** Height of the rectangle in pixels. */ + height: number; + /** X-axis radius for rounded corners in pixels. */ + rx: number; + /** Y-axis radius for rounded corners in pixels. */ + ry: number; + /** SVG transform attribute value for positioning and transforming the rectangle. */ + transform: string; +} + +/** + * Creates a fully configured rectangle options object for rendering SVG rectangles in charts. + * + * @param {string} id - Unique identifier for the rectangle element used in DOM selection and events. + * @param {string} fill - Fill color for the rectangle interior (CSS color string or gradient reference). + * @param {ChartBorderProps} border - Border configuration object containing width and color properties. + * @param {number} opacity - Opacity value between 0 (transparent) and 1 (opaque). + * @param {Rect} rect - Rectangle dimensions and position object with x, y, width and height properties. + * @param {number} rx - X-axis radius for rounded corners in pixels. + * @param {number} ry - Y-axis radius for rounded corners in pixels. + * @param {string} transform - SVG transform attribute value for positioning and transformations. + * @param {string} strokeDasharray - Pattern for dashed lines (e.g., "5,2" for dashed borders). + * @returns {RectOption} Fully configured rectangle options object ready for rendering. + * @private + */ +export const createRectOption: Function = ( + id: string, + fill: string, + border: ChartBorderProps, + opacity: number, + rect: Rect, + rx: number, + ry: number, + transform: string, + strokeDasharray: string +): RectOption => { + const stroke: string = border.width !== 0 && border.color !== '' && border.color !== null && border.color + ? border.color + : 'transparent'; + + return { + id, + fill, + stroke: stroke, + strokeWidth: border.width as Required, + strokeDasharray: strokeDasharray, + opacity: opacity, + d: '', + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + rx: rx, + ry: ry, + transform: transform + }; +}; diff --git a/components/charts/src/chart/base/default-properties.tsx b/components/charts/src/chart/base/default-properties.tsx new file mode 100644 index 0000000..202278e --- /dev/null +++ b/components/charts/src/chart/base/default-properties.tsx @@ -0,0 +1,717 @@ + +import { AxisModel, MarginModel, SeriesProperties } from '../chart-area/chart-interfaces'; +import { ChartAxisLabelProps, ChartAxisTitleProps } from '../chart-axis/base'; +import { ChartBorderProps, ChartAreaProps, ChartLegendProps, ChartStackLabelsProps, ChartTitleProps, Column, MajorGridLines, MajorTickLines, MinorGridLines, MinorTickLines, Row, ChartTooltipProps, ChartZoomSettingsProps, ChartStripLineProps, ChartAccessibilityProps } from './interfaces'; + +// Define default stripline settings to avoid duplication +const defaultStripLineSettings: ChartStripLineProps = { + visible: false, + range: { + shouldStartFromAxis: false, + start: undefined, + end: undefined, + size: undefined, + sizeType: 'Auto' + }, + style: { + color: '#808080', + opacity: 1, + dashArray: '', + imageUrl: '', + border: { color: '', width: 1, dashArray: '' }, + zIndex: 'Behind' + }, + text: { + content: '', + font: { color: '', fontFamily: '', fontSize: '', fontStyle: '', fontWeight: '', opacity: 1 }, + rotation: undefined, + hAlign: 'Center', + vAlign: 'Center' + }, + repeat: { + enable: false, + every: undefined, + until: undefined + }, + segment: { + enable: false, + start: undefined, + end: undefined, + axisName: undefined + } +}; + +interface ChartConfig { + chart: { + border: ChartBorderProps; + margin: MarginModel + enableSideBySidePlacement: boolean; + }; + ChartArea: ChartAreaProps; + ChartTitle: ChartTitleProps; + ChartSubTitle: ChartTitleProps; + Column: Column; + Row: Row; + MajorGridLines: MajorGridLines + MajorTickLines: MajorTickLines; + MinorTickLines: MinorTickLines; + MinorGridLines: MinorGridLines; + LabelStyle: ChartAxisLabelProps; + TitleStyle: ChartAxisTitleProps; + PrimaryXAxis: Partial; + PrimaryYAxis: Partial; + SecondaryAxis: Partial; + ChartSeries: Partial; + ChartStackLabels: ChartStackLabelsProps + ChartLegend: ChartLegendProps; + ChartZoom: ChartZoomSettingsProps; + ChartTooltip: ChartTooltipProps; + accessibility: ChartAccessibilityProps; + StripLines: ChartStripLineProps[]; +} + +export const defaultChartConfigs: ChartConfig = { + + chart: { + border: { color: '#DDDDDD', width: 0, dashArray: '' }, + margin: { top: 10, right: 10, bottom: 10, left: 10 }, + enableSideBySidePlacement: true + }, + accessibility: { + ariaLabel: '', + focusable: true, + tabIndex: 0, + role: '' + }, + ChartArea: { + border: { color: '', width: 0, dashArray: '' }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + opacity: 1, + backgroundImage: '', + background: 'transparent', + width: undefined + }, + ChartTitle: { + text: '', + color: '', + fontFamily: '', + fontSize: '', + fontStyle: '', + fontWeight: '', + align: 'Center', + position: 'Top', + opacity: 1, + textOverflow: 'Wrap', + x: 0, + y: 0, + background: 'transparent', + border: { color: 'transparent', width: 0, dashArray: '', cornerRadius: 0.8 }, + accessibility: { + ariaLabel: '', + role: 'img', + focusable: true, + tabIndex: 0 + } + + }, + ChartSubTitle: { + text: '', + color: '', + fontFamily: '', + fontSize: '', + fontStyle: '', + fontWeight: '', + align: 'Center', + position: 'Top', + opacity: 1, + textOverflow: 'Wrap', + x: 0, + y: 0, + background: 'transparent', + border: { color: 'transparent', width: 0, dashArray: '', cornerRadius: 0.8 }, + accessibility: { + ariaLabel: '', + role: 'img', + focusable: true, + tabIndex: 0 + } + }, + Column: { + width: '100%', + border: { + color: '', dashArray: '', width: 1 + } + }, + Row: { + height: '100%', + border: { + color: '', dashArray: '', width: 1 + } + }, + MajorGridLines: { + width: null, + color: '', + dashArray: '' + }, + MajorTickLines: { + width: 0, + height: 5, + color: '' + }, + MinorTickLines: { + width: 0.7, + height: 5, + color: '' + }, + MinorGridLines: { + width: 0.7, + color: '', + dashArray: '' + }, + LabelStyle: { + color: '', + fontFamily: '', + fontStyle: '', + fontWeight: '', + opacity: 1, + fontSize: '', + align: 'Center', + rotationAngle: 0, + format: '', + skeleton: '', + padding: 5, + position: 'Outside', + placement: 'BetweenTicks', + intersectAction: 'Trim', + enableWrap: false, + enableTrim: false, + maxLabelWidth: 34, + edgeLabelPlacement: 'Shift' + }, + TitleStyle: { + color: '', + fontFamily: '', + fontStyle: '', + fontWeight: '', + opacity: 1, + fontSize: '', + align: 'Center', + overflow: 'Wrap', + text: '', + padding: 5, + rotationAngle: undefined + }, + PrimaryXAxis: { + name: 'primaryXAxis', + span: 1, + rowIndex: 0, + columnIndex: 0, + valueType: 'Double', + opposedPosition: false, + indexed: false, + visible: true, + minimum: undefined, + maximum: undefined, + interval: undefined, + startFromZero: true, + desiredIntervals: undefined, + maxLabelDensity: 3, + zoomFactor: 1, + zoomPosition: 0, + series: [], + labels: [], + indexLabels: {}, + axisMajorGridLineOptions: [], + axisMajorTickLineOptions: [], + axisMinorGridLineOptions: [], + axisMinorTickLineOptions: [], + axisLabelBorderOptions: [], + axislabelOptions: [], + axisTitleOptions: [], + internalVisibility: true, + actualRange: { minimum: 0, maximum: 0, interval: 0, delta: 0 }, + doubleRange: { start: 0, end: 0, delta: 0, median: 0 }, + intervalDivs: [10, 5, 2, 1], + visibleRange: { minimum: 0, maximum: 0, interval: 0, delta: 0 }, + visibleLabels: [], + startLabel: '', + endLabel: '', + maxLabelSize: { height: 0, width: 0 }, + rect: { x: 0, y: 0, width: 0, height: 0 }, + updatedRect: { x: 0, y: 0, width: 0, height: 0 }, + axisLineOptions: { + id: '', + d: '', + stroke: '', + strokeWidth: 0, + dashArray: '', + fill: '' + }, + paddingInterval: 0, + maxPointLength: 0, + isStack100: false, + titleCollection: [], + titleSize: { height: 0, width: 0 }, + isAxisOpposedPosition: false, + isAxisInverse: false, + angle: 0, + rotatedLabel: '', + labelStyle: { + color: '', + fontFamily: '', + fontStyle: '', + fontWeight: '', + opacity: 1, + fontSize: '', + align: 'Center', + rotationAngle: 0, + format: '', + skeleton: '', + padding: 5, + position: 'Outside', + placement: 'BetweenTicks', + intersectAction: 'Trim', + enableWrap: false, + enableTrim: false, + maxLabelWidth: 34, + edgeLabelPlacement: 'Shift' + }, + titleStyle: { + color: '', + fontFamily: '', + fontStyle: '', + fontWeight: '', + opacity: 1, + fontSize: '', + align: 'Center', + overflow: 'Wrap', + text: '', + padding: 5, + rotationAngle: undefined + }, + lineStyle: { width: 1, color: '', dashArray: '' }, + plotOffset: { + left: 0, + top: 0, + right: 0, + bottom: 0 + }, + tickPosition: 'Outside', + minorTicksPerInterval: 0, + rangePadding: 'Auto', + logBase: 10, + majorGridLines: { + width: 0, + color: '', + dashArray: '' + }, + majorTickLines: { + width: 0, + height: 5, + color: '' + }, + minorTickLines: { + width: 0.7, + height: 5, + color: '' + }, + minorGridLines: { + width: 0.7, + color: '', + dashArray: '' + }, + intervalType: 'Auto', + skeleton: '', + skeletonType: 'DateTime', + stripLines: [{ ...defaultStripLineSettings }] + }, + PrimaryYAxis: { + name: 'primaryYAxis', + span: 1, + rowIndex: 0, + columnIndex: 0, + valueType: 'Double', + opposedPosition: false, + indexed: false, + visible: true, + minimum: undefined, + maximum: undefined, + interval: undefined, + startFromZero: true, + desiredIntervals: undefined, + maxLabelDensity: 3, + zoomFactor: 1, + zoomPosition: 0, + logBase: 10, + labelStyle: { + color: '', + fontFamily: '', + fontStyle: '', + fontWeight: '', + opacity: 1, + fontSize: '', + align: 'Center', + rotationAngle: 0, + format: '', + skeleton: '', + padding: 5, + position: 'Outside', + placement: 'BetweenTicks', + intersectAction: 'Trim', + enableWrap: false, + enableTrim: false, + maxLabelWidth: 34, + edgeLabelPlacement: 'Shift' + }, + titleStyle: { + color: '', + fontFamily: '', + fontStyle: '', + fontWeight: '', + opacity: 1, + fontSize: '', + align: 'Center', + overflow: 'Wrap', + text: '', + padding: 5, + rotationAngle: undefined + }, + lineStyle: { width: 0, color: '', dashArray: '' }, + plotOffset: { + left: 0, + top: 0, + right: 0, + bottom: 0 + }, + tickPosition: 'Outside', + minorTicksPerInterval: 0, + rangePadding: 'Auto', + majorGridLines: { + width: 1, + color: '', + dashArray: '' + }, + majorTickLines: { + width: 0, + height: 5, + color: '' + }, + minorTickLines: { + width: 0.7, + height: 5, + color: '' + }, + minorGridLines: { + width: 0.7, + color: '', + dashArray: '' + }, + stripLines: [{ ...defaultStripLineSettings }] + }, + SecondaryAxis: { + span: 1, + rowIndex: 0, + columnIndex: 0, + valueType: 'Double', + opposedPosition: false, + indexed: false, + visible: true, + minimum: undefined, + maximum: undefined, + interval: undefined, + startFromZero: true, + desiredIntervals: undefined, + maxLabelDensity: 3, + zoomFactor: 1, + zoomPosition: 0, + series: [], + labels: [], + indexLabels: {}, + axisMajorGridLineOptions: [], + axisMajorTickLineOptions: [], + axisMinorGridLineOptions: [], + axisMinorTickLineOptions: [], + axisLabelBorderOptions: [], + axislabelOptions: [], + axisTitleOptions: [], + internalVisibility: true, + actualRange: { minimum: 0, maximum: 0, interval: 0, delta: 0 }, + doubleRange: { start: 0, end: 0, delta: 0, median: 0 }, + intervalDivs: [10, 5, 2, 1], + visibleRange: { minimum: 0, maximum: 0, interval: 0, delta: 0 }, + visibleLabels: [], + startLabel: '', + endLabel: '', + maxLabelSize: { height: 0, width: 0 }, + rect: { x: 0, y: 0, width: 0, height: 0 }, + updatedRect: { x: 0, y: 0, width: 0, height: 0 }, + axisLineOptions: { + id: '', + d: '', + stroke: '', + strokeWidth: 0, + dashArray: '', + fill: '' + }, + paddingInterval: 0, + maxPointLength: 0, + isStack100: false, + titleCollection: [], + titleSize: { height: 0, width: 0 }, + isAxisOpposedPosition: false, + isAxisInverse: false, + angle: 0, + rotatedLabel: '', + labelStyle: { + color: '', + fontFamily: '', + fontStyle: '', + fontWeight: '', + opacity: 1, + fontSize: '', + align: 'Center', + rotationAngle: 0, + format: '', + skeleton: '', + padding: 5, + position: 'Outside', + placement: 'BetweenTicks', + intersectAction: 'Trim', + enableWrap: false, + enableTrim: false, + maxLabelWidth: 34, + edgeLabelPlacement: 'Shift' + }, + titleStyle: { + color: '', + fontFamily: '', + fontStyle: '', + fontWeight: '', + opacity: 1, + fontSize: '', + align: 'Center', + overflow: 'Wrap', + text: '', + padding: 5, + rotationAngle: undefined + }, + lineStyle: { width: 0, color: '', dashArray: '' }, + plotOffset: { + left: 0, + top: 0, + right: 0, + bottom: 0 + }, + tickPosition: 'Outside', + minorTicksPerInterval: 0, + rangePadding: 'Auto', + logBase: 10, + majorGridLines: { + width: 0, + color: '', + dashArray: '' + }, + majorTickLines: { + width: 0, + height: 5, + color: '' + }, + minorTickLines: { + width: 0.7, + height: 5, + color: '' + }, + minorGridLines: { + width: 0.7, + color: '', + dashArray: '' + }, + stripLines: [{ ...defaultStripLineSettings }] + }, + ChartSeries: { + type: 'Line', + visible: true, + xField: '', + yField: '', + name: '', + opacity: 1, + dashArray: '', + width: 2, + marker: { + visible: false, + width: 7, + height: 7, + shape: null, + filled: true, + imageUrl: '', + fill: null, + border: { + width: 2, + color: '', + dashArray: '' + }, + offset: { x: 0, y: 0 }, + opacity: 1, + highlightable: true, + dataLabel: { + visible: false, + showZero: true, + labelField: null, + fill: 'transparent', + format: null, + opacity: 1, + rotationAngle: 0, + enableRotation: false, + position: 'Auto', + borderRadius: { x: 5, y: 5 }, + textAlign: 'Center', + border: { width: 1, color: '' }, + margin: { + right: 5, bottom: 5, left: 5, top: 5 + }, + font: { + fontStyle: 'Normal', + fontSize: '12px', + fontWeight: 'Normal', + color: '', + fontFamily: '', + opacity: 1 + } + } + }, + fill: null, + interior: '', + border: { width: 0, color: '' }, + animation: { enable: true, duration: 1000, delay: 0 }, + xAxisName: null, + yAxisName: null, + dataSource: '', + legendShape: 'SeriesType', + query: '', + enableComplexProperty: false, + zOrder: 0, + splineType: 'Natural', + step: 'Left', + clipRect: { x: 0, y: 0, width: 0, height: 0 }, + dataModule: null, + points: [], + category: '', + index: 0, + yMin: 0, + xMin: 0, + xMax: 0, + xData: [], + yData: [], + yMax: 0, + symbolElement: null, + visiblePoints: [], + currentViewData: [], + emptyPointSettings: { fill: 'gray', mode: 'Gap', border: { color: 'gray', width: 1 } }, + noRisers: false, + colorField: '', + currentData: [], + accessibility: { + ariaLabel: '', + focusable: true, + tabIndex: 0, + role: '' + }, + cornerRadius: { topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0 }, + groupName: '', + isLegendClicked: false, + sizeField: '', + minRadius: 1, + maxRadius: 3, + enableTooltip: true + }, + ChartStackLabels: { + visible: false, + fill: 'transparent', + border: { + width: undefined, + color: undefined + }, + font: { + fontStyle: 'Normal', + fontSize: '12px', + fontWeight: 'Normal', + color: '', + fontFamily: '' + }, + align: 'Center', + margin: { + left: 5, + right: 5, + top: 5, + bottom: 5 + }, + rotationAngle: 0, + borderRadius: { x: 5, y: 5 }, + format: '' + }, + ChartTooltip: { + enable: false, + showMarker: true, + shared: false, + fill: undefined, + headerText: undefined, + opacity: undefined, + format: undefined, + enableAnimation: true, + duration: 300, + fadeOutDuration: 1000, + showNearestPoint: true, + showNearestTooltip: true, + showHeaderLine: false, + location: undefined, + border: { color: '', width: 1, dashArray: '' }, + fadeOutMode: 'Move', + textStyle: { color: '', fontFamily: '', fontSize: '', fontStyle: '' } + + }, + ChartLegend: { + visible: true, + height: undefined, + width: undefined, + location: { x: 0, y: 0 }, + position: 'Auto', + padding: 8, + itemPadding: 0, + align: 'Center', + textStyle: { + opacity: 1 + }, + shapeHeight: undefined, + shapeWidth: undefined, + border: { width: 1, color: '', dashArray: '' }, + margin: { left: 0, right: 0, top: 0, bottom: 0 }, + containerPadding: { left: 0, right: 0, top: 0, bottom: 0 }, + shapePadding: 8, + background: 'transparent', + opacity: 1, + toggleVisibility: true, + title: undefined, + titleAlign: 'Center', + titleStyle: { + opacity: 1 + }, + titleOverflow: 'Wrap', + maxTitleWidth: 100, + maxLabelWidth: undefined, + enablePages: true, + inversed: false, + reverse: false, + fixedWidth: false, + accessibility: { tabIndex: 0, focusable: true } + }, + ChartZoom: { + selectionZoom: false, + pan: false, + mode: 'XY', + toolbar: { visible: false, items: ['ZoomIn', 'ZoomOut', 'Pan', 'Reset'] }, + accessibility: {}, + mouseWheelZoom: false, + pinchZoom: false + }, + StripLines: [{ ...defaultStripLineSettings }] +}; diff --git a/components/charts/src/chart/base/enum.tsx b/components/charts/src/chart/base/enum.tsx new file mode 100644 index 0000000..22632a0 --- /dev/null +++ b/components/charts/src/chart/base/enum.tsx @@ -0,0 +1,433 @@ +/** + * Defines the possible orientations for an axis. + * ```props + * Horizontal :- Represents a horizontal axis. + * Vertical :- Represents a vertical axis. + * ``` + * + * @private + */ +export type Orientation = + 'Horizontal' | + 'Vertical'; + +/** + * Defines the position of ticks or labels relative to the axis line. + * ```props + * Inside :- Positions ticks or labels inside the axis line. + * Outside :- Positions ticks or labels outside the axis line. + * ``` + */ +export type AxisLabelPosition = + 'Inside' | + 'Outside'; + +/** + * Defines the action to take when axis labels intersect. + * ```props + * None :- Display all labels without any modification. + * Hide :- Hide the label when it intersects. + * Trim :- Trim the label text when it intersects. + * Wrap :- Wrap the label text to the next line when it intersects. + * MultipleRows :- Arrange labels in multiple rows when they intersect. + * Rotate45 :- Rotate the label 45 degrees when it intersects. + * Rotate90 :- Rotate the label 90 degrees when it intersects. + * ``` + */ +export type LabelIntersectMode = + 'None' | + 'Hide' | + 'Trim' | + 'Wrap' | + 'MultipleRows' | + 'Rotate45' | + 'Rotate90'; + +/** + * Defines how axis labels are positioned relative to the tick marks. + * ```props + * BetweenTicks :- Render the label between the tick marks. + * OnTicks :- Render the label directly on the tick marks. + * ``` + */ +export type LabelPlacement = + 'BetweenTicks' | + 'OnTicks'; + +/** + * Defines the type of padding applied to the chart axis range. + * ```props + * Auto :- Automatically applies padding: 'Normal' for vertical axes and 'None' for horizontal axes. + * None :- No padding is applied to the axis range. + * Normal :- Applies standard padding based on range calculations. + * Additional :- Adds one interval of the axis as padding to both the minimum and maximum values. + * Round :- Rounds the axis range to the nearest value divisible by the interval. + * ``` + */ +export type ChartRangePadding = + 'Auto' | + 'None' | + 'Normal' | + 'Additional' | + 'Round'; + +/** + * Defines the built-in themes available for rendering the chart. + * ```props + * Material3 :- Renders the chart using the Material 3 theme. + * Material3Dark :- Renders the chart using the Material 3 dark theme. + * ``` + */ +export type Theme = + 'Material3' | + 'Material3Dark'; + +/** + * Defines how empty data points are handled in the chart. + * ```props + * Gap :- Displays empty points as gaps in the chart. + * Zero :- Displays empty points with a value of zero. + * Drop :- Ignores empty points during rendering. + * Average :- Displays empty points using the average of the previous and next data points. + * ``` + */ +export type EmptyPointMode = + 'Gap' | + 'Zero' | + 'Drop' | + 'Average'; + +/** + * Specifies the available types of chart series. + * ```props + * Line :- Represents a line series. + * Column :- Represents a column series. + * Area :- Represents an area series. + * Bar :- Represents a bar series. + * StackingColumn :- Represents a stacking column series. + * StackingBar :- Represents a stacking bar series. + * StepLine :- Represents a step line series. + * SplineArea :- Represents a spline area series. + * Scatter :- Represents a scatter series. + * Spline :- Represents a spline series. + * Bubble :- Represents a bubble series. + * ``` + */ +export type ChartSeriesType = + 'Line' | + 'Column' | + 'Area' | + 'Bar' | + 'StackingColumn' | + 'StackingBar' | + 'StepLine' | + 'SplineArea' | + 'Scatter' | + 'Spline' | + 'Bubble'; + +/** + * Specifies the position where the step begins in a step line series. + * ```props + * Left :- Steps begin from the left side of the second data point. + * Right :- Steps begin from the right side of the first data point. + * Center :- Steps begin between the data points. + * ``` + */ +export type StepPosition = + 'Left' | + 'Right' | + 'Center'; + +/** + * Specifies how labels at the edges of the axis are handled. + * ```props + * None :- Displays edge labels without any adjustment. + * Hide :- Hides the labels at the edges of the axis. + * Shift :- Shifts the edge labels to avoid overlap or clipping. + * ``` + */ +export type EdgeLabelPlacement = + 'None' | + 'Hide' | + 'Shift'; + + +/** + * Specifies the position of the chart title. + * ```props + * Top :- Places the title at the top of the chart. + * Right :- Places the title on the right side of the chart. + * Bottom :- Places the title at the bottom of the chart. + * Left :- Places the title on the left side of the chart. + * Custom :- Places the title at a custom position based on specified x and y coordinates. + * ``` + */ +export type TitlePosition = + 'Top' | + 'Right' | + 'Bottom' | + 'Left' | + 'Custom'; + +/** + * Defines how tooltips fade out in the chart. + * ```props + * Click :- Removes the tooltip when the user clicks. + * Move:- Removes the tooltip after a short delay when the pointer moves. + * ``` + */ +export type FadeOutMode = + 'Click' | + 'Move'; + +/** + * Represents the data type used for axis values. + * ```props + * Double :- Represents a numeric axis. Suitable for continuous numerical data. + * DateTime :- Represents a DateTime axis. Used for time-based data. + * Category :- Represents a category axis. Ideal for discrete categories or labels. + * Logarithmic :- Represents a logarithmic axis. Useful for data with exponential scale. + * ``` + */ +export type AxisValueType = + 'Double' | + 'DateTime' | + 'Category' | + 'Logarithmic'; + +/** + * Defines the actions to take when data labels intersect. + * ```props + * None :- Displays all labels without modification. + * Hide :- Hides the label when it intersects. + * Rotate90 :- Rotates the label 90 degrees when it intersects. + * ``` + */ +export type DataLabelIntersectMode = + 'None' | + 'Hide' | + 'Rotate90'; + +/** + * Specifies the position options for labels in the chart. + * ```props + * Outer :- Positions the label outside the data point. + * Top :- Positions the label on top of the data point. + * Bottom :- Positions the label below the data point. + * Middle :- Positions the label at the center of the data point. + * Auto :- Automatically positions the label based on the series type. + * ``` + */ +export type LabelPosition = + 'Outer' | + 'Top' | + 'Bottom' | + 'Middle' | + 'Auto'; + +/** + * Defines the shapes available for legend items in the chart. + * ```props + * Circle :- Renders a circular legend shape. + * Rectangle :-Renders a rectangular legend shape. + * Triangle :- Renders a triangular legend shape. + * Diamond :- Renders a diamond-shaped legend. + * Cross :- Renders a cross-shaped legend. + * HorizontalLine :- Renders a horizontal line as the legend shape. + * VerticalLine :- Renders a vertical line as the legend shape. + * Pentagon :- Renders a pentagon-shaped legend. + * InvertedTriangle :- Renders an inverted triangle shape. + * SeriesType :- Uses the shape based on the series type. + * Image :- Renders a custom image as the legend shape. + * ``` + */ +export type LegendShape = + 'Circle' | + 'Rectangle' | + 'Triangle' | + 'Diamond' | + 'Cross' | + 'HorizontalLine' | + 'VerticalLine' | + 'Pentagon' | + 'InvertedTriangle' | + 'SeriesType' | + 'Image'; + + +/** + * Defines the shapes available for chart markers. + * ```props + * Circle :- Specifies the marker shape as a circle. + * Rectangle :- Specifies the marker shape as a rectangle. + * Triangle :- Specifies the marker shape as a triangle. + * Diamond :- Specifies the marker shape as a diamond. + * Cross :- Specifies the marker shape as a cross. + * Plus :- Specifies the marker shape as a plus symbol. + * HorizontalLine :- Specifies the marker shape as a horizontal line. + * VerticalLine :- Specifies the marker shape as a vertical line. + * Pentagon :- Specifies the marker shape as a pentagon. + * InvertedTriangle :- Specifies the marker shape as an inverted triangle. + * Image :- Specifies the marker shape as an image. + * Star :- Specifies the marker shape as a star. + * None :- Disables the marker by not rendering any shape. + * ``` + */ +export type ChartMarkerShape = + 'Circle' | + 'Rectangle' | + 'Triangle' | + 'Diamond' | + 'Cross' | + 'Plus' | + 'HorizontalLine' | + 'VerticalLine' | + 'Pentagon' | + 'InvertedTriangle' | + 'Image' | + 'Star' | + 'None'; + +/** + * Specifies the position of the chart legend. + * ```props + * Auto :- Automatically places the legend based on the chart area type. + * Top :- Displays the legend at the top of the chart. + * Left :- Displays the legend on the left side of the chart. + * Bottom :- Displays the legend at the bottom of the chart. + * Right :- Displays the legend on the right side of the chart. + * Custom :- Positions the legend based on specified x and y coordinates. + * ``` + */ +export type LegendPosition = + 'Auto' | + 'Top' | + 'Left' | + 'Bottom' | + 'Right' | + 'Custom'; + +/** + * Defines how chart text should behave when it exceeds the boundaries of its container. + * ```props + * None :- Displays the full text even if it overlaps with other chart elements. + * Wrap :- Breaks the text into multiple lines to fit within the container. + * Trim :- Cuts off the text and may append ellipsis if it exceeds the container width. + * ``` + */ +export type TextOverflow = + 'None' | + 'Wrap' | + 'Trim'; + +/** + * Defines the set of interactive tools available in the chart's zooming toolbar. + * ```props + * ZoomIn :- Displays a tool that allows users to zoom into the chart. + * ZoomOut :- Displays a tool that allows users to zoom out of the chart. + * Pan :- Enables panning across the chart by dragging. + * Reset :- Resets the chart view to its original state. + * ``` + */ +export type ToolbarItems = + 'ZoomIn' | + 'ZoomOut' | + 'Pan' | + 'Reset'; + +/** + * Specifies the zooming mode for the chart. + * ```props + * XY :- Zooms both the horizontal (X) and vertical (Y) axes. + * X :- Zooms only the horizontal (X) axis. + * Y:- Zooms only the vertical (Y) axis. + * ``` + */ +export type ZoomMode = + 'XY' | + 'X' | + 'Y'; + +/** + * Specifies the type of spline used for rendering. + * ```props + * Natural :- Renders a natural spline. + * Monotonic :- Renders a monotonic spline. + * Cardinal :- Renders a cardinal spline. + * Clamped :- Renders a clamped spline. + * ``` + */ +export type SplineType = + 'Natural' | + 'Monotonic' | + 'Cardinal' | + 'Clamped'; + +/** + * Specifies the interval type for a datetime axis. + * ```props + * Auto :- Automatically determines the interval based on the data. + * Years :- Sets the interval in years. + * Months :- Sets the interval in months. + * Days :- Sets the interval in days. + * Hours :- Sets the interval in hours. + * Minutes :- Sets the interval in minutes. + * Seconds :- Sets the interval in seconds. + * ``` + */ +export type IntervalType = + 'Auto' | + 'Years' | + 'Months' | + 'Days' | + 'Hours' | + 'Minutes' | + 'Seconds'; + +/** + * Specifies the types of skeleton formats available for date and time formatting. + * ```props + * Date :- Formats only the date. + * DateTime :- Formats both date and time. + * Time :- Formats only the time. + * ``` + */ +export type SkeletonType = + 'Date' | + 'DateTime' | + 'Time'; + +/** + * Defines the unit of strip line size. + * ```props + * Auto :- In numeric axis, it will consider a number and DateTime axis, it will consider as milliseconds. + * Pixel :- The stripline gets their size in pixel. + * Years :- The stripline size is based on year in the DateTime axis. + * Months :- The stripline size is based on month in the DateTime axis. + * Days :- The stripline size is based on day in the DateTime axis. + * Hours :- The stripline size is based on hour in the DateTime axis. + * Minutes :- The stripline size is based on minutes in the DateTime axis. + * Seconds:- The stripline size is based on seconds in the DateTime axis. + * ``` + */ +export type StripLineSizeUnit = + 'Auto' | + 'Pixel' | + 'Years' | + 'Months' | + 'Days' | + 'Hours' | + 'Minutes' | + 'Seconds'; + +/** + * Specifies the order of the strip line. + * ```props + * Over :- Places the strip line over the series elements. + * Behind :- Places the strip line behind the series elements. + * ``` + */ +export type ZIndex = + 'Over' | + 'Behind'; diff --git a/components/charts/src/chart/base/interfaces.tsx b/components/charts/src/chart/base/interfaces.tsx new file mode 100644 index 0000000..b21787c --- /dev/null +++ b/components/charts/src/chart/base/interfaces.tsx @@ -0,0 +1,3072 @@ +import { AxisLabelPosition, ChartRangePadding, ChartSeriesType, EmptyPointMode, StepPosition, TitlePosition, AxisValueType, FadeOutMode, LabelPosition, Theme, LegendShape, ChartMarkerShape, LegendPosition, TextOverflow, ZoomMode, ToolbarItems, SplineType, IntervalType, SkeletonType, StripLineSizeUnit, ZIndex, DataLabelIntersectMode } from './enum'; +import { DataManager, Query } from '@syncfusion/react-data'; +import { AxisDataProps, ChartSizeProps, VisibleRangeProps } from '../chart-area/chart-interfaces'; +import { Animation } from '../common/base'; +import { HorizontalAlignment, VerticalAlignment } from '@syncfusion/react-base'; + +/** + * Represents the border configuration for the chart title. + */ +export interface TitleBorder { + + /** + * Defines the color of the border. Accepts any valid CSS color string, including hexadecimal and RGBA formats. + * + * @default 'transparent' + */ + color?: string; + + /** + * Specifies the thickness of the border around the chart title and subtitle. + * + * @default 0 + */ + width?: number; + + /** + * Sets the corner radius of the border, enabling rounded corners. + * + * @default 0.8 + */ + cornerRadius?: number; + + /** + * Specifies the dash pattern used for the border stroke. + * Accepts a string of numbers that define the lengths of dashes and gaps. + * + * @default '' + */ + dashArray?: string; +} + +/** + * Defines the configuration options for customizing the chart title and subtitle. + * + * @private + */ +export interface TitleSettings { + + /** + * Specifies the font style for the chart title and subtitle text. + * + * @default 'Normal' + */ + fontStyle?: string; + + /** + * Sets the font size for the chart title and subtitle. + * + * @default '15px' + */ + size?: string; + + /** + * Specifies the font weight (thickness) for the chart title and subtitle text. + * + * @default '500' + */ + fontWeight?: string; + + /** + * Sets the text color for the chart title and subtitle. + * + * @default '' + */ + color?: string; + + /** + * Determines how the title and subtitle text is aligned within its container. + * + * @default 'Center' + */ + textAlignment?: HorizontalAlignment; + + /** + * Specifies the font family used for the chart title and subtitle. + */ + fontFamily?: string; + + /** + * Sets the opacity of the title and subtitle text. + * + * @default 1 + */ + opacity?: number; + + /** + * Controls how the title and subtitle text behaves when it exceeds the available space. + * + * @default 'Wrap' + */ + textOverflow?: TextOverflow; + + /** + * Specifies the position of the chart title and subtitle. + * + * Available options: + * - `Top`: Displays the title and subtitle at the top of the chart. + * - `Left`: Displays them on the left side. + * - `Bottom`: Displays them at the bottom. + * - `Right`: Displays them on the right side. + * - `Custom`: Positions them based on the specified `x` and `y` coordinates. + * + * @default 'Top' + */ + position?: TitlePosition; + + /** + * Defines the X-coordinate for positioning the chart title and subtitle when using `Custom` position. + * + * @default 0 + */ + x?: number; + + /** + * Defines the Y-coordinate for positioning the chart title and subtitle when using `Custom` position. + * + * @default 0 + */ + y?: number; + + /** + * Sets the background color for the chart title and subtitle area. + * + * @default 'transparent' + */ + background?: string; + + /** + * Configures the border settings for the chart title and subtitle. + */ + border?: TitleBorder; + + /** + * Provides accessibility options for the chart title and subtitle elements. + * + * @default { ariaLabel: null, focusable: true, role: null, tabIndex: 0 } + */ + accessibility?: ChartAccessibilityProps; +} + +/** + * Defines the configuration options for customizing the appearance of a border. + */ +export interface ChartBorderProps { + + /** + * Specifies the color of the border. Accepts any valid CSS color string, including hexadecimal and RGBA formats. + * + * @default '' + */ + color?: string; + + /** + * Sets the width of the border in pixels. + * + * @default 1 + */ + width?: number; + + /** + * Defines the dash pattern for the border stroke. + * Accepts a string of numbers that specify the lengths of dashes and gaps. + * + * @default '' + */ + dashArray?: string; +} + +/** + * Defines the margin settings around the chart area. + */ +export interface ChartMarginProps { + + /** + * Specifies the left margin of the chart, in pixels. + * + * @default 10 + */ + left?: number; + + /** + * Specifies the right margin of the chart, in pixels. + * + * @default 10 + */ + right?: number; + + /** + * Specifies the top margin of the chart, in pixels. + * + * @default 10 + */ + top?: number; + + /** + * Specifies the bottom margin of the chart, in pixels. + * + * @default 10 + */ + bottom?: number; +} + +/** + * Defines the font styling options for text elements. + */ +export interface ChartFontProps { + + /** + * Specifies the font style of the text (e.g., 'Normal', 'Italic'). + * + * @default 'Normal' + */ + fontStyle?: string; + + /** + * Sets the font size of the text in pixels. + * + * @default '' + */ + fontSize?: string; + + /** + * Specifies the font weight (thickness) of the text (e.g., 'Normal', 'Bold', '400'). + * + * @default 'Normal' + */ + fontWeight?: string; + + /** + * Sets the color of the text. Accepts any valid CSS color string, including hexadecimal and RGBA formats. + * + * @default '' + */ + color?: string; + + /** + * Specifies the font family used for the text (e.g., 'Arial', 'Verdana', 'sans-serif'). + * + * @default '' + */ + fontFamily?: string; + + /** + * Sets the opacity level of the text. A value of 1 means fully opaque, while 0 means fully transparent. + * + * @default 1 + */ + opacity?: number; + +} + +/** + * Defines the configuration options for customizing the chart area. + */ +export interface ChartAreaProps { + + /** + * Customizes the border appearance of the chart area, controlling border color, width, and dash pattern. + * + * @default {color: '', width: 0, dashArray: ''} + */ + border?: ChartBorderProps; + + /** + * Sets the background color of the chart area. + * Accepts valid CSS color strings including hex, RGB, and named colors. + * + * @default 'transparent' + */ + background?: string; + + /** + * Controls the transparency level of the chart area's background. + * A value of 1 is fully opaque, while 0 is fully transparent. + * + * @default 1 + */ + opacity?: number; + + /** + * Specifies a background image for the chart area. + * Accepts a URL or a local image path. + * + * @default null + */ + backgroundImage?: string; + + /** + * Sets the width of the chart area in pixels, helping maintain consistent chart proportions across different container sizes. + * Accepts values in pixels (e.g., '500px'). + * + * @default null + */ + width?: string; + + /** + * Defines the margin around the chart area. + * Creates space between the chart container and the plotting area. + * + * @default {left: 0, right: 0, top: 0, bottom: 0} + */ + margin?: ChartMarginProps; +} + +/** + * Defines the configuration options for enabling and customizing zooming and panning in the chart. + */ +export interface ChartZoomSettingsProps { + + /** + * Enables zooming by selecting a rectangular region within the chart area. + * Users can click and drag to define the region they want to zoom into. + * + * @default false + */ + selectionZoom?: boolean; + + /** + * Enables zooming with pinch gestures on touch-enabled devices. + * Users can pinch in to zoom in and pinch out to zoom out. + * + * @default false + */ + pinchZoom?: boolean; + + /** + * Enables chart zooming using the mouse wheel for desktop users. + * Scroll up to zoom in and scroll down to zoom out. + * + * @default false + */ + mouseWheelZoom?: boolean; + + /** + * Specifies the zooming direction for chart interactions. + * + * This property controls which axes can be zoomed when using the chart's zoom features. + * Selecting the appropriate mode allows users to focus on specific dimensions of the data. + * + * Available options: + * - `XY`: Enables both horizontal and vertical zooming, allowing users to zoom freely in any direction. + * - `X`: Enables horizontal zooming only, restricting zoom operations to the X-axis. + * - `Y`: Enables vertical zooming only, restricting zoom operations to the Y-axis. + * + * Note: The `selectionZoom` property must be set to `true` for this setting to take effect. + * + * @default 'XY' + */ + mode?: ZoomMode; + + /** + * Enables chart panning without requiring toolbar interaction. + * When enabled, users can pan a zoomed chart directly by clicking and dragging. + * + * @default false + */ + pan?: boolean; + + /** + * Provides accessibility options for zoom-related UI elements. + * Enhances screen reader support and keyboard navigation for zoom controls. + * + * @default {} + */ + accessibility?: ChartAccessibilityProps; + + /** + * Provides configuration settings for the zoom toolbar displayed on the chart. + * Lets you control its visibility, toolbar items, and position within the chart area. + * + * @default { visible: false, items: ['ZoomIn', 'ZoomOut', 'Pan', 'Reset'] } + */ + toolbar?: ChartToolbarProps; + +} + +/** + * Represents the event arguments triggered when a zoom operation is completed on the chart. + */ +export interface ZoomEndEvent { + + /** + * The name of the axis that was zoomed during the operation. + */ + axisName?: string; + + /** + * The zoom factor applied to the axis before the zoom operation. + */ + previousZoomFactor?: number; + + /** + * The zoom position of the axis before the zoom operation. + */ + previousZoomPosition?: number; + + /** + * The zoom factor applied to the axis after the zoom operation. + */ + currentZoomFactor?: number; + + /** + * The zoom position of the axis after the zoom operation. + */ + currentZoomPosition?: number; + + /** + * The visible range of the axis after the zoom operation. + * Includes updated minimum, maximum, interval, and delta values. + */ + currentVisibleRange?: VisibleRangeProps; + + /** + * The visible range of the axis before the zoom operation. + * Includes previous minimum, maximum, interval, and delta values. + */ + previousVisibleRange?: VisibleRangeProps; + +} + +/** + * Represents the event arguments triggered during a zooming operation on the chart. + */ +export interface ZoomStartEvent { + + /** + * A collection of axis data involved in the zoom operation. + */ + axisData: AxisDataProps[]; + + /** + * Indicates whether the event should be canceled. + * Set to `true` to prevent the default action. + */ + cancel: boolean; +} + +/** + * Defines the positioning options for the zoom toolbar within the chart. + */ +export interface ToolbarPosition { + + /** + * Specifies the horizontal alignment of the toolbar. + * + * Available options: + * - `Right`: Aligns the toolbar to the right side of the chart. + * - `Center`: Centers the toolbar horizontally within the chart. + * - `Left`: Aligns the toolbar to the left side of the chart. + * + * @default 'Right' + */ + hAlign?: HorizontalAlignment; + + /** + * Specifies the vertical alignment of the toolbar. + * + * Available options: + * - `Top`: Positions the toolbar at the top of the chart. + * - `Center`: Centers the toolbar vertically within the chart. + * - `Bottom`: Positions the toolbar at the bottom of the chart. + * + * @default 'Top' + */ + vAlign?: VerticalAlignment; + + /** + * Sets the horizontal offset of the toolbar from its default position, in pixels. + * + * @default 0 + */ + x?: number; + + /** + * Sets the vertical offset of the toolbar from its default position, in pixels. + * + * @default 0 + */ + y?: number; + +} + +/** + * Defines the configuration options for customizing the chart zoom toolbar. + */ +export interface ChartToolbarProps { + + /** + * When set to `true`, the zoom toolbar is displayed by default and remains permanently visible during chart interaction. + * + * @default false + */ + visible?: boolean; + + /** + * Defines the set of interactive tools available in the chart's zooming toolbar. + * + * Available options: + * - `ZoomIn`: Displays a tool that allows users to zoom into the chart. + * - `ZoomOut`: Displays a tool that allows users to zoom out of the chart. + * - `Pan`: Enables panning across the chart by dragging. + * - `Reset`: Resets the chart view to its original state. + * + * @default '["ZoomIn", "ZoomOut", "Pan", "Reset"]' + */ + items?: ToolbarItems[]; + + /** + * Customizes the position of the zoom toolbar within the chart area. + */ + position?: ToolbarPosition; +} + +/** + * Defines the configuration options for customizing a row within the chart layout. + */ +export interface Row { + + /** + * Sets the height of the row. + * Accepts values in pixels (e.g., `'100px'`) or percentages (e.g., `'100%'`). + * If set to `'100%'`, the row occupies the full height of the chart. + * + * @default '100%' + */ + height?: string; + + /** + * Customizes the border of the row. + * Accepts a `ChartBorderProps` object to configure the border color, width, and dash pattern. + * + * @default { color: '', width: 1, dashArray: '' } + */ + border?: ChartBorderProps; +} + +/** + * Represents the configuration options for the Chart component. + * + * @public + */ +export interface ChartComponentProps { + + /** + * Unique identifier for the chart element. + * + * @default '' + */ + id?: string; + + /** + * Sets the width of the chart. Accepts values in pixels (e.g., `'100px'`) or percentages (e.g., `'100%'`). + * If set to `'100%'`, the chart occupies the full width of its parent container. + * + * @default null + */ + width?: string; + + /** + * Sets the height of the chart. Accepts values in pixels (e.g., `'100px'`) or percentages (e.g., `'100%'`). + * If set to `'100%'`, the chart occupies the full height of its parent container. + * + * @default null + */ + height?: string; + + /** + * Customizes the chart border using `color`, `width`, and `dashArray` properties. + * + * @default { color: '', width: 1, dashArray: '' } + */ + border?: ChartBorderProps; + + /** + * Represents child elements that can be nested inside the chart component. + * + * @private + */ + children?: React.ReactNode; + + /** + * Customizes the margins around the chart. + * Defines the space between the chart's outer edge and its chart area. + * + * @default { top: 10, right: 10, bottom: 10, left: 10 } + */ + margin?: ChartMarginProps; + + /** + * Sets the background color of the chart. + * Accepts valid CSS color strings in hex or RGBA formats. + * + * @default null + */ + background?: string; + + /** + * Sets the background image of the chart. + * Accepts a URL or local image path. + * + * @default null + */ + backgroundImage?: string; + + /** + * If set to `true`, the chart is rendered in a transposed layout, swapping the X and Y axes. + * + * @default false + */ + transposed?: boolean; + + /** + * Applies a visual theme to the chart. + * + * Available options: + * - `Material3`: Applies the Material 3 light theme. + * - `Material3Dark`: Applies the Material 3 dark theme. + * + * @default 'Material3' + */ + theme?: Theme; + + /** + * Enables animation effects for chart elements such as axis labels, gridlines, series, markers, and data labels. + * When set to `true`, animations are triggered during interactions like legend item clicks or when the data source is updated. + * + * @default true + */ + enableAnimation?: boolean; + + /** + * Defines a set of colors used for rendering chart series. + * Each color in the array is applied sequentially to the series. + * + * @default [] + */ + palettes?: string[]; + + /** + * Provides accessibility options for chart container element. + * + * @default { ariaLabel: null, focusable: true, role: null, tabIndex: 0 } + */ + accessibility?: ChartAccessibilityProps; + + /** + * Specifies the visual outline style applied when the chart container receives focus. + * + * @default { width: 1.5, color: null, offset: 0 } + */ + focusOutline?: FocusOutlineProps; + + /** + * Controls whether columns for different series appear side by side in a column chart. + * + * @default true + */ + enableSideBySidePlacement?: boolean; + + /** + * Triggered continuously while a zooming operation is in progress. + * Fires when the user finishes a zoom action, such as releasing the mouse or completing a pinch gesture. + * Provides details about the zoomed axis, including its name, zoom factor, zoom position, and visible range before and after the operation. + * + * @event onZoomEnd + */ + onZoomEnd?: (args: ZoomEndEvent) => void; + + /** + * Triggered after a zoom selection operation is completed. + * Fires during user interactions such as dragging, pinching, or mouse wheel zooming. + * Provides access to the axis data involved in the zoom operation. + * + * @event onZoomStart + */ + onZoomStart?: (args: ZoomStartEvent) => void; + + /** + * Triggered when the mouse moves over the chart. + * Provides information about the mouse event, including the target element and pointer coordinates relative to the chart. + * + * @event onMouseMove + */ + onMouseMove?: (args: ChartMouseEvent) => void; + + /** + * Triggered when the mouse enters a chart element. + * Provides information about the mouse event, including the target element and pointer coordinates relative to the chart. + * + * @event onMouseEnter + */ + onMouseEnter?: (args: ChartMouseEvent) => void; + + /** + * Triggered when the chart is clicked. + * Provides information about the mouse event, including the target element and pointer coordinates. + * + * @event onClick + */ + onClick?: (args: ChartMouseEvent) => void; + + /** + * Triggered when the mouse leaves the chart. + * Provides information about the mouse event, including the target element and pointer coordinates relative to the chart. + * + * @event onClick + */ + onMouseLeave?: (args: ChartMouseEvent) => void; + + /** + * Triggered after a legend item is clicked. + * Provides details about the clicked legend item including associated series and data points. + * + * @event onLegendClick + */ + onLegendClick?: (args: LegendClickEvent) => void; + + /** + * Triggered after an axis label is clicked. + * Provides details about the clicked label including its position, value, and associated axis. + * + * @event onAxisLabelClick + */ + onAxisLabelClick?: (args: AxisLabelClickEvent) => void; + + /** + * Triggered when a data point in the chart is clicked. + * Provides details about the clicked point, including its position, series index, and mouse coordinates. + * + * @event onPointClick + */ + onPointClick?: (args: PointClickEvent) => void; + + /** + * Triggered after the chart is resized. + * Provides details about the chart's size before and after the resize. + * + * @event onResize + */ + onResize?: (args: ResizeEvent) => void; +} + +/** + * Provides information about mouse events triggered within the chart. + */ +export interface ChartMouseEvent { + + /** + * The ID of the element that is the target of the mouse event. + */ + target: string; + + /** + * The X-coordinate of the mouse pointer relative to the chart. + */ + x: number; + + /** + * The Y-coordinate of the mouse pointer relative to the chart. + */ + y: number; + +} + +/** + * Represents a chart axis with configurable properties for data type, appearance, and behavior. + */ +export interface ChartAxisProps { + + /** + * Specifies the type of data the axis represents to ensure appropriate rendering. + * + * Available options: + * - `Double`: Numeric axis for numerical data. + * - `DateTime`: Date-time axis for temporal data. + * - `Category`: Category axis for categorical data. + * - `Logarithmic`: Logarithmic axis for data with a wide range of values. + * + * @default 'Double' + */ + valueType?: AxisValueType; + + /** + * A unique identifier for the axis. + * To associate an axis with a series, set this name in the series' `xAxisName` or `yAxisName`. + * + * @default '' + */ + name?: string; + + /** + * Specifies the index of the column to which the axis is assigned when the chart area is divided using `columns`. + * + * @default 0 + */ + columnIndex?: number; + + /** + * Specifies how many columns or rows the axis should span in the chart layout. + * + * @default 1 + */ + span?: number; + + /** + * Specifies the index of the row to which the axis is assigned when the chart area is divided using `rows`. + * + * @default 0 + */ + rowIndex?: number; + + /** + * When set to `true`, the axis is rendered on the opposite side of its default position. + * + * @default false + */ + opposedPosition?: boolean; + + /** + * When set to `true`, the axis is rendered in reverse order, displaying values from maximum to minimum. + * + * @default false + */ + inverted?: boolean; + + /** + * When set to `true`, data points are rendered based on their index position rather than their actual x-axis values. + * + * @default false + */ + indexed?: boolean; + + /** + * When set to `false`, axis labels are hidden from the chart. + * + * @default true + */ + visible?: boolean; + + /** + * Sets the maximum value of the axis range. + * Defines the upper bound of the axis and controls the visible data range. + * + * @default null + */ + maximum?: Object; + + /** + * Sets the minimum value of the axis range. + * Defines the lower bound of the axis and controls the visible data range. + * + * @default null + */ + minimum?: Object; + + /** + * Specifies the interval between axis labels or ticks. + * + * @default null + */ + interval?: number; + + /** + * Defines how intervals are calculated and displayed on a date-time axis. + * + * Available options: + * - `Auto`: Automatically determines the interval based on the data. + * - `Years`: Uses yearly intervals. + * - `Months`: Uses monthly intervals. + * - `Days`: Uses daily intervals. + * - `Hours`: Uses hourly intervals. + * - `Minutes`: Uses minute-based intervals. + * - `Seconds`: Uses second-based intervals. + * + * @default 'Auto' + */ + intervalType?: IntervalType; + + /** + * Specifies the skeleton format used for processing date-time values. + * + * @default '' + */ + skeleton?: string; + + /** + * Specifies the format type used for date-time formatting. + * + * Available options: + * - `Date`: Formats and displays only the date portion. + * - `DateTime`: Formats and displays both date and time. + * - `Time`: Formats and displays only the time portion. + * + * @default 'DateTime' + */ + skeletonType?: SkeletonType; + + /** + * When set to `true`, the axis starts from zero, ensuring a baseline reference for data comparison. + * When set to `false`, the axis starts from the minimum value in the dataset, which can help highlight variations + * in data when zero is not a meaningful starting point. + * + * @default true + */ + startFromZero?: boolean; + + /** + * Specifies the desired number of intervals for the axis. + * The actual number may vary depending on the available space and data range. + * + * @default null + */ + desiredIntervals?: number; + + /** + * Specifies the maximum number of labels per 100 pixels of axis length. + * + * @default 3 + */ + maxLabelDensity?: number; + + /** + * Sets the position of the zoomed axis within the zoomed range. + * Value ranges from 0 to 1. + * + * @default 0 + */ + zoomPosition?: number; + + /** + * Scales the axis by the specified factor. + * For example, a `zoomFactor` of 0.5 scales the chart by 200% along this axis. + * + * > Value must be between 0 and 1. + * + * @default 1 + */ + zoomFactor?: number; + + /** + * Customizes the appearance of the axis line. + * Accepts an `AxisLine` object to configure properties such as color, width, and dash pattern. + * + * @default {width: 1, color: '', dashArray: ''} + */ + lineStyle?: AxisLine; + + /** + * Sets padding around the chart area in pixels. + * + * @default {left: 0, right: 0, top: 0, bottom: 0} + */ + plotOffset?: ChartPaddingProps; + + /** + * Specifies React child elements to be rendered within the chart component. + * Can include configuration components, annotations, or custom elements. + * + * @private + */ + children?: React.ReactNode; + + /** + * Determines the position of axis ticks relative to the axis line. + * + * Available options: + * - `Inside`: Renders ticks inside the axis line. + * - `Outside`: Renders ticks outside the axis line. + * + * @default 'Outside' + */ + tickPosition?: AxisLabelPosition; + + /** + * Specifies the number of minor ticks per interval. + * + * @default 0 + */ + minorTicksPerInterval?: number; + + /** + * Controls how padding is applied to the axis range. + * + * Available options: + * - `None`: No padding. + * - `Normal`: Padding based on range calculation. + * - `Additional`: Adds one interval to both ends of the range. + * - `Round`: Rounds the range to the nearest interval. + * + * @default 'Auto' + */ + rangePadding?: ChartRangePadding; + + /** + * Sets the base value for a logarithmic axis. + * + * > `valueType` must be set to `Logarithmic` for this to take effect. + * + * @default 10 + */ + logBase?: number; + +} + +/** + * Defines the configuration options for customizing a column within the chart layout. + */ +export interface Column { + + /** + * Sets the width of the column. + * Accepts values in pixels (e.g., `'100px'`) or percentages (e.g., `'100%'`). + * If set to `'100%'`, the column occupies the full width of the chart. + * + * @default '100%' + */ + width?: string; + + /** + * Customizes the border of the column. + * Accepts a `ChartBorderProps` object to configure the border color, width, and dash pattern. + * + * @default { color: '', width: 1, dashArray: '' } + */ + border?: ChartBorderProps; +} + +/** + * Defines the properties for the Columns layout component used in chart rendering. + * + * @private + */ +export interface ColumnsProps { + + /** + * React child elements to be rendered inside the Columns layout. + * These can include axis configurations, series, or other chart components. + */ + children?: React.ReactNode; +} + +/** + * Defines the appearance settings for major grid lines in the chart. + */ +export interface MajorGridLines { + + /** + * Specifies the width of the major grid lines, in pixels. + * A value of `0` hides the grid lines. + * + * @default null + */ + width?: number | null; + + /** + * Defines the dash pattern for the major grid lines. + * Accepts a string of comma-separated numbers (e.g., `'5,5'`) to create dashed lines. + * + * @default '' + */ + dashArray?: string; + + /** + * Specifies the color of the major grid lines. + * Accepts any valid CSS color string. + * + * @default null + */ + color?: string; +} + +/** + * Defines the appearance settings for minor grid lines in the chart. + */ +export interface MinorGridLines { + + /** + * Specifies the width of the minor grid lines, in pixels. + * A value of `0` hides the grid lines. + * + * @default 1 + */ + width?: number; + + /** + * Defines the dash pattern for the minor grid lines. + * Accepts a string of comma-separated numbers (e.g., `'2,2'`) to create dashed lines. + * + * @default '' + */ + dashArray?: string; + + /** + * Specifies the color of the minor grid lines. + * Accepts any valid CSS color string. + * + * @default null + */ + color?: string; +} + +/** + * Defines the appearance settings for major tick lines in the chart. + */ +export interface MajorTickLines { + + /** + * Specifies the width of the major tick lines, in pixels. + * A value of `0` hides the tick lines. + * + * @default 0 + */ + width?: number; + + /** + * Specifies the height of the major tick lines, in pixels. + * Determines how far the tick lines extend from the axis. + * + * @default 5 + */ + height?: number; + + /** + * Specifies the color of the major tick lines. + * Accepts any valid CSS color string. + * + * @default null + */ + color?: string; +} + +/** + * Defines the appearance settings for minor tick lines in the chart. + */ +export interface MinorTickLines { + + /** + * Specifies the width of the minor tick lines, in pixels. + * A value of `0` hides the tick lines. + * + * @default 1 + */ + width?: number; + + /** + * Specifies the height of the minor tick lines, in pixels. + * Determines how far the tick lines extend from the axis. + * + * @default 3 + */ + height?: number; + + /** + * Specifies the color of the minor tick lines. + * Accepts any valid CSS color string. + * + * @default null + */ + color?: string; +} + +/** + * Defines the properties for the Series layout component, typically used to wrap and render one or more chart series. + * + * @private + */ +export interface SeriesProps { + + /** + * React child elements (series components) to be rendered inside the Series container. + */ + children?: React.ReactNode; +} + +/** + * Represents the configuration model for a chart series. + * Extend this interface to define properties related to series data and appearance. + * + * @public + */ +export interface ChartSeriesProps { + /** + * Specifies the name of the field in the data source that maps values to the x-axis of the chart. This property is essential for identifying which data dimension should be plotted horizontally, such as categories, timestamps, or numerical values. + * + * @default '' + */ + xField?: string; + + /** + * Controls the visibility of the series on the chart. + * When set to `false`, the series is hidden. + * + * @default true + */ + visible?: boolean; + + /** + * Defines the border styling for the series. This includes customization options such as border color and width. + * + * @default { color: '', width: 0 } + */ + border?: ChartBorderProps; + + /** + * Specifies the name of the horizontal axis associated with the series. + * Requires the `axes` configuration in the chart. + * + * @default null + */ + xAxisName?: string | null; + + /** + * Specifies the name of the vertical axis associated with the series. Requires the `axes` configuration in the chart. + * + * @default null + */ + yAxisName?: string | null; + + /** + * Sets the fill color of the series. Accepts any valid CSS color value, including hex codes, RGB, RGBA, HSL. + * + * @default null + */ + + fill?: string | null; + + /** + * Defines the stroke width of the series, in pixels. This controls the thickness of the line type series. + * + * @default 1 + */ + width?: number; + + /** + * Defines the dash pattern for the stroke in `Line` type series. Use a string format to specify dash and gap lengths (e.g., "4,2"). + * + * @default '' + */ + dashArray?: string; + + /** + * Specifies the data source for the series. + * Can be an array of JSON objects or an instance of `DataManager`. + * + * @default '' + */ + dataSource?: Object | DataManager; + + /** + * Defines a query to retrieve data from the data source. + * Applicable only when using `ej.DataManager` as the data source. + * + * @default '' + */ + query?: Query | string; + + /** + * Enables complex property mapping to improve performance when binding large data sets. + * + * @default false + */ + enableComplexProperty?: boolean; + + /** + * Sets the name of the series, which is displayed in the chart legend. + * Useful for identifying multiple series. + * + * @default '' + */ + name?: string; + + /** + * Specifies the name of the field in the data source that provides the values to be plotted along the y-axis. This property is used to map vertical data points in the chart, such as numerical values or metrics. + * + * @default '' + */ + yField?: string; + + /** + * Sets the opacity of the series. + * Accepts a value between 0 (fully transparent) and 1 (fully opaque). + * + * @default 1 + */ + opacity?: number; + + /** + * Specifies the name of the data field that contains the values used to determine the size (radius) of each bubble in a bubble chart. + * + * @default '' + */ + sizeField?: string; + + /** + * Determines the rendering order of the series within the chart. Series with higher `zOrder` values are drawn above those with lower values. + * + * @default 0 + */ + zOrder?: number; + + /** + * Defines the type of series used to visualize the data. + * Supported types include: + * - `Line` - Renders a line chart. + * - `Column` - Renders a column chart. + * - `Area` - Renders an area chart. + * - `Bar` - Renders a bar chart. + * - `StackingColumn` - Renders a stacking column chart. + * - `StackingBar` - Renders a stacking bar chart. + * - `StepLine` - Renders a step line chart. + * - `SplineArea` - Renders a spline area chart. + * - `Scatter` - Renders a scatter chart. + * - `Spline` - Renders a spline chart. + * - `Bubble` - Renders a bubble chart. + * + * @default 'Line' + */ + type?: ChartSeriesType; + + /** + * Specifies the position of steps in step line chart type. + * * `Left` - Steps begin from the left side of the second point. + * * `Right` - Steps begin from the right side of the first point. + * * `Center` - Steps begin between the data points. + * + * @default 'Left' + */ + step?: StepPosition; + + /** + * Enhances accessibility for series elements to ensure compatibility with assistive technologies, such as screen readers and keyboard navigation. + * + * @default { ariaLabel: null, descriptionFormat: null, focusable: true, role: null, tabIndex: 0 } + */ + accessibility?: SeriesAccessibility; + + /** + * Customizes the appearance of empty points in the series. + * Points with `null` or `undefined` values are treated as empty. + * + * @default { border: {color: 'gray', width: 1 }, mode: 'Gap', fill: 'gray' } + */ + emptyPointSettings?: EmptyPointSettings; + + /** + * Determines whether vertical risers are rendered in a step series. + * When set to `true`, the step series is drawn without vertical lines between horizontal steps, resulting in a flat step appearance. + * Applicable only to step series chart types. + * + * @default false + */ + noRisers?: boolean; + + /** + * Maps a data source field to assign individual colors to each point in the series. + * + * @default '' + */ + colorField?: string; + + /** + * + * Specifies animation settings for the series, including options to enable or disable animation, and configure its duration and delay for smoother visual transitions. + * + * @default { enable: true, duration: 1000, delay: 0 } + */ + animation?: Animation; + + /** + * Groups series in stacked column and stacked bar charts. + * Series with the same `stackingGroup` value are stacked together. + * + * @default '' + */ + stackingGroup?: string; + + /** + * Selects the algorithm used to draw curved lines between data points in spline series. + * + * Available options: + * * `Natural` - Renders a natural spline. + * * `Cardinal` - Renders a cardinal spline. + * * `Clamped` - Renders a clamped spline. + * * `Monotonic` - Renders a monotonic spline. + * + * @default 'Natural' + */ + splineType?: SplineType; + + /** + * Specifies the tension parameter for cardinal splines. This affects the curvature of the spline. + * + * @default 0.5 + */ + cardinalSplineTension?: number; + + /** + * Defines a custom format string for displaying tooltips when hovering over data points in this series. + * + * The format string can include placeholders that will be replaced with actual values. + * + * @default '' + */ + tooltipFormat?: string; + + /** + * Maps a specific field from the data source to use as tooltip content. + * The mapped field's value is stored in the point's tooltip property and it can be accessed through tooltip format. + * + * + * @default '' + */ + tooltipField?: string; + + /** + * Specifies the shape used to represent the series in the chart legend. + * + * @default 'SeriesType' + */ + legendShape?: LegendShape; + + /** + * Sets a fixed column width in pixels for points in column and bar charts. This property overrides relative sizing and ensures consistent column width across categories. + * + * @default null + */ + columnWidthInPixel?: number; + + /** + * Defines the spacing between columns in column or bar charts. + * Accepts a value between 0 and 1. + * + * @default 0 + */ + columnSpacing?: number; + + /** + * Specifies the relative width of each column in column and bar charts. Accepts a value between `0` and `1`, where `1` means columns occupy the full available width within a category, and `0.5` means they occupy half the space. + * + * @default null + */ + columnWidth?: number; + + /** + * Specifies a group name to overlay mutually exclusive chart series. + * Series in the same group share the same baseline and axis location. + * + * @default '' + */ + groupName?: string; + + /** + * Defines the corner radius for data points. + * + * @default { topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0 } + */ + cornerRadius?: CornerRadius; + + /** + * Settings for displaying markers at each data point in the series. Allows customization of shape, size, and color of marker. + * + * @private + */ + marker?: ChartMarkerProps; + + /** + * Sets the minimum radius for data points (bubbles) in the series. + * + * @default 1 + */ + minRadius?: number; + + /** + * Sets the maximum radius for data points (bubbles) in the series. + * + * @default 3 + */ + maxRadius?: number; + + /** + * Determines whether tooltips for the chart series are enabled. + * Set to `false` to hide the tooltip for a particular series. + * + * @default true + */ + enableTooltip?: boolean; + +} + +/** + * Configuration options for handling empty data points in a chart series. + */ +export interface EmptyPointSettings { + + /** + * Sets the fill color for empty points in the series. + * + * @default null + */ + fill?: string; + + /** + * Customizes the border of empty points, including color and width. + * + * @default { color: 'transparent', width: 0 } + */ + border?: ChartBorderProps; + + /** + * Specifies how empty or missing data points should be rendered in the series. + * Available modes: + * * `Gap` - Displays empty points as gaps in the series. + * * `Zero` - Treats empty points as zero values. + * * `Drop` - Ignores empty points during rendering. + * * `Average` - Replaces empty points with the average of the previous and next points. + * + * @default 'Gap' + */ + mode?: EmptyPointMode; +} + +/** + * Configuration options for enhancing the accessibility of chart elements. + */ +export interface ChartAccessibilityProps { + + /** + * Provides a descriptive label for the chart to assist screen readers. + * This value is automatically mapped to the `aria-label` attribute in the DOM. + * + * @default null + */ + ariaLabel?: string; + + /** + * Specifies the ARIA role of the chart element. Helps assistive technologies understand the semantic purpose of the chart (e.g., "img", "figure", "application"). + * If not set, the default role will be inferred based on the element type. + * + * @default null + */ + role?: string; + + /** + * Determines whether chart elements can receive keyboard focus. + * Set to `false` to exclude chart elements from the tab order, which may be useful for purely decorative charts. + * + * @default true + */ + focusable?: boolean; + + /** + * Controls the tab order for keyboard navigation. + * A value of `0` places the chart element in the natural tab sequence. + * + * @default 0 + */ + tabIndex?: number; +} + +/** + * Extends accessibility settings specifically for chart series elements. + */ +export interface SeriesAccessibility extends ChartAccessibilityProps { + + /** + * Defines a format string for the accessibility description of the chart series. + * This format is used by screen readers to describe the series contextually. + * + * @default null + */ + descriptionFormat?: string; +} + +/** + * Configuration options for rendering markers in a chart series. + */ +export interface ChartMarkerProps { + + /** + * When set to `true`, the marker is rendered and visible on the chart. + * + * @default false + */ + visible?: boolean; + + /** + * Specifies the shape of the marker used in the series. + * Available options: + * * `Circle` - Circular marker. + * * `Rectangle` - Rectangular marker. + * * `Triangle` - Triangular marker. + * * `Diamond` - Diamond-shaped marker. + * * `HorizontalLine` - Horizontal line marker. + * * `VerticalLine` - Vertical line marker. + * * `Pentagon` - Pentagon-shaped marker. + * * `InvertedTriangle` - Inverted triangle marker. + * * `Image` - Custom image marker. + * * `Star` - Star-shaped marker. + * + * @default null + */ + shape?: ChartMarkerShape | null; + + /** + * Sets the URL of the image to be used as a marker and it requires `shape` to be set to `Image`. + * + * @default '' + */ + imageUrl?: string; + + /** + * Specifies the height of the marker in pixels. + * + * @default 5 + */ + height?: number; + + /** + * Determines whether the marker should be filled with the series color. + * When set to `true`, the marker is filled using the corresponding series color, enhancing visual distinction. + * When set to `false`, the marker will be rendered with no fill or default styling. + * + * @default false + */ + filled?: boolean; + + /** + * Specifies the width of the marker in pixels. + * + * @default 5 + */ + width?: number; + + /** + * Defines the border styling for the marker, including width and color. + * + * @default { color: '', width: 2, dashArray: '' } + */ + border?: ChartBorderProps; + + /** + * Sets the fill color of the marker. Accepts valid CSS color values such as hex codes or RGBA. Defaults to the series color if not specified. + * + * @default null + */ + fill?: string | null; + + /** + * + * When set to `true`, markers are visually emphasized on hover or selection, enhancing visibility and user feedback during data exploration. + * + * @default true + */ + highlightable?: boolean; + + /** + * Sets the opacity of the marker. + * Accepts values from 0 (fully transparent) to 1 (fully opaque). + * + * @default 1 + */ + opacity?: number; + + /** + * Defines the configuration for displaying and styling data labels associated with markers. + * + * @private + */ + dataLabel?: ChartDataLabelProps; + + /** + * Adjusts the marker's position relative to its data point using horizontal and vertical offsets. + * + * @default { x: 0, y: 0 } + */ + offset?: ChartLocationProps; + + /** + * Allows rendering of child components within the marker. + * + * @private + */ + children?: React.ReactNode; +} + +/** + * Configuration options for customizing data labels in chart series. + */ +export interface ChartDataLabelProps { + + /** + * Controls the visibility of data labels in the series. Set to `true` to display labels for each data point. + * + * @default false + */ + visible?: boolean; + + /** + * Sets whether to display data labels for zero values in the series. + * + * When `true`, labels are shown even when the value is zero. + * When `false`, labels for zero values are hidden. + * + * @default true + */ + showZero?: boolean; + + /** + * Maps a specific field from the data source to use as the data label content. + * + * The mapped field's value is displayed as the label for each data point. + * + * @default null + */ + labelField?: string | null; + + /** + * Specifies the background color of the data label. + * Use any valid CSS color values such as hex codes or RGBA. + * + * @default 'transparent' + */ + fill?: string; + + /** + * Used to format the data label. This property accepts global string formats such as `C`, `n1`, `P`, etc. It also accepts placeholders like `{value}°C`, where `{value}` represents the data label (e.g., 20°C). + * + * @default null + */ + format?: string | null; + + /** + * Specifies opacity level of the data label background. + * Accepts values from 0 (transparent) to 1 (opaque). + * + * @default 1 + */ + opacity?: number; + + /** + * Specifies the rotation angle (in degrees) for the data label. + * + * A positive value rotates the label clockwise, while a negative value rotates it counterclockwise. + * This property is only effective when `enableRotation` is set to `true`. + * + * @default 0 + */ + rotationAngle?: number; + + /** + * Specifies whether the data label should be rotated based on the provided angle. + * + * When set to `true`, the label is rotated according to the `angle` value. + * When set to `false`, the label remains in its default orientation regardless of the angle. + * + * @default false + */ + enableRotation?: boolean; + + /** + * Sets the position of the data label relative to the data point. + * Available options: + * * `Outer` - Outside the data point. + * * `Top` - Above the data point. + * * `Bottom` - Below the data point. + * * `Middle` - Centered on the data point. + * * `Auto` - Automatically determined based on context. + * + * @default 'Auto' + */ + position?: LabelPosition; + + + /** + * Specifies the horizontal (`x`) and vertical (`y`) corner radius + * for the background of the data label. + * + * This controls how rounded the corners of the label background appear. + * + * @default { x: 5, y: 5 } + */ + borderRadius?: BorderRadiusProps; + + /** + * Sets the alignment of the data label relative to the data point. + * + * Available options: + * * `Left` - Left-aligned. + * * `Center` - Center-aligned. + * * `Right` - Right-aligned. + * + * @default 'Center' + */ + textAlign?: HorizontalAlignment; + + /** + * Customizes the border appearance of the data label, including width and color. + * + * @default { color: '', width: 1 } + */ + border?: ChartBorderProps; + + /** + * Sets the margin around the data label with top, right, bottom, and left values. + * + * @default { left: 5, right: 5, top: 5, bottom: 5 } + */ + margin?: ChartMarginProps; + + /** + * Customizes the font used in the data label, including size, color, style, weight, and family. + * + * @default { color: '', fontFamily: '', fontSize: '12px', fontStyle: 'Normal', fontWeight: 'Normal', opacity: 1 } + */ + font?: ChartFontProps; + + /** + * Specifies how overlapping data labels are handled. + * Available options: + * * `None` - All labels are shown, even if they overlap. + * * `Hide` - Overlapping labels are hidden. + * * `Rotate90` - Labels are rotated 90° to reduce overlap. + * + * @default 'Hide' + */ + intersectMode?: DataLabelIntersectMode; + + /** + * Optional function to customize the content of the data label. + * + * If provided, this callback will be invoked for each data label during rendering. + * It receives the following arguments: + * - `index`: The index of the data point in the series. + * - `text`: The current formatted text of the data label. + * + * @param index The index of the data point in the series. + * @param text The current formatted text of the data label. + * @returns A string or boolean value to customize the label rendering. + * + * @default null + */ + formatter?: (index: number, text: string) => string | boolean; +} + +/** + * A function type used to customize the content of data labels. + * + * @param {number} index - The index of the data point associated with the label. + * @param {text} text - The current formatted text of the data label. + * @returns {string} A string representing the customized label content. + * @private + */ +export type DataLabelContentFunction = (index: number, text: string) => string | boolean; + +/** + * Represents the event arguments triggered when a data point is clicked in the chart. + */ +export interface PointClickEvent { + + /** + * The index of the clicked point within its data series. + */ + pointIndex: number; + + /** + * The index of the series that contains the clicked point. + */ + seriesIndex: number; + + /** + * The x-coordinate of the mouse pointer relative to the chart area at the time of the click. + */ + x: number; + + /** + * The y-coordinate of the mouse pointer relative to the chart area at the time of the click. + */ + y: number; + + /** + * The x-coordinate of the mouse pointer relative to the entire page. + */ + pageX?: number; + + /** + * The y-coordinate of the mouse pointer relative to the entire page. + */ + pageY?: number; +} + +/** + * Defines the corner radius settings for chart elements. + */ +export interface CornerRadius { + + /** + * Specifies the radius for the top-left corner. + * + * @default 0 + */ + topLeft?: number; + + /** + * Specifies the radius for the top-right corner. + * + * @default 0 + */ + topRight?: number; + + /** + * Specifies the radius for the bottom-left corner. + * + * @default 0 + */ + bottomLeft?: number; + + /** + * Specifies the radius for the bottom-right corner. + * + * @default 0 + */ + bottomRight?: number; +} + +/** + * Defines configuration options for the chart's title, including styling, positioning, and accessibility features. + */ +export interface ChartTitleProps { + + /** + * Specifies the main title text of the chart. This text provides context or a label for the chart's data. + * + * @default 'Normal' + */ + text?: string; + + /** + * Specifies the font style of the title (e.g., 'Normal', 'Italic'). + * + * @default 'Normal' + */ + fontStyle?: string; + + /** + * Sets the font size of the text in pixels. + * + * @default '15px' + */ + fontSize?: string; + + /** + * Specifies the font weight (thickness) of the title (e.g., 'Normal', 'Bold', '400'). + * + * @default '500' + */ + fontWeight?: string; + + /** + * Sets the color of the title. Accepts any valid CSS color string, including hexadecimal and RGBA formats. + * + * @default '' + */ + color?: string; + + /** + * Determines the alignment of the title within its container. + * + * Available options: + * - `Left`: Aligns the text to the left. + * - `Center`: Aligns the text to the center. + * - `Right`: Aligns the text to the right. + * + * @default 'Center' + */ + align?: HorizontalAlignment; + + /** + * Specifies the font family used for the title (e.g., 'Arial', 'Verdana', 'sans-serif'). + * + * @default '' + */ + fontFamily?: string; + + /** + * Sets the opacity level of the title. A value of 1 means fully opaque, while 0 means fully transparent. + * + * @default 1 + */ + opacity?: number; + + /** + * Controls how the title behaves when it overflows its container. + * + * Available options: + * - `Wrap`: Wraps the text to the next line. + * - `Trim`: Trims the overflowed text. + * - `None`: Displays the text even if it overlaps other elements. + * + * @default 'Wrap' + */ + textOverflow?: TextOverflow; + + /** + * Determines the position of the chart title relative to the chart area. + * + * Available options: + * - `Top`: Displays the title above the chart. + * - `Left`: Displays the title to the left of the chart. + * - `Bottom`: Displays the title below the chart. + * - `Right`: Displays the title to the right of the chart. + * - `Custom`: Allows manual positioning using `x` and `y` coordinates. + * + * @default 'Top' + */ + position?: TitlePosition; + + /** + * X-coordinate for positioning the chart title. Only applicable when `position` is set to `Custom`. + * + * @default 0 + */ + x?: number; + + /** + * Y-coordinate for positioning the chart title.Only applicable when `position` is set to `Custom`. + * + * @default 0 + */ + y?: number; + + /** + * The background color of the chart title area. + * Accepts any valid CSS color value. + * + * @default 'transparent' + */ + background?: string; + + /** + * Defines the border styling for the chart title area. + * + * @default { color: '', width: 1, dashArray: '' } + */ + border?: TitleBorder; + + /** + * Provides customization options to enhance accessibility for the chart title. + * + * @default { ariaLabel: null, focusable: true, role: null, tabIndex: 0 } + */ + accessibility?: ChartAccessibilityProps; +} + +/** + * Represents the configuration model for tooltips in charts. + */ +export interface ChartTooltipProps { + + /** + * When set to `true`, tooltips are displayed when hovering over data points. + * + * @default false + */ + enable?: boolean; + + /** + * When set to `true`, displays colored markers within the tooltip to indicate the corresponding series for each data point. + * + * @default true + */ + showMarker?: boolean; + + /** + * When set to `true`, displays a single tooltip showing all data points that share the same x-value. + * + * @default false + */ + shared?: boolean; + + /** + * Sets the background color of the tooltip. + * Accepts any valid CSS color value (hex, RGB, named colors). + * + * @default null + */ + fill?: string; + + /** + * Customizes the header text displayed at the top of the tooltip. + * By default, displays the series name. + * + * @default null + */ + headerText?: string; + + /** + * Controls the transparency level of the tooltip. + * Values range from 0 (fully transparent) to 1 (fully opaque). + * + * @default null + */ + opacity?: number; + + /** + * Defines a custom format string for tooltip content displayed when hovering over data points. + * + * The format can include placeholder tokens that will be replaced with actual values: + * - `${point.x}` - Represents the x-value of the data point. + * - `${point.y}` - Represents the y-value of the data point. + * - `${series.name}` - Represents the name of the data series the point belongs to. + * + * @default null + */ + format?: string; + + /** + * When set to `true`, enables smooth animation when the tooltip transitions between data points. + * + * @default true + */ + enableAnimation?: boolean; + + /** + * Sets the duration of tooltip animations in milliseconds. + * + * @default 300 + */ + duration?: number; + + /** + * Controls how long the fade-out animation lasts when hiding the tooltip. + * + * @default 1000 + */ + fadeOutDuration?: number; + + /** + * Specifies a fixed position for the tooltip relative to the chart. + * For example, `x: 20` positions the tooltip 20 pixels to the right. + * + * @default { x: 0, y: 0 } + */ + location?: ChartLocationProps; + + /** + * When set to `true`, includes the nearest data point in the shared tooltip. + * + * @default true + */ + showNearestPoint?: boolean; + + /** + * When set to `true`, displays a horizontal line separating the tooltip header from its content. + * + * @default false + */ + showHeaderLine?: boolean; + + /** + * Defines the font styling for the tooltip text, including font family, size, weight, and color. + * + * @default { color: '', fontFamily: '', fontStyle: 'Normal', fontWeight: 'Normal', opacity: 1 } + */ + textStyle?: ChartFontProps; + + /** + * Customizes the tooltip border, including color and width. + * + * @default { color: '', width: 1, dashArray: '' } + */ + border?: ChartBorderProps; + + /** + * Specifies the fade-out animation mode when hiding the tooltip. + * + * Available options: + * - `Click`: The tooltip is removed when the user clicks on the chart. + * - `Move`: The tooltip fades out after a short delay when the pointer moves away. + * + * @default 'Move' + */ + fadeOutMode?: FadeOutMode; + + /** + * When set to `true`, displays a tooltip for the data point nearest to the cursor. + * Applicable for line, area, spline, and spline area series. + * + * @default true + */ + showNearestTooltip?: boolean; + + /** + * A callback function that allows for custom rendering of chart tooltips. + * This function is invoked for each tooltip and receives its properties as an argument. + * Available arguments: + * - `text`: The content of the tooltip, which can be a string or an array of strings. + * + * @param text The content of the tooltip to be formatted. + * @returns A string, an array of strings, or a boolean to customize the tooltip rendering. + * + * @default null + */ + formatter?: (text: string | string[]) => string | string[] | boolean; +} + +/** + * Provides data for the event triggered when an axis label is clicked in a chart. + */ +export interface AxisLabelClickEvent { + + /** + * The name of the axis to which the clicked label belongs is represented. + */ + axisName: string; + + /** + * The text content of the clicked axis label. + */ + text: string; + + /** + * The index of the clicked axis label. + */ + index: number; + + /** + * The location of the clicked axis label within the chart. + */ + location: ChartLocationProps; + + /** + * The value associated with the clicked axis label. + */ + value: number; +} + +/** + * Provides event arguments for the chart resize event after it has occurred. + */ +export interface ResizeEvent { + + /** + * Size of the chart before resizing. + */ + previousSize: ChartSizeProps; + + /** + * Size of the chart after resizing. + */ + currentSize: ChartSizeProps; + +} + +/** + * Defines the border radius configuration for chart elements. + * This interface provides properties to customize the corner radius for both horizontal and vertical axes. + */ +export interface BorderRadiusProps { + /** + * Sets the horizontal (X-axis) corner radius for the element background. + * This property controls the curvature of the left and right corners. + * + * @default 5 + */ + x?: number; + + /** + * Sets the vertical (Y-axis) corner radius for the element background. + * This property controls the curvature of the top and bottom corners. + * + * @default 5 + */ + y?: number; +} + +/** + * Defines the configuration options for stack labels in a chart. + */ +export interface ChartStackLabelsProps { + + /** + * When set to `true`, stack labels are displayed on the chart. + * + * @default false + */ + visible?: boolean; + + /** + * Sets the background color of the stack labels. + * Accepts valid CSS color values such as hex codes or RGBA. + * + * @default 'transparent' + */ + fill?: string; + + /** + * Custom format string for the stack label text. + * Supports placeholders such as `{value}`, where `{value}` represents the total stack value. + * + * @default null + */ + format?: string | null; + + /** + * Specifies the rotation angle of the stack labels in degrees. + * + * @default 0 + */ + rotationAngle?: number; + + /** + * Defines the border radius configuration for the stack label background. + * Controls the curvature of corners for both horizontal and vertical axes. + * + * @default { rx: 0, ry: 0 } + */ + borderRadius?: BorderRadiusProps; + + /** + * Configures the margin around the stack label. + * + * @default { left: 0, right: 0, top: 0, bottom: 0 } + */ + margin?: ChartMarginProps; + + /** + * Customizes the border appearance of the stack labels. + * + * @default { color: 'transparent', width: 0 } + */ + border?: ChartBorderProps; + + /** + * Defines the font styling for the stack label text. + * + * @default { fontStyle: 'Normal', fontSize: '12px', fontWeight: 'Normal', color: '', fontFamily: '' } + */ + font?: ChartFontProps; + + /** + * Determines the alignment of the text within its container. + * + * Available options: + * - `Left`: Aligns the text to the left. + * - `Center`: Aligns the text to the center. + * - `Right`: Aligns the text to the right. + * + * @default 'Center' + */ + align?: HorizontalAlignment; +} + +/** + * Defines the configuration options for the chart legend. + */ +export interface ChartLegendProps { + + /** + * When set to `false`, the legend is hidden from view, allowing more space for the chart area. + * + * @default true + */ + visible?: boolean; + + /** + * Specifies the height of the legend area in pixels. + * + * @default null + */ + height?: string; + + /** + * Specifies the width of the legend area in pixels. + * + * @default null + */ + width?: string; + + /** + * Specifies the exact coordinates for positioning the legend when using custom positioning. + * Contains x and y properties to determine the precise location within the chart container. + * + * > The `position` must be set to `Custom` for this to take effect. + * + * @default { x: 0, y: 0 } + */ + location?: ChartLocationProps; + + /** + * Determines where the legend appears in relation to the chart area. + * Controls both the orientation of legend items and their overall placement. + * + * Available options: + * * `Auto` - Intelligently positions the legend based on available space. + * * `Top` - Positions the legend above the chart with horizontal item layout. + * * `Left` - Positions the legend to the left with vertical item layout. + * * `Bottom` - Positions the legend below the chart with horizontal item layout. + * * `Right` - Positions the legend to the right with vertical item layout. + * * `Custom` - Positions according to the coordinates specified in the `location` property. + * + * @default 'Auto' + */ + position?: LegendPosition; + + /** + * Sets the internal padding between the legend border and its content elements. + * + * @default 8 + */ + padding?: number; + + /** + * Controls the horizontal and vertical spacing between adjacent legend items. + * + * @default null + */ + itemPadding?: number; + + /** + * Specifies the alignment of the legend within its container region in the chart. + * The behavior of this property depends on the legend's position: + * + * - For horizontal legend positions (`Top`, `Bottom`, `Auto`): + * - `Left`: Aligns the legend to the start (left) of the chart container. + * - `Center`: Aligns the legend to the center of the chart container. + * - `Right`: Aligns the legend to the end (right) of the chart container. + * + * - For vertical legend positions (`Left`, `Right`): + * - `Top`: Aligns the legend to the top of the chart container. + * - `Center`: Aligns the legend to the vertical center of the chart container. + * - `Bottom`: Aligns the legend to the bottom of the chart container. + * + * If an invalid alignment is provided for the current layout direction, + * the legend defaults to `Center` alignment. + * + * @default 'Center' + */ + align?: HorizontalAlignment | VerticalAlignment; + + /** + * Customizes the appearance of text in legend items. + * + * @default { fontStyle: 'Normal', fontSize: '12px', fontWeight: 'Normal', color: '', fontFamily: '' } + */ + textStyle?: ChartFontProps; + + /** + * Controls the height of the visual indicator symbol for each legend item. + * + * @default 10 + */ + shapeHeight?: number; + + /** + * Controls the width of the visual indicator symbol for each legend item. + * + * @default 10 + */ + shapeWidth?: number; + + /** + * Customizes the border around the entire legend area. + * Controls color, width, and dash pattern of the legend's outer frame. + * + * @default { width: 1, color: '', dashArray: '' } + */ + border?: ChartBorderProps; + + /** + * Sets the external spacing around the legend, controlling its distance from other chart elements. + * + * @default { left: 0, right: 0, top: 0, bottom: 0 } + */ + margin?: ChartMarginProps; + + /** + * Sets the internal spacing between the legend border and its content. + * Creates visual breathing room within the legend container for better readability. + * + * @default { left: 0, right: 0, top: 0, bottom: 0 } + */ + containerPadding?: ChartPaddingProps; + + /** + * Controls the space between each legend item's shape and its text. + * + * @default 8 + */ + shapePadding?: number; + + /** + * Sets the background color for the legend area. + * Can use any valid CSS color format including hex, rgb, or named colors. + * + * @default 'transparent' + */ + background?: string; + + /** + * Controls the transparency of the entire legend. + * Values range from 0 (completely transparent) to 1 (fully opaque). + * + * @default 1 + */ + opacity?: number; + + /** + * Controls whether clicking a legend item toggles the visibility of its corresponding series. + * When disabled, legend items become informational only without interactive behavior. + * + * @default true + */ + toggleVisibility?: boolean; + + /** + * Specifies a title to be displayed for the legend. + * The title provides a descriptive heading for the legend items. + * + * @default null + */ + title?: string; + + /** + * Customizes the appearance of the legend title text. + * + * @default { fontStyle: 'Normal', fontSize: '12px', fontWeight: 'Normal', color: '', fontFamily: '' } + */ + titleStyle?: ChartFontProps; + + /** + * Determines the alignment of the title within its container. + * + * Available options: + * - `Left`: Aligns the title to the left. + * - `Center`: Aligns the title to the center. + * - `Right`: Aligns the title to the right. + * + * @default 'Center' + */ + titleAlign?: HorizontalAlignment; + + /** + * Controls how the title behaves when it overflows its container. + * + * Available options: + * - `Wrap`: Wraps the title to the next line. + * - `Trim`: Trims the overflowed title. + * - `None`: Displays the title even if it overlaps other elements. + * + * @default 'Wrap' + */ + titleOverflow?: TextOverflow; + + /** + * Limits the maximum width of the legend title in pixels. + * Text exceeding this width will wrap according to the textWrap property. + * + * @default 100 + */ + maxTitleWidth?: number; + + /** + * Limits the maximum width of individual legend item labels in pixels. + * Prevents long text from extending beyond the desired width. + * + * @default null + */ + maxLabelWidth?: number; + + /** + * When set to `true`, navigation controls (such as arrows or pagination indicators) are shown in the legend, + * allowing users to view additional legend items that do not fit within the visible area. + * + * @default true + */ + enablePages?: boolean; + + /** + * When enabled, reverses the order of elements within each legend item. + * Places text before the shape/symbol instead of the default shape-then-text order. + * + * @default false + */ + inversed?: boolean; + + /** + * When set to `true`, the legend items are shown in reverse sequence— the last item in the data appears first, and the first item appears last. + * + * @default false + */ + reverse?: boolean; + + /** + * When enabled, forces all legend items to have the same width. + * Creates a more uniform, grid-like appearance for the legend. + * + * @default false + */ + fixedWidth?: boolean; + + /** + * Provides options to enhance accessibility for screen readers and keyboard navigation. + * + * @default { ariaLabel: null, focusable: true, role: null, tabIndex: 0 } + */ + accessibility?: ChartAccessibilityProps; +} + +/** + * Provides event arguments triggered when a legend item is clicked in the chart. + */ +export interface LegendClickEvent { + + /** + * The shape of the visual marker used in the clicked legend item. + */ + shape: LegendShape; + + /** + * The name of the chart series associated with the clicked legend item. + */ + seriesName: string; + + /** + * The label text displayed in the clicked legend item. + */ + text: string; + + /** + * Indicates whether the event should be canceled. + * Set to `true` to prevent the default action. + */ + cancel: boolean; +} + +/** + * Configures the properties for customizing strip lines on a chart axis. + * Strip lines are used to highlight specific vertical or horizontal ranges in the plot area, + * making it easier to visualize key thresholds, targets, or events. + */ +export interface ChartStripLineProps { + + /** + * Enables the visibility of strip lines on the chart. + * When set to `true`, the strip lines will be rendered on the axis. + * + * @default false + */ + visible?: boolean; + + /** + * Specifies the range and dimensions of the strip line on the axis. This object defines + * where the strip line begins, its width or height, and how its size is calculated, + * forming the core of the highlighted region. + * + * @default { shouldStartFromAxis: false, start: null, end: null, size: null, sizeType: 'Auto' } + */ + range?: StripLineRangeProps; + + /** + * Customizes the visual appearance of the strip line, including its background and border. + * You can set properties like color, opacity, background images, and dash patterns + * to make the strip line stand out or blend in with the chart's design. + * + * @default { color: '#808080', opacity: 1, dashArray: '', imageUrl: '', border: { color: '', width: 1, dashArray: '' }, zIndex: 'Behind' } + */ + style?: StripLineStyleProps; + + /** + * Configures the text displayed within the strip line. This allows you to add descriptive + * labels or annotations directly on the highlighted range, with options to control text + * content, styling, rotation, and alignment. + * + * @default { content: '', style: { color: '', fontFamily: '', fontSize: '', fontStyle: '', fontWeight: '', opacity: 1 }, rotation: null, hAlign: 'Center', vAlign: 'Center' } + */ + text?: StripLineTextProps; + + /** + * Configures repeating strip lines that recur at a regular interval. This is useful for + * highlighting patterns, such as weekends on a date-time axis or alternating bands + * for every 'n' units on a numeric axis. + * + * @default { enable: false, every: null, until: null } + */ + repeat?: StripLineRepeatProps; + + /** + * Configures a segmented strip line that is rendered only within the specified range of + * another axis. This enables the creation of conditional highlights that are visible only + * when a data point falls within a specific range on two different axes. + * + * @default { enable: false, start: null, end: null, axisName: null } + */ + segment?: StripLineSegmentProps; +} + +/** + * Defines the axis range and dimensions for a strip line. This determines the position, + * length, and scaling of the highlighted area on the chart's plot area. + */ +export interface StripLineRangeProps { + + /** + * Specifies the starting value of the strip line on the axis. + * This can be a numeric, date-time, or category value that marks the beginning + * of the highlighted range. This property is ignored if `size` is set. + * + * @default null + */ + start?: string | number | Date; + + /** + * Specifies the ending value of the strip line on the axis. When both `start` and `end` + * are provided, the strip line will cover the exact range between them. + * This property is ignored if `size` is set. + * + * @default null + */ + end?: string | number | Date; + + /** + * Determines the width or height of the strip line, calculated from the `start` value. + * This offers a flexible way to define the strip line's length without specifying + * an explicit `end` value. + * + * @default null + */ + size?: number; + + /** + * Specifies how the `size` property is interpreted, either in pixel values or axis units. + * When set to 'Pixel', the strip line has a fixed size regardless of zoom level. + * 'Auto' (or axis units) makes the strip line scale with the axis. + * + * @default 'Auto' + */ + sizeType?: StripLineSizeUnit; + + /** + * Determines whether the strip line should originate from the axis origin (zero). + * If `true`, the strip line will render from the baseline of the axis up to the `end` value, + * which is ideal for highlighting ranges from the start of the plot area. + * + * @default false + */ + shouldStartFromAxis?: boolean; +} + +/** + * Configures the visual styling of the strip line. This allows for customization of + * colors, borders, and layering to ensure the strip line is both informative + * and aesthetically pleasing within the chart's design. + */ +export interface StripLineStyleProps { + + /** + * Sets the background color of the strip line. This color fills the entire area + * defined by the strip line's range, making it visually distinct from the + * rest of the plot area. + * + * @default '#808080' + */ + color?: string; + + /** + * Sets the opacity of the strip line, ranging from 0 (fully transparent) to 1 (fully opaque). + * This is useful for creating subtle highlights that do not obscure the chart's + * grid lines or data points behind them. + * + * @default 1 + */ + opacity?: number; + + /** + * Defines a dash pattern for the strip line's border, creating a dashed or dotted line effect. + * The pattern is specified as a string of comma-separated numbers (e.g., "10,5"), + * representing the length of the dash followed by the length of the gap. + * + * @default '' + */ + dashArray?: string; + + /** + * Specifies a URL for a background image to be displayed within the strip line. + * This image will be clipped to the boundaries of the strip line and can be used + * to create textured or patterned highlights. + * + * @default '' + */ + imageUrl?: string; + + /** + * Configures the border properties of the strip line, such as its color, width, and dash pattern. + * A border can help visually separate the strip line from adjacent elements + * or the chart's plot area. + * + * @default { color: '', width: 1, dashArray: '' } + */ + border?: ChartBorderProps; + + /** + * Controls the rendering order of the strip line relative to the chart series. + * Use 'Behind' to draw it behind the series or 'Over' to draw it in front. + * + * @default 'Behind' + */ + zIndex?: ZIndex; +} + +/** + * Configures the text displayed inside a strip line, allowing for annotations and labels. + * This provides control over the text's content, appearance, and positioning, making it + * possible to add meaningful context directly to the highlighted range. + */ +export interface StripLineTextProps { + + /** + * Specifies the text content to be displayed within the strip line. This can be a simple + * label, a value, or any descriptive string that adds information to the highlighted area. + * + * @default '' + */ + content?: string; + + /** + * Defines the font and color styling for the strip line text. This includes properties + * like font family, size, weight, and color, allowing the text to match the chart's + * overall aesthetic or to be emphasized. + * + * @default { color: '', fontFamily: '', fontSize: '', fontStyle: '', fontWeight: '', opacity: 1 } + */ + font?: ChartFontProps; + + /** + * Sets the rotation angle of the text in degrees from its anchor point. + * This is useful for fitting longer text into narrow strip lines or for creating + * stylistic effects. + * + * @default null + */ + rotation?: number; + + /** + * Controls the horizontal alignment of the text within the strip line. + * Options include 'Left', 'Center', and 'Right', determining whether the text is + * positioned at the beginning, middle, or end of the strip line's horizontal space. + * + * @default 'Center' + */ + hAlign?: HorizontalAlignment; + + /** + * Controls the vertical alignment of the text within the strip line. + * Options include 'Top', 'Center', and 'Bottom', positioning the text at the top, + * middle, or bottom of the strip line's vertical space. + * + * @default 'Center' + */ + vAlign?: VerticalAlignment; +} + +/** + * Configures the properties for creating repeating strip lines, which are used to + * highlight recurring intervals across an axis. This is ideal for visualizing patterns + * like weekly cycles, alternating colored bands, or periodic thresholds. + */ +export interface StripLineRepeatProps { + + /** + * Enables or disables the repeating strip line feature. When `true`, the strip line + * will be redrawn at the specified interval across the axis range. + * + * @default false + */ + enable?: boolean; + + /** + * Defines the interval at which the strip line should repeat. This value determines + * the gap between the start of one strip line and the start of the next, + * creating a regular, predictable pattern. + * + * @default null + */ + every?: string | number | Date; + + /** + * Specifies the maximum axis value at which the repetitions should stop. If not provided, + * the strip lines will continue to repeat until the end of the visible axis range. + * + * @default null + */ + until?: string | number | Date; +} + +/** + * Configures a segmented strip line that is rendered only when the data falls within + * the specified range of another axis. This allows for creating conditional highlights + * that depend on values from two different axes. + */ +export interface StripLineSegmentProps { + + /** + * Enables or disables the segmented strip line feature. When `true`, the strip line's + * visibility will be controlled by the range specified on a different axis. + * + * @default false + */ + enable?: boolean; + + /** + * Specifies the name of the axis that will define the visible range of the strip line. + * The strip line will only appear in the sections where the data aligns + * with the `start` and `end` values of this target axis. + * + * @default null + */ + axisName?: string; + + /** + * Defines the starting value for the segment on the axis specified by `axisName`. + * This marks the beginning of the range on the controlling axis where the + * strip line becomes visible. + * + * @default null + */ + start?: string | number | Date; + + /** + * Defines the ending value for the segment on the axis specified by `axisName`. + * This marks the end of the range on the controlling axis, beyond which the + * strip line will no longer be visible. + * + * @default null + */ + end?: string | number | Date; +} + +/** + * Defines the visual appearance settings for an axis line in the chart. + */ +export interface AxisLine { + + /** + * Specifies the thickness of the axis line in pixels. + * + * @default 1 + */ + width?: number; + + /** + * Defines the dash pattern used to render the axis line. + * Accepts a string of comma-separated numbers (e.g., `'2,2'`) to create dashed lines. + * + * @default '' + */ + dashArray?: string; + + /** + * Specifies the color of the axis line. + * Accepts any valid CSS color string. + * + * @default '' + */ + color?: string; +} + +/** + * Defines the appearance of the focus outline for interactive UI elements. + */ +export interface FocusOutlineProps { + + /** + * Customizes the focus border color. + * If not specified, the default focus border color is used. + * + * @default null + */ + color?: string; + + /** + * Customizes the focus border width. + * If not specified, the default width is used. + * + * @default 1.5 + */ + width?: number; + + /** + * Customizes the focus border margin. + * If not specified, the default margin is used. + * + * @default 0 + */ + offset?: number; +} + +/** + * Represents a point location in a 2D chart coordinate space. + * + */ +export interface ChartLocationProps { + + /** + * The horizontal position (x-coordinate) of the location in chart space. + * + * @default 0 + */ + x: number; + + /** + * The vertical position (y-coordinate) of the location in chart space. + * + * @default 0 + */ + y: number; +} + +/** + * Defines optional padding values around the chart content. + * + * Padding creates spacing between the chart's rendering area and its container + * or surrounding elements, helping to control layout and visual balance. + */ +export interface ChartPaddingProps { + /** + * Padding on the left side of the chart, in pixels. + * + * @default 0 + */ + left?: number; + + /** + * Padding on the right side of the chart, in pixels. + * + * @default 0 + */ + right?: number; + + /** + * Padding on the top side of the chart, in pixels. + * + * @default 0 + */ + top?: number; + + /** + * Padding on the bottom side of the chart, in pixels. + * + * @default 0 + */ + bottom?: number; +} + +/** + * Represents a function that customizes the content of a tooltip. + * + * @param {string | string[]} text - The input string used to generate the tooltip content. + * @returns {string | string[] | boolean} A string representing the modified tooltip content. + * @private + */ +export type TooltipContentFunction = (text: string | string[]) => string | string[] | boolean; + diff --git a/components/charts/src/chart/chart-area/ChartArea.tsx b/components/charts/src/chart/chart-area/ChartArea.tsx new file mode 100644 index 0000000..c51e18c --- /dev/null +++ b/components/charts/src/chart/chart-area/ChartArea.tsx @@ -0,0 +1,38 @@ +import { ChartAreaProps } from '../base/interfaces'; +import { useContext, useEffect } from 'react'; +import { ChartContext } from '../layout/ChartProvider'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartProviderChildProps } from './chart-interfaces'; + +/** + * ChartArea component for configuring the area within the chart where the series are rendered. + * + * @param {ChartAreaProps} props - Props for configuring the chart area. + * @returns {null} This component does not render any visible output. + */ +export const ChartArea: React.FC = (props: ChartAreaProps) => { + const context: ChartProviderChildProps = useContext(ChartContext); + + /** + * Update the chart area configuration when relevant props change. + * Merges the provided props with default configuration values, with proper handling of nested objects. + */ + useEffect(() => { + context?.setChartArea( + { + ...defaultChartConfigs.ChartArea, + ...props, + border: { + ...defaultChartConfigs.ChartArea.border, + ...props.border + }, + margin: { + ...defaultChartConfigs.ChartArea.margin, + ...props.margin + } + } + ); + }, [props.width, props.border?.width, props.border?.color, + props.border?.dashArray, props.background, props.backgroundImage, props.margin, props.opacity]); + return null; // This is a configuration component with no UI representation. +}; diff --git a/components/charts/src/chart/chart-area/ChartStackLabels.tsx b/components/charts/src/chart/chart-area/ChartStackLabels.tsx new file mode 100644 index 0000000..907d63a --- /dev/null +++ b/components/charts/src/chart/chart-area/ChartStackLabels.tsx @@ -0,0 +1,42 @@ +import { ChartContext } from '../layout/ChartProvider'; +import { useContext, useEffect } from 'react'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartStackLabelsProps } from '../base/interfaces'; +import { ChartProviderChildProps } from './chart-interfaces'; + +/** + * ChartStackLabels component for configuring stack labels in the chart. + * Stack labels are used to display the total value of stacked series. + * This is a configuration-only component and does not render any visual output. + * + * @param {ChartStackLabelsProps} props - Properties used to customize stack label appearance and behavior. + * @returns {null} This component does not render any visible output. + */ +export const ChartStackLabels: React.FC = (props: ChartStackLabelsProps) => { + const context: ChartProviderChildProps = useContext(ChartContext); + useEffect(() => { + context?.setChartStackLabels({ ...defaultChartConfigs.ChartStackLabels, ...props }); + }, [ + props.visible, + props.fill, + props.format, + props.rotationAngle, + props.borderRadius?.x, + props.borderRadius?.y, + props.margin?.left, + props.margin?.right, + props.margin?.top, + props.margin?.bottom, + props.border?.width, + props.border?.color, + props.font?.color, + props.font?.fontSize, + props.font?.fontStyle, + props.font?.fontFamily, + props.font?.fontWeight, + props.align + ]); + return null; +}; + +export default ChartStackLabels; diff --git a/components/charts/src/chart/chart-area/ChartSubTitle.tsx b/components/charts/src/chart/chart-area/ChartSubTitle.tsx new file mode 100644 index 0000000..4070344 --- /dev/null +++ b/components/charts/src/chart/chart-area/ChartSubTitle.tsx @@ -0,0 +1,37 @@ +import { useContext, useEffect } from 'react'; +import { ChartContext } from '../layout/ChartProvider'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartTitleProps } from '../base/interfaces'; +import { ChartProviderChildProps } from './chart-interfaces'; + +/** + * ChartSubtitle component for configuring and setting the subtitle of the chart. + * + * @param {ChartTitleProps} props - Props used to customize the chart subtitle. + * @returns {null} This component does not render any visible output. + */ +export const ChartSubtitle: React.FC = (props: ChartTitleProps) => { + const context: ChartProviderChildProps = useContext(ChartContext); + + /** + * Updates the chart subtitle in the shared chart context whenever relevant props change. + */ + useEffect(() => { + context?.setChartSubTitle({ ...defaultChartConfigs.ChartSubTitle, ...props }); + }, [props.text, + props.color, + props.fontSize, + props.position, + props.opacity, + props.textOverflow, + props.x, + props.y, + props.border?.color, + props.border?.width, + props.background, + props.fontFamily, + props.fontWeight, + props.align + ]); + return null; // This is a configuration component with no UI representation. +}; diff --git a/components/charts/src/chart/chart-area/ChartTitle.tsx b/components/charts/src/chart/chart-area/ChartTitle.tsx new file mode 100644 index 0000000..184fc1f --- /dev/null +++ b/components/charts/src/chart/chart-area/ChartTitle.tsx @@ -0,0 +1,37 @@ +import { ChartContext } from '../layout/ChartProvider'; +import { useContext, useEffect } from 'react'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartTitleProps } from '../base/interfaces'; +import { ChartProviderChildProps } from './chart-interfaces'; + +/** + * ChartTitle component for configuring and setting the main title of the chart. + * + * @param {ChartTitleProps} props - Props used to customize the chart title. + * @returns {null} This component does not render any visible output. + */ +export const ChartTitle: React.FC = (props: ChartTitleProps) => { + const context: ChartProviderChildProps = useContext(ChartContext); + + /** + * Updates the chart title in the shared chart context whenever relevant props change. + */ + useEffect(() => { + context?.setChartTitle({ ...defaultChartConfigs.ChartTitle, ...props }); + }, [props.text, + props.color, + props.fontSize, + props.position, + props.opacity, + props.textOverflow, + props.x, + props.y, + props.border?.color, + props.border?.width, + props.background, + props.fontFamily, + props.fontWeight, + props.align + ]); + return null; +}; diff --git a/components/charts/src/chart/chart-area/chart-interfaces.tsx b/components/charts/src/chart/chart-area/chart-interfaces.tsx new file mode 100644 index 0000000..84d24a5 --- /dev/null +++ b/components/charts/src/chart/chart-area/chart-interfaces.tsx @@ -0,0 +1,3532 @@ +import { JSX, ReactElement } from 'react'; +import { ChartSeriesType, ChartMarkerShape, Theme, IntervalType, LegendShape, Orientation, TitlePosition, StripLineSizeUnit, ZIndex } from '../base/enum'; +import { ChartBorderProps, ChartAreaProps, ChartComponentProps, ChartStackLabelsProps, ChartFontProps, ZoomEndEvent, MajorGridLines, MajorTickLines, ChartMarkerProps, MinorGridLines, MinorTickLines, TitleSettings, ChartTooltipProps, ChartSeriesProps, ChartZoomSettingsProps, ChartAxisProps, ChartStripLineProps, ChartTitleProps, ChartLegendProps, Column, Row, ChartDataLabelProps, ChartLocationProps, CornerRadius } from '../base/interfaces'; +import { BaseLegend } from '../base/Legend-base'; +import { Animation } from '../common/base'; +import { DataLabelRendererResults } from '../renderer/SeriesRenderer/DataLabelRender'; +import { IThemeStyle } from '../utils/theme'; +import { ChartAxisLabelProps, ChartAxisTitleProps } from '../chart-axis/base'; +import AreaSeriesRenderer from '../renderer/SeriesRenderer/AreaSeriesRenderer'; +import StackingColumnSeriesRenderer from '../renderer/SeriesRenderer/StackingColumnSeriesRenderer'; +import StackingBarSeriesRenderer from '../renderer/SeriesRenderer/StackingBarSeriesRenderer'; +import ScatterSeriesRenderer from '../renderer/SeriesRenderer/ScatterSeriesRenderer'; +import BubbleSeriesRenderer from '../renderer/SeriesRenderer/BubbleSeriesRenderer'; +import SplineAreaSeriesRenderer from '../renderer/SeriesRenderer/SplineAreaSeriesRenderer'; +import BarSeries from '../renderer/SeriesRenderer/BarSeriesRenderer'; +import SplineSeriesRenderer from '../renderer/SeriesRenderer/SplineSeriesRenderer'; +import StepLineSeriesRenderer from '../renderer/SeriesRenderer/StepLineSeriesRenderer'; +import ColumnSeries from '../renderer/SeriesRenderer/ColumnSeriesRenderer'; +import LineSeriesRenderer from '../renderer/SeriesRenderer/lineSeriesRenderer'; +import { useData } from '../common/data'; +import { StackValuesType } from '../utils/helper'; +import { TooltipRefHandle } from '@syncfusion/react-svg-tooltip'; +import { VerticalAlignment, HorizontalAlignment } from '@syncfusion/react-base'; + +/** + * Represents a two-dimensional size with width and height. + * + */ +export interface ChartSizeProps { + /** + * Defines the height of the element in pixels. + */ + width: number; + + /** + * Defines the width of the element in pixels. + */ + height: number; +} + +/** + * Represents an RGB color value. + * + * @private + */ +export interface ColorValue { + /** + * Red component value (0-255) + */ + r: number, + + /** + * Green component value (0-255) + */ + g: number, + + /** + * Blue component value (0-255) + */ + b: number +} + +/** + * Represents the result of a data management operation. + * + * @private + */ +export interface DataManagerResult { + /** + * The resulting data object + */ + result: Object; + + /** + * The count of items in the result + */ + count: number; +} + +/** + * Represents a rectangle defined by its position and dimensions. + * + * @private + */ +export type Rect = { + /** + * The x-coordinate of the rectangle's top-left corner. + */ + x: number; + + /** + * The y-coordinate of the rectangle's top-left corner. + */ + y: number; + + /** + * The height of the rectangle. + */ + height: number; + + /** + * The width of the rectangle. + */ + width: number; +} + +/** + * Defines the margin settings around the chart area. + * + * @private + */ +export interface MarginModel { + + /** + * The left margin of the chart, specified in pixels. + * + * @default 10 + */ + left: number; + + /** + * The right margin of the chart, specified in pixels. + * + * @default 10 + */ + right: number; + + /** + * The top margin of the chart, specified in pixels. + * + * @default 10 + */ + top: number; + + /** + * The bottom margin of the chart, specified in pixels. + * + * @default 10 + */ + bottom: number; +} + +/** + * Represents the main configuration for the chart. + * + * @private + */ +export interface Chart { + /** + * The initial clipping rectangle for the chart + */ + initialClipRect: Rect; + + /** + * Collection of rectangles for data labels + */ + dataLabelCollections: Rect[]; + + /** + * Internationalization object for the chart + */ + intl: Object; + + /** + * Animation settings for the chart + */ + animated: Animation; + + /** + * Determines whether tooltip tracking is disabled + */ + disableTrackTooltip: boolean; + + /** + * Indicates whether chart movement has started + */ + startMove: boolean; + + /** + * Indicates whether redraw operations should be delayed + */ + delayRedraw: boolean; + + /** + * Indicates whether the chart is being used on a touch device + */ + isTouch: boolean; + + /** + * Indicates whether a double tap has occurred + */ + isDoubleTap: boolean; + + /** + * Indicates whether panning has started + */ + startPanning: boolean; + + /** + * Indicates whether UI operations have been performed + */ + performedUI: boolean; + + /** + * Rectangle used for zooming operations + */ + zoomingRect: Rect; + + /** + * Indicates whether the chart is currently zoomed + */ + isZoomed: boolean; + + /** + * Indicates whether the chart is currently being zoomed via mouse wheel or pinch gesture + */ + isGestureZooming: boolean; + + /** + * Y-coordinate of mouse down event + */ + mouseDownY: number; + + /** + * X-coordinate of mouse down event + */ + mouseDownX: number; + + /** + * Indicates whether chart dragging is in progress + */ + isChartDrag: boolean; + + /** + * Indicates whether points have been removed + */ + pointsRemoved: boolean; + + /** + * Settings for zoom functionality + */ + zoomSettings: ChartZoomSettingsProps; + + /** + * Threshold value for various operations + */ + threshold: number; + + /** + * Previously recorded mouse move X-coordinate + */ + previousMouseMoveX: number; + + /** + * Previously recorded mouse move Y-coordinate + */ + previousMouseMoveY: number; + + /** + * Collections of rotated data label locations + */ + rotatedDataLabelCollections: ChartLocationProps[][] | null; + + /** + * A React state dispatcher to update the series rendering options. + */ + setSeriesOptions?: React.Dispatch>; + + /** + * Contains the visual representation options for each series in the chart. + */ + seriesOptions?: RenderOptions[]; + + /** + * Contains the configuration for data point markers across different series. + */ + markerOptions?: ChartMarkerProps[]; + + /** + * Contains formatting and positioning options for data labels displayed with series points. + */ + dataLabelOptions?: DataLabelRendererResult[]; + + /** + * Contains rendering details for labels displaying cumulative values at the top of stacked series. + */ + stackLabelsOptions: DataLabelRendererResults[]; + + /** + * Count of the mouse click + */ + clickCount: number; + + /** + * Reference to the chart instance used for zoom-based redraw operations. + */ + zoomRedraw: boolean; + + /** + * Animation duration for chart transitions, in milliseconds. + */ + duration: number; + + /** Height of the chart. */ + height: number; + + /** Width of the chart. */ + width: number; + + /** The HTML element in which the chart is rendered. */ + element: HTMLElement; + + /** Margin around the chart. */ + margin: { top: number; right: number; bottom: number; left: number }; + + /** Border settings for the chart. */ + border: { color: string; width: number; dashArray: string }; + + /** Rectangle bounds of the chart area. */ + rect: { x: number; y: number; width: number; height: number }; + + /** Background color of the chart. */ + background: string; + + /** Background image of the chart. */ + backgroundImage: string; + + /** Available size for rendering the chart. */ + availableSize: { height: number; width: number }; + + /** Clipping rectangle for the chart area. */ + clipRect: Rect; + + /** Enables right-to-left rendering. */ + enableRtl: boolean; + + /** Locale used in the chart. */ + locale: string; + + /** Theme applied to the chart. */ + theme: Theme; + + /** Style settings based on the theme. */ + themeStyle: IThemeStyle; + + /** Indicates if the chart is transposed. */ + iSTransPosed: boolean; + + /** Indicates if an inverted axis is required. */ + requireInvertedAxis: boolean; + + /** Collection of all series in the chart. */ + series: ChartSeriesProps[]; + + /** Enables series animation. */ + animateSeries: boolean; + + /** Indicates if RTL rendering is enabled. */ + isRtlEnabled: boolean; + + /** Indicates if the chart is redrawn. */ + redraw: boolean; + + /** Series that are currently visible. */ + visibleSeries: SeriesProperties[]; + + /** Row configurations in the chart. */ + rows: RowProps[]; + + /** Column configurations in the chart. */ + columns: ColumnProps[]; + + /** Collection of horizontal axes. */ + horizontalAxes: AxisModel[]; + + /** Collection of vertical axes. */ + verticalAxes: AxisModel[]; + + /** All axes used in the chart. */ + axisCollection: AxisModel[]; + + /** Enables or disables animation. */ + enableAnimation?: boolean; + + /** Rectangle for the chart area. */ + chartAreaRect: Rect; + + /** Title settings for the chart. */ + titleSettings: TitleOptions; + + /** Subtitle settings for the chart. */ + subTitleSettings: TitleOptions; + + /** Layout values for axis calculations. */ + chartAxislayout: ChartAxisLayout; + + /** Configuration for the chart area. */ + chartArea: ChartAreaProps; + + /** Options for rendering pane lines. */ + paneLineOptions: PathOptions[]; + + /** + * The `selectionMode` property determines how data points or series can be highlighted or selected. + */ + selectionMode: SelectionMode; + + /** Mouse X value of the chart. */ + mouseX: number; + + /** Mouse Y value of the chart. */ + mouseY: number; + + /** + * Configuration options for the secondary axis in the chart. + */ + axes: AxisModel[]; + + /** + * Properties and configuration options passed to the chart component. + * These include data, appearance settings, and behavior customizations. + */ + chartProps: ChartComponentProps; + + /** + * This property controls whether columns for different series appear next to each other in a column chart. + * + */ + enableSideBySidePlacement?: boolean; + + /** + * Configuration options for stack labels in the chart. + * Stack labels display the total value for stacked series, including customization options. + * for appearance and positioning, and other visual elements to enhance chart readability. + * This property allows users to modify how stack labels are rendered in a stacked chart. + */ + stackLabels?: ChartStackLabelsProps; + + /** + * Index of the currently selected legend item. + */ + currentLegendIndex: number; + + /** + * ID of the previously interacted chart element. + */ + previousTargetId: string; + + /** + * Index of the currently selected series in the chart. + */ + currentSeriesIndex: number; + + /** + * Index of the currently selected data point in the chart. + */ + currentPointIndex: number; + + /** + * Indicates whether a legend item was clicked. + */ + isLegendClicked: boolean; + + /** + * Target width or dimension to which the chart should resize. + */ + resizeTo: number; + + /** + * Triggers a re-measurement and re-rendering of the chart + */ + triggerRemeasure?: () => void; + + /** + * Reference to the tooltip component for managing tooltip functionality. + */ + tooltipRef: React.RefObject; + + /** + * Reference to the tooltip component for managing tooltip functionality. + */ + trackballRef: React.RefObject + + /** + * Module that handles tooltip creation and management. + */ + tooltipModule: ChartTooltipProps; + + /** + * Configuration options for the stripline behind in the chart. + */ + striplineBehind: StriplineOptions[]; + + /** + * Configuration options for the stripline over in the chart. + */ + striplineOver: StriplineOptions[]; + + /** + * Triggers the rendering of chart axes. + */ + axisRender: (isLegendClicked?: boolean) => void; + + /** + * Defines a set of colors used for rendering chart series. + * Each color in the array is applied sequentially to the series. + * + * @default [] + */ + palettes?: string[]; +} + + +/** + * Represents the layout measurements and clipping rectangles for chart axes and series. + * + * @private + */ +export interface ChartAxisLayout { + /** + * The initial clipping rectangle that defines the drawable chart area before axis adjustments. + */ + initialClipRect: Rect; + + /** + * The size occupied by the left axis area (in pixels). + */ + leftSize: number; + + /** + * The size occupied by the right axis area (in pixels). + */ + rightSize: number; + + /** + * The size occupied by the top axis area (in pixels). + */ + topSize: number; + + /** + * The size occupied by the bottom axis area (in pixels). + */ + bottomSize: number; + + /** + * The clipping rectangle applied to the series area, adjusted for axes layout. + */ + seriesClipRect: Rect; +} + +/** + * Represents the layout state of the chart, including elements like title, subtitle, legend, and visible series. + * + * @private + */ +export interface LayoutState { + /** + * The root DOM element where the chart is rendered. + */ + element: Element; + + /** + * Indicates whether the chart series should be animated during rendering. + */ + animateSeries: boolean; + + /** + * The chart instance containing configuration and state. + */ + chart?: Chart; + + /** + * Configuration for the chart's main title, including text, style, size, and rendering options. + */ + chartTitle?: { + /** + * The text content of the chart title. + */ + title: string; + + /** + * The style settings applied to the chart title. + */ + titleStyle: TitleSettings; + + /** + * The size of the chart title element. + */ + titleSize: ChartSizeProps; + + /** + * Additional rendering options for the chart title. + */ + titleOptions: TitleOptions; + }; + + /** + * Configuration for the chart's subtitle, including text, style, and size. + */ + chartSubTitle?: { + /** + * The text content of the chart subtitle. + */ + title: string; + + /** + * The style settings applied to the chart subtitle. + */ + titleStyle: TitleSettings; + + /** + * The size of the chart subtitle element. + */ + titleSize: ChartSizeProps; + }; + + /** + * The legend configuration and rendering logic for the chart. + */ + ChartLegend?: BaseLegend; + + /** + * The list of series that are currently visible in the chart. + */ + visibleSeries?: SeriesProperties[]; +} + +/** + * Represents the base configuration for zoom functionality. + * + * @private + * @extends ZoomSettings + */ +export interface BaseZoom extends ChartZoomSettingsProps { + /** + * The rectangle area where zooming is performed. + */ + zoomingRect?: Rect; + + /** + * Indicates whether the chart is currently zoomed. + */ + isZoomed?: boolean; + + /** + * Indicates whether panning is in progress. + */ + isPanning?: boolean; + + /** + * Indicates whether UI operations have been performed. + */ + performedUI?: boolean; + + /** + * Indicates whether panning has started. + */ + startPanning?: boolean; + + /** + * Array of zoom axis ranges. + */ + zoomAxes?: IZoomAxisRange[]; + + /** + * List of touch points at the start of interaction. + */ + touchStartList?: ITouches[] | TouchList; + + /** + * Collection of zoom complete event arguments. + */ + zoomCompleteEvtCollection: ZoomEndEvent[]; + + /** + * List of touch points during movement. + */ + touchMoveList?: ITouches[] | TouchList; + + /** + * Element that is the target of pinch actions. + */ + pinchTarget?: Element; + + /** + * Rectangle defining the offset for zoom operations. + */ + offset?: Rect; +} + +/** + * Represents touch interaction data. + * + * @private + */ +export interface ITouches { + /** + * X coordinate of the touch point relative to the page. + */ + pageX?: number; + + /** + * Y coordinate of the touch point relative to the page. + */ + pageY?: number; + + /** + * Unique identifier for the touch point. + */ + pointerId?: number; +} + +/** + * Represents the range values for zoom axis. + * + * @private + */ +export interface IZoomAxisRange { + /** + * Actual minimum value of the axis. + */ + actualMin?: number; + + /** + * Actual delta value of the axis. + */ + actualDelta?: number; + + /** + * Minimum value of the axis. + */ + min?: number; + + /** + * Delta value of the axis. + */ + delta?: number; +} + +/** + * Represents a numerical range with additional computed properties. + * + * @private + */ +export interface DoubleRangeType { + /** + * The starting value of the range. + */ + start: number; + + /** + * The ending value of the range. + */ + end: number; + + /** + * The difference between the end and start values. + */ + delta: number; + + /** + * The midpoint value of the range. + */ + median: number; +} + +/** + * Represents a label that is visible on an axis, including its text, style, size, and position. + * + * @private + */ +export interface VisibleLabel { + /** + * The displayed text of the label. It can be a single string or multiple lines as an array of strings. + */ + text: string | string[]; + + /** + * The numeric value associated with the label on the axis. + */ + value: number; + + /** + * The font styling applied to the label text. + */ + labelStyle: ChartFontProps; + + /** + * The rendered size (width and height) of the label text. + */ + size: ChartSizeProps; + + /** + * The size of the label when it is broken into multiple lines (wrapped). + */ + breakLabelSize: ChartSizeProps; + + /** + * The index position of the label in the labels collection. + */ + index: number; + + /** + * The original, unmodified text of the label before any formatting or truncation. + */ + originalText: string; +} + + +/** + * Extends the base Axis interface with additional properties for internal calculations, + * rendering, and configuration specific to chart axes. + * + * @private + */ +export interface AxisModel extends ChartAxisProps { + /** + * The orientation of the axis (e.g., 'Horizontal' or 'Vertical'). + */ + orientation?: Orientation; + + /** + * The collection of series associated with this axis. + */ + series: SeriesProperties[]; + + /** + * The array of label strings displayed on the axis. + */ + labels: string[]; + + /** + * Object representing index labels (optional). + */ + indexLabels?: {}; + + /** + * Specifies whether Right-To-Left (RTL) layout is enabled for this axis. + */ + isRTLEnabled?: boolean; + + /** + * Specifies whether the axis is positioned on the opposed side of the chart. + */ + isAxisOpposedPosition: boolean; + + /** + * Indicates whether the axis direction is inverted. + */ + isAxisInverse: boolean; + + /** + * Internal flag indicating the visibility of the axis. + */ + internalVisibility: boolean; + + /** + * Optional React element module related to the axis (used in React implementations). + */ + baseModule?: ReactElement; + + /** + * Numerical range information including start, end, delta, and median values. + */ + doubleRange: DoubleRangeType; + + /** + * The actual visible range of the axis, including min, max, interval, and delta. + */ + actualRange: VisibleRangeProps; + + /** + * Array of interval divisions used for axis calculations. + */ + intervalDivs: number[]; + + /** + * The currently visible range on the axis. + */ + visibleRange: VisibleRangeProps; + + /** + * Collection of visible labels on the axis. + */ + visibleLabels: VisibleLabel[]; + + /** + * A function to format axis label values. + */ + format: Function; + + /** + * The first label on the axis. + */ + startLabel: string; + + /** + * The last label on the axis. + */ + endLabel: string; + + /** + * The maximum rendered size of the axis labels. + */ + maxLabelSize: ChartSizeProps; + + /** + * The rectangular area representing the axis layout. + */ + rect: Rect; + + /** + * The updated rectangular area after layout adjustments. + */ + updatedRect: Rect; + + /** + * Configuration options for the axis line rendering. + */ + axisLineOptions: PathOptions; + + /** + * Configuration options for axis labels as SVG text elements. + */ + axislabelOptions: TextOption[]; + + /** + * Major grid line settings. + */ + majorGridLines: MajorGridLines; + + /** + * Minor grid line settings. + */ + minorGridLines: MinorGridLines; + + /** + * Minor tick line settings. + */ + minorTickLines: MinorTickLines; + + /** + * Major tick line settings. + */ + majorTickLines: MajorTickLines; + + /** + * Collection of path options for rendering major grid lines. + */ + axisMajorGridLineOptions: PathOptions[]; + + /** + * Collection of path options for rendering minor grid lines. + */ + axisMinorGridLineOptions: PathOptions[]; + + /** + * Collection of path options for rendering major tick lines. + */ + axisMajorTickLineOptions: PathOptions[]; + + /** + * Collection of path options for rendering minor tick lines. + */ + axisMinorTickLineOptions: PathOptions[]; + + /** + * Collection of text options for axis titles. + */ + axisTitleOptions: TextOption[]; + + /** + * The padding interval applied to the axis. + */ + paddingInterval: number; + + /** + * The maximum length of points on the axis. + */ + maxPointLength: number; + + /** + * Indicates whether the axis is used in a 100% stacked chart. + */ + isStack100: boolean; + + /** + * Collection of title strings for the axis. + */ + titleCollection: string[]; + + /** + * The rendered size of the axis title. + */ + titleSize: ChartSizeProps; + + /** + * Specifies the type of interval for the axis (e.g., years, months, days). + */ + actualIntervalType: IntervalType; + + /** + * Indicates if the interval is intended to be in decimal format. + */ + isIntervalInDecimal: boolean; + + /** + * Defines the interval value for the datetime axis. + */ + dateTimeInterval: number; + + /** + * Collection of path options for axis label borders. + */ + axisLabelBorderOptions: PathOptions[]; + + /** + * The rotation angle applied to axis labels. + */ + angle: number; + + /** + * The rotated label text. + */ + rotatedLabel: string; + + /** + * Reference to the parent chart instance. + */ + chart: Chart; + + /** + * Options for customizing the appearance of the axis title, including font family, size, style, weight, and color. + */ + titleStyle: ChartAxisTitleProps; + + /** + * This property allows defining various font settings to control how the labels are displayed on the axis. + */ + labelStyle: ChartAxisLabelProps; + + /** + * The JSX element representing the label of the axis + */ + labelElement: JSX.Element; + + /** + * The JSX element representing the major tick lines on the axis. + */ + majorTickLineElement: JSX.Element; + + /** + * The JSX element representing the minor tick lines on the axis + */ + minorTickLineElement: JSX.Element; + + /** + * The JSX element representing the border or outline of the axis. + */ + borderElement: JSX.Element; + + /** + * Specifies the collection of strip lines for the axis, which are visual elements used to mark or highlight specific ranges. + */ + stripLines?: ChartStripLineProps[]; +} + +/** + * Defines the properties for the StripLine component, used to wrap and render StripLine elements. + * + * @private + */ +export interface StripLineProps { + /** + * Specifies the child elements (StripLine components) to be rendered inside the StripLine container. + */ + children?: React.ReactNode; +} + +/** + * Interface for a class StriplineOptions. + * + * @private + */ +export interface StriplineOptions { + + /** + * Toggles the visibility of the strip line. + */ + visible?: boolean; + + /** + * Defines the starting and ending range of the strip line on the axis. + */ + range?: StripLineRangeProps; + + /** + * Configures the visual appearance and styling of the strip line. + */ + style?: StripLineStyleProps; + + /** + * Customizes the text displayed within the strip line. + */ + text?: StripLineTextProps; + + /** + * Configures repeating strip lines to render across a regular interval. + */ + repeat?: StripLineRepeatProps; + + /** + * Defines a segmented strip line in the axis. + */ + segment?: StripLineSegmentProps; + + /** + * The rectangular area for the stripline. + */ + rect?: Rect; + + /** + * The stripline settings model. + */ + stripLine?: ChartStripLineProps; + + /** + * Unique identifier for the stripline element. + */ + id?: string; + + /** + * The axis associated with the stripline. + */ + axis?: AxisModel; + + /** + * The index of the axis in the collection. + */ + axisIndex?: number; + + /** + * Reference to the chart instance. + */ + chart?: Chart; + + /** + * The position of the stripline. + */ + position?: ZIndex; + +} + +/** + * Defines the axis range for a strip line. + */ +export interface StripLineRangeProps { + + /** + * Specifies the starting value of the strip line on the axis. + */ + start?: Object | number | Date; + + /** + * Specifies the ending value of the strip line on the axis. + */ + end?: Object | number | Date; + + /** + * Determines the width or height of the strip line, calculated from the `start` value. + */ + size?: number; + + /** + * Specifies how the `size` property is interpreted (e.g., as pixels or axis units). + */ + sizeType?: StripLineSizeUnit; + + /** + * Determines whether the strip line should originate from the axis origin (zero). + */ + shouldStartFromAxis?: boolean; +} + +/** + * Configures the visual styling of the strip line. + */ +export interface StripLineStyleProps { + + /** + * Sets the background color of the strip line. + */ + color?: string; + + /** + * Sets the opacity of the strip line, ranging from 0 (transparent) to 1 (opaque). + */ + opacity?: number; + + /** + * Defines a dash pattern for the strip line's border (e.g., "10,5"). + */ + dashArray?: string; + + /** + * Specifies a URL for a background image to be displayed within the strip line. + */ + imageUrl?: string; + + /** + * Configures the border properties of the strip line, such as color and width. + */ + border?: ChartBorderProps; + + /** + * Controls the rendering order of the strip line relative to the chart series. + * Use 'Behind' to draw it behind the series or 'Over' to draw it in front. + */ + zIndex?: ZIndex; +} + +/** + * Configures the text displayed inside a strip line. + */ +export interface StripLineTextProps { + + /** + * Specifies the text content to be displayed within the strip line. + */ + content?: string; + + /** + * Defines the font and color styling for the strip line text. + */ + style?: ChartFontProps; + + /** + * Sets the rotation angle of the text in degrees. + */ + rotation?: number; + + /** + * Controls the horizontal alignment of the text. + */ + hAlign?: HorizontalAlignment; + + /** + * Controls the vertical alignment of the text. + */ + vAlign?: VerticalAlignment; +} + +/** + * Configures the properties for creating repeating strip lines. + */ +export interface StripLineRepeatProps { + + /** + * Enables or disables the repeating strip line feature. + */ + isEnabled?: boolean; + + /** + * Defines the interval at which the strip line should repeat. + */ + every?: Object | number | Date; + + /** + * Specifies the maximum axis value at which the repetitions should stop. + */ + until?: Object | number | Date; +} + +/** + * Configures a segmented strip line that is rendered only within the specified range + */ +export interface StripLineSegmentProps { + + /** + * Enables or disables the segmented strip line feature. + */ + isEnabled?: boolean; + + /** + * Specifies the name of the axis that will define the visible range of the strip line. + */ + axisName?: string; + + /** + * Defines the starting value for the segment on the axis specified by `axisName`. + */ + start?: Object | number | Date; + + /** + * Defines the ending value for the segment on the axis specified by `axisName`. + */ + end?: Object | number | Date; +} + +/** + * Represents a row in the chart layout containing axis information and computed size metrics. + * + * @private + */ +export interface RowProps { + /** + * The collection of axis models associated with this row. + */ + axes: AxisModel[]; + + /** + * Sizes of elements positioned far from the chart center in this row. + */ + farSizes: number[]; + + /** + * Sizes of elements positioned near the chart center in this row. + */ + nearSizes: number[]; + + /** + * Sizes of elements positioned inside far side of the chart in this row. + */ + insideFarSizes: number[]; + + /** + * Sizes of elements positioned inside near side of the chart in this row. + */ + insideNearSizes: number[]; + + /** + * The computed height of the row in pixels. + */ + computedHeight: number; + + /** + * The computed top offset position of the row relative to the chart container. + */ + computedTop: number; + + /** + * The height of the row as a string accepts input both as '100px' and '100%'. + * If specified as '100%', the row renders to the full height of its chart. + * + * @default '100%' + */ + height: string; + + /** + * Options to customize the border of the rows. + */ + border: ChartBorderProps; +} + +/** + * Represents a column in the chart layout containing axis information and computed size metrics. + * + * @private + */ +export interface ColumnProps { + /** + * The collection of axis models associated with this column. + */ + axes: AxisModel[]; + + /** + * Sizes of elements positioned far from the chart center in this column. + */ + farSizes: number[]; + + /** + * Sizes of elements positioned near the chart center in this column. + */ + nearSizes: number[]; + + /** + * Sizes of elements positioned inside far side of the chart in this column. + */ + insideFarSizes: number[]; + + /** + * Sizes of elements positioned inside near side of the chart in this column. + */ + insideNearSizes: number[]; + + /** + * The computed width of the column in pixels. + */ + computedWidth: number; + + /** + * The computed left offset position of the column relative to the chart container. + */ + computedLeft: number; + + /** + * The height of the row as a string accepts input both as '100px' and '100%'. + * If specified as '100%', the row renders to the full height of its chart. + * + * @default '100%' + */ + width: string; + + /** + * Options to customize the border of the rows. + */ + border: ChartBorderProps; +} + +/** + * Defines the thickness or margin values for all four sides of an element. + * + * @private + */ +export interface Thickness { + /** + * The thickness or margin on the left side, in pixels. + */ + left: number; + + /** + * The thickness or margin on the right side, in pixels. + */ + right: number; + + /** + * The thickness or margin on the top side, in pixels. + */ + top: number; + + /** + * The thickness or margin on the bottom side, in pixels. + */ + bottom: number; +} + + +/** + * Defines the properties used to render a path or line element in SVG, typically for gridlines or custom paths in charts. + * + * @private + */ +export interface PathOptions { + /** + * Specifies the dash pattern for the path, used to create dashed lines. + */ + dashArray?: string | number | undefined; + + /** + * The unique identifier for the SVG path element. + */ + id?: string; + + /** + * The SVG path data (`d` attribute) that defines the shape of the path. + */ + d?: string; + + /** + * The stroke color of the path. + */ + stroke?: string; + + /** + * The width of the stroke used to draw the path, in pixels. + */ + strokeWidth: number; + + /** + * Specifies the pattern of dashes and gaps used to stroke the path. + */ + strokeDasharray?: string; + + /** + * The fill color of the path. + */ + fill?: string; + + /** + * The starting X coordinate for line-based paths. + */ + x1?: number; + + /** + * The ending X coordinate for line-based paths. + */ + x2?: number; + + /** + * The starting Y coordinate for line-based paths. + */ + y1?: number; + + /** + * The ending Y coordinate for line-based paths. + */ + y2?: number; + + /** + * The opacity for line-based paths. + */ + opacity?: number; + + /** + * The X coordinate of the center for elliptical or circular paths. + */ + cx?: number; + + /** + * The Y coordinate of the center for elliptical or circular paths. + */ + cy?: number; + + /** + * The radius on X-axis for elliptical paths. + */ + rx?: number; + + /** + * The radius on Y-axis for elliptical paths. + */ + ry?: number; +} + + +/** + * Defines the properties used to render a text element in SVG, typically for axis labels, titles, or data labels in charts. + * + * @private + */ +export interface TextOption { + /** + * Unique identifier for the text element. + */ + id?: string; + + /** + * Specifies the text anchor alignment. Common values include 'start', 'middle', and 'end'. + */ + anchor?: string; + + /** + * The text content to be rendered. Can be a string or an array of strings (for multi-line text). + */ + text: string | string[]; + + /** + * The transform attribute used to apply SVG transformations like rotation or translation. + */ + transform?: string; + + /** + * The X-coordinate for the text position. + */ + x: number; + + /** + * The Y-coordinate for the text position. + */ + y: number; + + /** + * The rotation angle of the text label in degrees. + */ + labelRotation?: number; + + /** + * Specifies the font family to be used for the text. + */ + fontFamily?: string; + + /** + * Specifies the weight (thickness) of the font. + */ + fontWeight?: string; + + /** + * Specifies the font size (e.g., '12px', '1em'). + */ + fontSize?: string; + + /** + * Specifies the font style (e.g., 'normal', 'italic'). + */ + fontStyle?: string; + + /** + * The opacity level of the text, ranging from 0 (transparent) to 1 (opaque). + */ + opacity?: number; + + /** + * The fill color of the text. + */ + fill?: string; + + /** + * Specifies the baseline alignment for the text (e.g., 'alphabetic', 'middle', 'hanging'). + */ + baseLine?: string; + + /** + * The length of the interval associated with this text label (used in axis rendering). + */ + intervalLength?: number; + + /** + * The width allocated for X position placement, often used in alignment calculations. + */ + XPositionWidth?: number; + + /** + * Indicates whether the text is part of a broken axis label. + */ + isAxisBreakLabel?: boolean; +} + +/** + * Represents the base configuration for Series. + * + * @private + * + * @extends Series + */ +export interface SeriesProperties extends ChartSeriesProps { + isPointRemoved: boolean; + /** + * Indicates whether a data point gets updated in the series. + * + * @default false + */ + pointUpdated: boolean; + /** + * Indicates whether a new data point has been added to the series. + * + * @default false + */ + isPointAdded: boolean; + + /** + * Controls whether marker animations should be skipped. + * + * @default false + */ + skipMarkerAnimation: boolean; + + /** + * Tolerance value for Y-axis calculations to handle minor variations in position. + */ + yTolerance: number; + + /** + * Tolerance value for X-axis calculations to handle minor variations in position. + */ + xTolerance: number; + + /** + * Indicates whether the series properties have changed and require re-rendering. + * Used internally to optimize performance by avoiding unnecessary redraws. + */ + propsChange: boolean; + + /** + * Reference to the chart component properties passed from parent component. + * Contains configuration options that affect series rendering and behavior. + */ + chartProps: ChartComponentProps; + + /** + * Indicates whether this series' legend item has been clicked by the user. + * Used to track legend interaction state for visibility toggling. + */ + isLegendClicked: boolean; + + /** + * Specifies the visual representation type for this series. + */ + drawType: ChartSeriesType; + + /** + * Indicates whether data fetch has been requested for this series. + */ + dataFetchRequested?: boolean; + + /** + * Specifies the position index of the series in the chart. + */ + position?: number; + + /** + * Indicates the number of rectangles used to render the series. + * Relevant for column, bar, and other rectangle-based series types. + */ + rectCount?: number; + + /** + * Determines whether this series is rendered using rectangles. + * Used to apply specific rendering optimizations for rectangle-based series. + */ + isRectSeries: boolean; + + /** + * Contains the calculated stacked values for this series. + * Used in stacked charts to determine the proper positioning of each data point. + */ + stackedValues: StackValuesType; + + /** + * Maximum size value for series points. + * Used to normalize point sizes in bubble and scatter charts. + */ + sizeMax: number; + + /** + * Stores the previous Y-coordinate value during animations or updates. + * Used to calculate transitions between different data states. + */ + previousY: number; + + /** + * Stores the previous X-coordinate value during animations or updates. + * Used to calculate transitions between different data states. + */ + previousX: number; + + /** + * URL path to the image used in legend for this series. + */ + legendImageUrl: string; + + /** + * Defines the visual category of this series (e.g., 'Line', 'Column', 'Area'). + */ + seriesType: string; + + /** + * Stores the index of a data point that has been removed from the series. + * Used for tracking animations and preserving continuity during data updates. + */ + removedPointIndex: number; + + /** + * Reference to the horizontal axis associated with this series. + */ + xAxis: AxisModel; + + /** + * Reference to the vertical axis associated with this series. + * Controls how y-values are mapped to screen coordinates. + */ + yAxis: AxisModel; + + /** + * Defines the rectangular region where the series should be rendered. + */ + clipRect?: Rect; + + /** + * Contains the data processing logic and state for this series. + * Handles data fetching, transformation and preparation for rendering. + */ + dataModule?: ReturnType | null; + + /** + * Contains the currently rendered subset of data in the viewable chart area. + * Helps optimize performance by only processing visible data points. + */ + currentViewData: Object | null; + + /** + * Collection of all data points in this series with their calculated positions. + */ + points: Points[]; + + /** + * Grouping identifier for the series when using multiple series types. + */ + category?: string; + + /** + * The position of this series in the chart's series collection. + */ + index: number; + + /** + * Primary color used for filling the series visualization elements.. + */ + interior: string; + + /** + * Optional secondary fill color used for gradient or specialized rendering effects. + */ + fill?: string | null; + + /** + * The smallest y-value present in the series data. + */ + yMin: number; + + /** + * The smallest x-value present in the series data. + */ + xMin: number; + + /** + * The largest x-value present in the series data. + */ + xMax: number; + + /** + * Array containing all x-coordinate values in the series. + */ + xData: number[]; + + /** + * Array containing all y-coordinate values in the series. + * Allows for direct access to y-values for calculations and rendering. + */ + yData: number[]; + + /** + * The largest y-value present in the series data. + * Used for axis scaling and determining the visible range. + */ + yMax: number; + + /** + * Reference to the parent chart instance containing this series. + */ + chart: Chart; + + /** + * Visibility of series in chart. + */ + visible: boolean; + + /** + * DOM/SVG element representing the data point markers for this series. + */ + symbolElement?: Element | null; + + /** + * Collection of data points that are currently in the visible chart area. + */ + visiblePoints?: Points[]; + + /** + * Total number of data records in the series. + */ + recordsCount: number; + + /** + * Defines the visual shape displayed in the chart legend for this series. + */ + legendShape?: LegendShape; + + /** + * Collection of control points used for drawing curves in line-based series. + */ + drawPoints?: ControlPoints[]; + + /** + * The primary SVG element container for this series visualization. + * Acts as the parent element for all visual components of the series. + */ + seriesElement: Element; + + /** + * The SVG path element that defines the actual shape of the series. + * Contains the path data for lines, areas, or other series visualizations. + */ + pathElement: Element; + + /** + * Controls the rendering order of this series relative to other series. + * Higher values cause the series to be rendered on top of series with lower values. + */ + zOrder?: number; + + /** + * Array of data values that are currently selected or active. + */ + currentData: Array; + + /** + * Indicates the progress of the marker animation. + */ + markerAnimationProgress?: number; + + /** + * Defines the clip path used for marker rendering. + */ + markerClipPath?: string; +} + +/** + * Represents a collection of marker options for chart data points with array-like functionality. + * Used to manage and iterate through multiple marker configurations in chart series. + * + * @private + */ +export interface MarkerOptionsList { + /** + * Array of marker options for rendering data point markers + */ + [index: number]: MarkerOptions; + /** + * Length of the marker options array + */ + length: number; + /** + * Map function for iterating through marker options + */ + map: (callback: (option: MarkerOptions, index: number) => JSX.Element) => JSX.Element[]; + +} + +/** + * Defines properties for a rectangular marker or clipping area used in chart visualizations. + * Controls both the dimensions and visual styling of rectangular elements in markers. + * + * @private + */ +export interface MarkerOptionsRect { + /** + * X-coordinate of the marker clipping rectangle + */ + x?: number; + /** + * Y-coordinate of the marker clipping rectangle + */ + y?: number; + /** + * Width of the marker clipping rectangle + */ + width?: number; + /** + * Height of the marker clipping rectangle + */ + height?: number; + /** + * Fill color of the rectangle + */ + fill?: string; + /** + * Stroke color for the rectangle border + */ + stroke?: string; + /** + * Stroke width for the rectangle border + */ + strokeWidth?: number; + /** + * Opacity of the rectangle + */ + opacity?: number; + /** + * Radius for x-axis rounded corners + */ + rx?: number; + /** + * Radius for y-axis rounded corners + */ + ry?: number; + /** + * SVG transform attribute value + */ + transform?: string; + /** + * Unique ID for the rectangle element + */ + id?: string; +} + +/** + * Represents a group of marker symbols within an SVG element. + * Used for organizing and applying shared transformations to collections of related markers. + * + * @private + */ +export interface MarkerSymbolGroup { + /** + * Unique ID for the symbol group + */ + id: string; + /** + * SVG transform attribute value for positioning the group + */ + transform: string; + /** + * SVG clip-path reference + */ + 'clip-path'?: string; +} + +/** + * Represents options for configuring data point markers in charts. + * Controls the visual appearance, position, shape, and animation of markers that highlight points on series. + * + * @private + */ +export interface MarkerOptions { + /** + * Unique identifier for the marker + */ + id: string; + + /** + * Fill color of the marker + */ + fill: string; + + /** + * Border properties of the marker + */ + border: ChartBorderProps; + + /** + * Opacity of the marker (0-1) + */ + opacity: number; + + /** + * Pattern of dashes for the marker outline + */ + dashArray?: string; + + /** + * SVG transform attribute value + */ + transform?: string; + + /** + * Allows for additional custom properties + */ + [key: string]: unknown; + + /** + * Shape of the marker + */ + shape?: ChartMarkerShape; + + /** + * X coordinate of the marker center + */ + cx?: number; + + /** + * Y coordinate of the marker center + */ + cy?: number; + + /** + * Previous X coordinate for animation + */ + previousCx?: number; + + /** + * Previous Y coordinate for animation + */ + previousCy?: number; +} + + +/** + * Represents an HTML element along with its available size dimensions. + * + * @private + */ +export interface ElementWithSize { + /** + * The HTML element reference. + */ + element: HTMLElement; + + /** + * The available size of the element, including width and height. + */ + availableSize: ChartSizeProps; +} + +/** + * Interface representing the two control points used for Bezier curve calculations in the chart. + * + * @private + */ +export interface ControlPoints { + /** + * The first control point location for the curve. + */ + controlPoint1: ChartLocationProps; + + /** + * The second control point location for the curve. + */ + controlPoint2: ChartLocationProps; +} + + +/** + * Interface representing the collection of series modules used in the chart. + * + * @private + */ +export interface SeriesModules { + /** + * The module representing the line series type. + */ + lineSeriesModule: typeof LineSeriesRenderer; + + /** + * The module representing the column series type. + */ + columnSeriesModule: typeof ColumnSeries; + + /** + * The module representing the bar series type. + */ + barSeriesModule: typeof BarSeries; + + /** + * The module representing the spline series type. + */ + splineSeriesModule: typeof SplineSeriesRenderer; + + /** + * The module representing the area series type. + */ + stepLineSeriesModule: typeof StepLineSeriesRenderer; + + /** + * The module representing the area series type. + */ + areaSeriesModule: typeof AreaSeriesRenderer; + + /** + * The module representing the stacking column series type. + */ + stackingColumnSeriesModule: typeof StackingColumnSeriesRenderer; + + /** + * The module representing the stacking bar series type. + */ + stackingBarSeriesModule: typeof StackingBarSeriesRenderer; + + /** + * The module represents the scatter series type. + */ + scatterSeriesModule: typeof ScatterSeriesRenderer + + /** + * The module represents the bubble series type. + */ + bubbleSeriesModule: typeof BubbleSeriesRenderer; + + /** + * The module representing the spline area series type. + */ + splineAreaSeriesModule: typeof SplineAreaSeriesRenderer; +} + +/** + * Represents the options used to render an SVG path element. + * + * @private + */ +export interface RenderOptions { + /** + * Unique identifier for the SVG element. + */ + id: string; + + /** + * Fill color of the SVG element. + */ + fill: string; + + /** + * Width of the stroke applied to the SVG element. + * + * @default undefined + */ + strokeWidth: number | undefined; + + /** + * Stroke color of the SVG element. + * + * @default undefined + */ + stroke: string | undefined; + + /** + * Opacity level of the SVG element, where 1 is fully opaque and 0 is fully transparent. + * + * @default undefined + */ + opacity: number | undefined; + + /** + * Stroke dash pattern for the SVG element, defined as a string of comma-separated values (e.g., "5,3"). + * + * @default undefined + */ + dashArray: string | undefined; + + /** + * The path data string that defines the shape to be rendered. + */ + d: string; +} + + + +/** + * Interface representing the configuration for a clipping rectangle in the chart. + * + * @private + */ +export interface ClipRectModel { + /** + * Specifies the stroke width of the clipping rectangle's border. + * + * @default undefined + */ + strokeWidth?: string | number | undefined; + + /** + * Specifies the stroke color of the clipping rectangle. + * + * @default undefined + */ + stroke?: string | undefined; + + /** + * Defines the unique identifier for the clipping rectangle element. + */ + id: string; + + /** + * Specifies the fill color of the clipping rectangle. + */ + fill: string; + + /** + * Defines the border settings for the clipping rectangle, including width and color. + */ + border: { + /** + * Width of the rectangle border. + */ + width: number; + /** + * Color of the rectangle border. + */ + color: string; + }; + + /** + * Specifies the opacity of the clipping rectangle. + */ + opacity: number; + + /** + * Defines the rectangular region's position and dimensions. + */ + rect: { + /** + * X-coordinate of the rectangle. + */ + x: number; + /** + * Y-coordinate of the rectangle. + */ + y: number; + /** + * Width of the rectangle. + */ + width: number | undefined; + /** + * Height of the rectangle. + */ + height: number | undefined; + }; + + /** + * Specifies the width of the clipping rectangle. + */ + width: number; + + /** + * Specifies the height of the clipping rectangle. + */ + height: number; +} + +/** + * Interface representing the properties of a series SVG element in the chart. + * + * @private + */ +export interface SeriesElementModel { + /** + * Specifies the unique identifier of the SVG element. + */ + id: string; + + /** + * Defines the transform attribute used to apply translation, rotation, or scaling to the SVG element. + */ + transform: string; + + /** + * Specifies the clipping path applied to the SVG element to restrict the rendering region. + */ + clipPath: string; + + /** + * Defines the CSS style string applied to the SVG element. + */ + style: string; + + /** + * Specifies the ARIA role attribute, indicating the purpose of the SVG element. + */ + role: string; + + /** + * Sets the tab index for the element to manage keyboard navigation order. + */ + tabindex: string; + + /** + * Defines the ARIA label attribute, providing a textual description for screen readers. + */ + 'aria-label': string; +} + +/** + * Represents a location where a tooltip should be positioned in the chart. + * May contain null values when position is being calculated or reset. + * + * @private + */ +export interface TooltipLocation { + /** + * The x-coordinate of the location. + */ + x: number | null; + + /** + * The y-coordinate of the location. + */ + y: number | null; +} + +/** + * Represents the precise position where a data label should be placed in the chart. + * Used for positioning textual information about data points. + * + * @private + */ +export interface LabelLocation { + /** + * The x-coordinate of the location. + */ + x: number; + + /** + * The y-coordinate of the location. + */ + y: number; +} + + +/** + * Defines the configuration options for rendering chart data labels as SVG elements. + * Controls styling, positioning, and formatting of the textual representations of data values. + * + * @private + */ +export interface dataLabelRenderOptions { + /** + * Unique identifier for the data label SVG element. + */ + id: string; + + /** + * The x-coordinate position of the data label. + */ + x: number; + + /** + * The y-coordinate position of the data label. + */ + y: number; + + /** + * The background fill color of the data label. + */ + fill: string; + + /** + * The font size of the data label text (e.g., '12px', '0.8em'). + */ + 'font-size': string; + + /** + * The font style of the data label text (e.g., 'normal', 'italic'). + */ + 'font-style': string; + + /** + * The font family used for the data label text (e.g., 'Arial', 'Verdana'). + */ + 'font-family': string; + + /** + * The font weight of the data label text (e.g., 'normal', 'bold'). + */ + 'font-weight': string; + + /** + * The text anchor alignment for the data label (e.g., 'start', 'middle', 'end'). + */ + 'text-anchor': string; + + /** + * Optional rotation angle for the data label in degrees. + */ + labelRotation?: number; + + /** + * Optional SVG transform string to apply additional transformations to the data label. + */ + transform?: string; + + /** + * Optional opacity level for the data label, from 0 (transparent) to 1 (opaque). + */ + opacity?: number; + + /** + * Optional dominant baseline alignment for the text (e.g., 'auto', 'middle', 'hanging'). + */ + 'dominant-baseline'?: string; + + /** + * The width of the stroke for the data label's background shape or border. + */ + strokeWidth: number; + + /** + * The color of the stroke for the data label's background shape or border. + */ + stroke: string; + + /** + * The dash pattern for the data label's background shape or border (e.g., '3,3' for dashed lines). + */ + dashArray: string; + + /** + * The path data attribute defining the shape of the data label's background container. + */ + d: string; +} + + +/** + * Represents the result of a text element rendering operation, containing both + * the path rendering options and positioning information. + * Used for managing text elements with background shapes or containers in charts. + * + * @private + */ +export interface TextElementResult { + /** + * The rendering options for the background shape or container of the text element. + */ + renderOptions: RenderOptions; + + /** + * The actual text content to be displayed. + */ + text: string; + + /** + * The x-coordinate transformation or offset to be applied to the text element. + */ + transX: number; + + /** + * The y-coordinate transformation or offset to be applied to the text element. + */ + transY: number; +} + + +/** + * Interface representing the style settings for text elements in the chart. + * Provides a consistent way to define appearance properties for all textual elements like labels and titles. + * + * @private + */ +export interface TextStyleModel { + /** + * The color of the text (e.g., '#000000' or 'red'). + */ + color: string; + + /** + * The font size of the text (e.g., '12px', '1em'). + */ + fontSize: string; + + /** + * The font style of the text (e.g., 'normal', 'italic', 'oblique'). + */ + fontStyle: string; + + /** + * The font family of the text (e.g., 'Arial', 'Verdana'). + */ + fontFamily: string; + + /** + * The weight of the font (e.g., 'normal', 'bold', 'lighter', or numeric values). + */ + fontWeight: string; +} + +/** + * Options for positioning and styling the container of a data label. + * Controls the rectangular area in which a data label is rendered and its visual appearance. + * + * @private + */ +export interface dataLabelOptions { + /** + * The x-coordinate of the label. + */ + x: number; + + /** + * The y-coordinate of the label. + */ + y: number; + + /** + * Optional SVG transform string to apply transformations like rotate, scale, etc. + */ + transform?: string; + + /** + * Optional width of the label. + */ + width?: number; + + /** + * Optional height of the label. + */ + height?: number; + + /** + * Optional x-axis radius for rounded corners. + */ + rx?: number; + + /** + * Optional y-axis radius for rounded corners. + */ + ry?: number; +} + +/** + * Options for defining a marker's position and shape in chart visualizations. + * Used to configure the visual representation of data points on series. + * + * @private + */ +export interface markerOptions { + /** + * The x-coordinate (center) of the marker. + */ + cx: number; + + /** + * The y-coordinate (center) of the marker. + */ + cy: number; + + /** + * Optional SVG path data string to define a custom shape. + */ + d?: string; + + /** + * Optional predefined shape name (e.g., 'circle', 'square', 'triangle'). + */ + pathShape?: string; +} + +/** + * Represents data for an axis during zoom operations in interactive charts. + * Stores zoom state, factors, and visible range information for axis manipulation. + * + */ +export interface AxisDataProps { + /** + * The zoom factor applied to the axis. + * A value between 0 and 1 indicating the scale level of the zoom. + */ + zoomFactor: number; + + /** + * The zoom position of the axis. + * Represents the starting position of the zoomed view as a normalized value. + */ + zoomPosition: number; + + /** + * The visible range of the axis after zooming. + * Includes minimum, maximum, interval, and delta values. + */ + axisRange: VisibleRangeProps; + + /** + * The name of the axis being zoomed. + */ + axisName: string; +} + +/** + * Represents configuration options for a chart title's position, style, and layout. + * + * @private + */ +export interface TitleOptions { + /** + * The x-coordinate position of the title. + */ + x: number; + + /** + * The y-coordinate position of the title. + */ + y: number; + + /** + * The rotation applied to the title text (e.g., '0', '45', '90'). + */ + rotation: string; + + /** + * The SVG text-anchor alignment for the title (e.g., 'start', 'middle', 'end'). + */ + textAnchor: string; + + /** + * The text style settings applied to the title. + */ + textStyle: TextStyleModel; + + /** + * The lines of text that make up the title. + */ + title: string[]; + + /** + * The rectangle defining the border area around the title. + */ + titleBorder: Rect; + + /** + * The position of the title relative to the chart (e.g., 'Top', 'Bottom', 'Left', 'Right'). + */ + position: TitlePosition; + + /** + * The width of the title border. + */ + borderWidth: number; + + /** + * The size of the rendered title. + */ + titleSize: ChartSizeProps; +} + +/** + * Represents the visible range configuration of a chart axis. + * + */ +export interface VisibleRangeProps { + /** + * The minimum value visible on the axis. + */ + minimum: number; + + /** + * The maximum value visible on the axis. + */ + maximum: number; + + /** + * The interval between axis ticks or labels. + */ + interval: number; + + /** + * The difference between the maximum and minimum values. + * Typically used for scaling and layout calculations. + */ + delta: number; +} + +/** + * Represents the rendering result for a data label in a chart series. + * Contains properties for both the background shape and text content. + * + * @private + */ +export interface DataLabelRendererResult { + /** + * Optional configuration for the background shape of the data label. + * When provided, a shape will be rendered behind the text. + */ + shapeRect?: { + /** Unique identifier for the shape element */ + id: string; + /** Background fill color of the shape */ + fill: string; + /** Border configuration for the shape */ + border: { + /** Optional width of the border in pixels */ + width?: number; + /** Optional color of the border */ + color?: string + }; + /** Opacity value for the shape (0-1) */ + opacity: number; + /** Position and dimension of the rectangle */ + rect: Rect; + /** Horizontal corner radius for rounded rectangles */ + rx: number; + /** Vertical corner radius for rounded rectangles */ + ry: number; + /** SVG transform string for positioning the shape */ + transform: string; + /** Optional stroke color for the outline */ + stroke?: string; + }; + /** + * Configuration for the text content of the data label. + * This is always required as data labels must display text. + */ + textOption: { + /** + * Text content to display in the data label. + * Can be a single string or an array of strings for multi-line labels. + */ + text: string | string[]; + /** + * Rendering options for the text element + */ + renderOptions: { + /** Unique identifier for the text element */ + id: string; + /** X-coordinate position of the text */ + x: number; + /** Y-coordinate position of the text */ + y: number; + /** Text color */ + fill: string; + /** Font size with unit (e.g., '12px') */ + 'font-size': string; + /** Font family name */ + 'font-family': string; + /** Font weight (e.g., 'normal', 'bold') */ + 'font-weight': string; + /** Font style (e.g., 'normal', 'italic') */ + 'font-style': string; + /** Text anchor position (e.g., 'start', 'middle', 'end') */ + 'text-anchor': string; + /** SVG transform string for positioning the text */ + transform: string; + }; + }; +} + +/** + * Represents the type definition of bubble series module. + * + * @private + */ +export interface BubbleSeriesType { + render: (series: SeriesProperties, isInverted: boolean) => + { options: RenderOptions[]; marker: ChartMarkerProps }; + doAnimation: Function; +} + +/** + * Represents the type definition of scatter series module. + * + * @private + */ +export interface ScatterSeriesType { + render: (series: SeriesProperties, isInverted: boolean) => + { options: RenderOptions[]; marker: ChartMarkerProps }; + renderPoint: ( + point: Points, + series: SeriesProperties, + markerShape: ChartMarkerShape, + scatterBorder: { width: number; color: string; dashArray: string }, + isInverted: boolean + ) => MarkerOptions | null; + doAnimation: Function; +} + +/** + * Represents the type definition and conatines the parameters required for step line series module. + * + * @private + */ +export interface StepLineSeriesType { + previousX: number; + previousY: number; + render: (series: SeriesProperties, isInverted: boolean) => + RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }; + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + _currentSeries: SeriesProperties, + _currentPoint: Points | undefined, + _pointIndex: number, + visibleSeries?: SeriesProperties[] + ) => { + strokeDasharray: string | number; strokeDashoffset: number; interpolatedD?: string; + animatedDirection?: string; animatedTransform?: string; + }; +} + +/** + * Represents the base configuration for DataLabel. + * + * @private + * + * @extends DataLabel + */ +export interface DataLabelProperties extends ChartDataLabelProps { + /** + * Indicates whether the data labels should be inverted. + */ + inverted?: boolean; + /** + * A common identifier used for data label elements. + * + * @default undefined + */ + commonId?: string; + /** + * Background color of the chart, used for positioning data labels. + * + * @default undefined + */ + chartBackground?: string; + /** + * X-coordinate position of the data label. + * + * @default undefined + */ + locationX?: number; + /** + * Reference to the parent chart instance. + * + * @default undefined + */ + chart?: Chart; + /** + * Height of the marker associated with the data label. + * + * @default undefined + */ + markerHeight?: number; + /** + * Width of the border around the data label. + * + * @default undefined + */ + borderWidth?: number; + /** + * Indicates whether the data label is rendered as a shape. + * + * @default undefined + */ + isShape?: boolean; + /** + * Background color of the font used in the data label. + * + * @default undefined + */ + fontBackground?: string; + +} +/** + * Defines a single data point in a chart series. + * + * @private + */ +export interface DataPoint { + /** + * The x-value of the data point. + * Can be a number (for a numeric axis) or a string (for a category or date axis). + */ + x: number | string; + /** + * The y-value of the data point. + * Represents the quantitative value plotted along the y-axis. + */ + y: number; +} +/** + * Interface representing the marker element data for animation. + * Used when animating individual marker properties. + * + * @private + */ +export interface MarkerElementData { + /** The x-radius for elliptical shapes */ + rx?: number; + /** The y-radius for elliptical shapes */ + ry?: number; + /** The radius for circular shapes */ + r?: number; + /** The opacity of the marker element */ + opacity?: number; + /** Additional data for tracking animation */ + _animationData?: { + /** Original x position */ + originalX?: number; + /** Original y position */ + originalY?: number; + /** Target x position */ + targetX?: number; + /** Target y position */ + targetY?: number; + }; +} +/** + * Represents the position and appearance data for a chart marker. + * Used to track marker positions across animations for smooth transitions. + * + * @private + */ +export type MarkerPosition = { + /** X-coordinate of the marker center */ + cx: number; + /** Y-coordinate of the marker center */ + cy: number; + /** SVG path data for complex marker shapes */ + d?: string; + /** The shape identifier of the marker */ + pathShape?: string; +}; +/** + * Represents the base configuration for Marker. + * + * @private + * + * @extends Marker + */ +export interface MarkerProperties extends ChartMarkerProps { + /** + * A collection of marker rendering options used for visualizing data points. + * Each option includes styling properties such as shape, size, color, and border. + */ + markerOptionsList?: MarkerOptionsList; + /** + * Contains configuration settings for marker rendering, including visibility, + * animation effects, and interaction behavior. + */ + options?: MarkerOptionsRect; + /** + * Represents the SVG group element that contains all marker symbols rendered for the series. + * Useful for batch operations such as animation, visibility toggling, or event handling. + */ + symbolGroup?: MarkerSymbolGroup; +} + +/** + * Represents a data point in a chart series with its properties and state information. + * + * @private + */ +export interface Points { + /** The text representation of the point's value. */ + textValue: string; + /** The original y-value before any transformations or calculations. */ + originalY: number; + /** Specifies the x-value of the point. */ + x: Object; + /** Specifies the y-value of the point. */ + y: Object; + /** Indicates whether the point is visible. */ + visible: boolean; + /** Specifies the text associated with the point. */ + text: string; + /** Specifies the tooltip content for the point. */ + tooltip: string; + /** Specifies the color of the point. */ + color: string; + /** Specifies the locations of symbols associated with the point. */ + symbolLocations: ChartLocationProps[] | null; + /** Specifies the x-value of the point. */ + xValue: number | null; + /** Specifies the y-value of the point. */ + yValue: number | null; + /** Specifies the index of the point in the series. */ + index: number; + /** Specifies the regions associated with the point. */ + regions: Rect[] | null; + /** Specifies the percentage value of the point. */ + percentage: number | null; + /** Indicates whether the point is empty. */ + isEmpty: boolean; + /** Specifies the region data of the point. */ + regionData: null; + /** Specifies the minimum value of the point. */ + minimum: number; + /** Specifies the maximum value of the point. */ + maximum: number; + /** Specifies the interior color of the point. */ + interior: string; + /** Specifies the series to which the point belongs. */ + series: Object; + /** + * Specifies whether the point is in the visible range. + */ + isPointInRange: boolean; + /** + * Defines the marker settings for the data point. + */ + marker: ChartMarkerProps; + /** Specifies the size value of the point. */ + size: Object; +} + +/** + * Interface defining the core functionality for line series rendering in charts. + * Provides methods for calculating line directions, rendering series, and handling animations. + * + * @private + */ +export interface LineSeriesInterface { + /** + * Determines the direction of a line segment between two points. + * + * @param firstPoint - The starting point coordinates and data + * @param secondPoint - The ending point coordinates and data + * @param series - The series properties containing style and behavior settings + * @param isInverted - Whether the chart axes are inverted + * @param getPointLocation - Function to retrieve point location coordinates + * @param startPoint - The starting point identifier + * @returns The direction string used for path construction + */ + getLineDirection: ( + firstPoint: Points, + secondPoint: Points, + series: SeriesProperties, + isInverted: boolean, + getPointLocation: Function, + startPoint: string + ) => string; + + /** + * Renders a line series with optional markers. + * + * @param series - The series properties containing data and styling information + * @param isInverted - Whether the chart axes are inverted + * @returns Either an array of render options or an object containing both options and marker properties + */ + render: (series: SeriesProperties, isInverted: boolean) => + RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }; + + /** + * Handles animation for line series. + * + * @param pathOptions - The path rendering options and properties + * @param index - The index of the series being animated + * @param animationState - Animation state object containing refs and progress + * @param enableAnimation - Flag indicating if animation is enabled + * @param _currentSeries - The current series being processed (unused) + * @param _currentPoint - The current point being processed (unused) + * @param _pointIndex - The index of the current point (unused) + * @param visibleSeries - Array of visible series for animation calculation + * @returns Animation properties including dash patterns and transforms + */ + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + _currentSeries: SeriesProperties, + _currentPoint: Points | undefined, + _pointIndex: number, + visibleSeries: SeriesProperties[] + ) => { + strokeDasharray: string | number; strokeDashoffset: number; interpolatedD?: string; + animatedDirection?: string; animatedTransform?: string; + }; +} + +/** + * Defines the animation state structure for area series rendering. + * This interface encapsulates all the necessary state and references required to manage + * smooth animations during area series transitions, including initial rendering and data updates. + * + * @private + */ +export interface AreaSeriesAnimateState { + /** + * Reference to an array storing the previous path lengths for each series. + */ + previousPathLengthRef: React.RefObject; + /** + * Reference to an array tracking which series are in their initial render state. + */ + isInitialRenderRef: React.RefObject; + /** + * Reference to a record storing the previously rendered path data strings. + */ + renderedPathDRef: React.RefObject>; + /** + * The current animation progress as a value between 0 and 1. + */ + animationProgress: number; + /** + * Reference tracking if this is the very first render of the entire chart. + */ + isFirstRenderRef: React.RefObject; + /** + * Reference to the previous series rendering options for all series. + */ + previousSeriesOptionsRef: React.RefObject; +} +/** + * Defines the contract for the Area Series Renderer module. + * + * @private + */ +export interface AreaSeriesRendererType { + /** + * Generates an SVG path command segment for a single data point in an area series. + */ + getAreaPathDirection: Function; + /** + * Generates SVG path segments to handle empty (null/undefined) data points in area series. + */ + getAreaEmptyDirection: Function; + /** + * Calculates and returns animation properties for area series paths. + */ + doAnimation: Function; + /** + * The main rendering function that processes an entire area series and generates all required SVG paths. + */ + render: Function; +} +/** + * Defines the animation state structure for spline area series rendering. + * + * @private + */ +export interface SplineAreaSeriesAnimateState { + /** + * Reference to an array storing the previous path lengths for each series. + */ + previousPathLengthRef: React.RefObject; + /** + * Reference to boolean flags indicating whether each series is in its initial render state. + */ + isInitialRenderRef: React.RefObject; + /** + * Reference to a record storing the previously rendered SVG path data strings. + */ + renderedPathDRef: React.RefObject>; + /** + * Current animation progress as a normalized value between 0 and 1. + */ + animationProgress: number; + /** + * Reference to a boolean flag indicating if this is the very first render of the chart. + */ + isFirstRenderRef: React.RefObject; + /** + * Reference to the previous frame's series rendering options. + */ + previousSeriesOptionsRef: React.RefObject; +} +/** + * Defines the contract for the Spline Area Series Renderer module. + * + * @private + */ +export interface SplineAreaSeriesInterface { + /** + * Computes natural spline coefficients using the natural boundary conditions. + */ + naturalSplineCoefficients: Function; + /** + * Generates control points for cubic Bezier curves between two consecutive data points. + */ + getControlPoints: Function; + /** + * Manages animation effects for spline area series during rendering transitions. + */ + doAnimation: Function; + /** + * Main rendering function that generates SVG path elements for spline area series. + */ + render: Function; +} +/** + * Represents parsed SVG path command data with coordinate values and control points. + * + * @private + */ +export interface CommandValues { + /** + * The SVG path command type identifier. + */ + type: string; + /** + * The target x-coordinate for the path command. + */ + x: number; + /** + * The target y-coordinate for the path command. + */ + y: number; + /** + * The x-coordinate of the first control point for cubic Bezier curves. + */ + cx1?: number; + /** + * The y-coordinate of the first control point for cubic Bezier curves. + */ + cy1?: number; + /** + * The x-coordinate of the second control point for cubic Bezier curves. + */ + cx2?: number; + /** + * The y-coordinate of the second control point for cubic Bezier curves. + */ + cy2?: number; +} + +/** + * Represents a single path command used in vector graphics or chart rendering. + * Each command includes a type (e.g., 'M' for move, 'L' for line, 'C' for curve) + * and a list of coordinates relevant to that command. + * + * @private + */ +export type PathCommand = { + + /** + * The type of path command, such as 'M' (move), 'L' (line), 'C' (cubic Bézier curve), etc. + * This determines how the coordinates should be interpreted. + */ + type: string; + + /** + * An array of numerical values representing the coordinates for the path command. + * The number and meaning of these values depend on the command type. + */ + coords: number[]; +}; + + +/** + * Defines the properties passed to child components within the chart provider context. + * These properties include chart configuration, data, rendering flags, and update methods. + * + * @private + */ +export interface ChartProviderChildProps { + /** + * General properties for the chart component such as dimensions, theme, and rendering options. + */ + chartProps: ChartComponentProps; + + /** + * Configuration for the chart's main title including text, style, and alignment. + */ + chartTitle: ChartTitleProps; + + /** + * Configuration for the chart's subtitle including text, style, and alignment. + */ + chartSubTitle: ChartTitleProps; + + /** + * Defines the layout and background settings of the chart area. + */ + chartArea: ChartAreaProps; + + /** + * Configuration for the chart legend including position, visibility, and styling. + */ + chartLegend: ChartLegendProps; + + /** + * Indicates whether the chart should be rendered. + */ + render: boolean; + + /** + * Reference to the parent DOM element along with its size information. + */ + parentElement: ElementWithSize; + + /** + * Array of column definitions used for data binding in the chart. + */ + columns: Column[]; + + /** + * Array of row data used for populating the chart. + */ + rows: Row[]; + + /** + * Series configuration including type, data mapping, and styling. + */ + chartSeries: ChartSeriesProps[]; + + /** + * Tooltip configuration for displaying data point information on hover. + */ + chartTooltip: ChartTooltipProps; + + /** + * Configuration for stack labels displayed on stacked chart series. + */ + chartStackLabels: ChartStackLabelsProps; + + /** + * Updates the stack labels configuration. + */ + setChartStackLabels: (stackLabels: ChartStackLabelsProps) => void; + + /** + * Updates the chart title configuration. + */ + setChartTitle: (titleProps: ChartTitleProps) => void; + + /** + * Updates the chart subtitle configuration. + */ + setChartSubTitle: (subtitleProps: ChartTitleProps) => void; + + /** + * Updates the chart area configuration. + */ + setChartArea: (areaProps: ChartAreaProps) => void; + + /** + * Updates the chart legend configuration. + */ + setChartLegend: (legendProps: ChartLegendProps) => void; + + /** + * Updates the chart columns. + */ + setChartColumns: (columns: Column[]) => void; + + /** + * Updates the chart rows. + */ + setChartRows: (rows: Row[]) => void; + + /** + * Updates the primary X-axis configuration. + */ + setChartPrimaryXAxis: (xAxis: AxisModel) => void; + + /** + * Updates the primary Y-axis configuration. + */ + setChartPrimaryYAxis: (yAxis: AxisModel) => void; + + /** + * Updates the chart series configuration. + */ + setChartSeries: (series: ChartSeriesProps[]) => void; + + /** + * Updates the collection of axes used in the chart. + */ + setChartAxes: (axes: AxisModel[]) => void; + + /** + * Configuration for zooming and panning behavior in the chart. + */ + chartZoom: ChartZoomSettingsProps; + + /** + * Updates the chart zoom settings. + */ + setChartZoom: (zoom: ChartZoomSettingsProps) => void; + + /** + * Collection of axis models used in the chart. + */ + axisCollection: AxisModel[]; + + /** + * Updates the chart tooltip configuration. + */ + setChartTooltip: (tooltip: ChartTooltipProps) => void; +} + +/** + * Represents the event arguments used during the rendering of a data point in the chart. + * + * @private + */ +export interface PointRenderingEvent { + /** + * The name of the series to which the point belongs + */ + seriesName: string; + + /** + * The data point being rendered. + */ + point: Points; + + /** + * The fill color applied to the point. + */ + fill: string; + + /** + * The border styling applied to the point. + */ + border: ChartBorderProps; + + /** + * The height of the marker representing the point, in pixels. + */ + markerHeight?: number; + + /** + * The width of the marker representing the point, in pixels + */ + markerWidth?: number; + + /** + * The shape used to render the point marker + */ + markerShape?: ChartMarkerShape; + + /** + * The corner radius configuration applied to the point marker. + */ + cornerRadius?: CornerRadius; + + /** + * Indicates whether the event should be canceled. + * Set to `true` to prevent the default action. + */ + cancel: boolean; +} + +/** + * Represents the event arguments triggered during the rendering of data labels in the chart. + * + * @private + */ +export interface DataLabelContentProps { + /** + * The name of the series to which the data label belongs. + */ + seriesName: string + /** + * The data point associated with the label. + */ + point: Points; + /** + * The text content of the label. + */ + text: string; + /** + * The dimensions of the label text, including width and height. + */ + textSize: ChartSizeProps; + /** + * The fill color applied to the label. + */ + color: string; + /** + * The border styling applied to the label. + */ + border: ChartBorderProps; + /** + * The font styling applied to the label text. + */ + font: ChartFontProps; + /** + * Specifies whether the data label position can be adjusted. + */ + location: LabelLocation; +} diff --git a/components/charts/src/chart/chart-axis/ChartAxes.tsx b/components/charts/src/chart/chart-axis/ChartAxes.tsx new file mode 100644 index 0000000..88e01a8 --- /dev/null +++ b/components/charts/src/chart/chart-axis/ChartAxes.tsx @@ -0,0 +1,129 @@ + +import { JSX, ReactElement, useContext, useEffect, useMemo } from 'react'; +import { ChartAxisProps, MajorGridLines, MajorTickLines, MinorGridLines, MinorTickLines } from '../base/interfaces'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartContext } from '../layout/ChartProvider'; +import { ChartAxisLabelProps, ChartAxisTitleProps } from './base'; +import { ChartMajorGridLines } from './MajorGridLines'; +import { ChartMinorGridLines } from './MinorGridLines'; +import { ChartMajorTickLines } from './MajorTickLines'; +import { ChartMinorTickLines } from './MinorTickLines'; +import { ChartAxisLabel } from './LabelStyle'; +import { ChartAxisTitle } from './TitleStyle'; +import { AxisModel, ChartProviderChildProps } from '../chart-area/chart-interfaces'; +import { ChartStripLines } from './StripLines'; +import { processChildElement, processStripLines } from './PrimaryXAxis'; +import { ChartStripLineProps } from '../base/interfaces'; +import * as React from 'react'; +import { extend, isNullOrUndefined } from '@syncfusion/react-base'; + +/** + * Interface for ChartAxes props. + * Represents a container component to group multiple chart axes (category or value). + */ +interface AxesProps { + /** + * Child axis components (`ChartAxis`) to be rendered. + */ + children?: ReactElement[] | ReactElement; +} +const axisCollection: AxisModel[] = []; + +/** + * `ChartAxes` is a non-rendering wrapper component used to group multiple axis components. + * It can include multiple `ChartAxis` components as children. + * + * @param {AxesProps} props - The properties for the ChartAxes component which include children that can be `ChartAxis` components. + * @returns {JSX.Element} - The ChartAxes component. + */ +export const ChartAxes: React.FC = (props: AxesProps): JSX.Element => { + const context: ChartProviderChildProps = useContext(ChartContext); + useEffect(() => { + context.setChartAxes(axisCollection); + }, [props.children]); + return <>{props.children}; +}; +ChartAxes.displayName = 'ChartAxes'; + +/** + * `ChartAxis` defines an individual axis (either X or Y) in the chart. + * It can be configured as a category or value axis. + * + * @param {Axis} props - The properties for the ChartAxis component. + * @returns {JSX.Element} - The ChartAxis component. + */ +export const ChartAxis: React.FC = (props: ChartAxisProps): JSX.Element => { + const childArray: React.ReactNode[] = React.Children.toArray(props.children); + const childrenPropsSignature: string = childArray + .map((child: React.ReactNode) => processChildElement(child, ChartStripLines)) + .join('|'); // simple delimiter for the string array + const serializedProps: string = useMemo(() => { + const { children, ...rest } = props; + return JSON.stringify(rest); + }, [props]); + useEffect(() => { + const axisProps: Partial = { ...defaultChartConfigs.SecondaryAxis, ...props }; + let majorGridLines: MajorGridLines = defaultChartConfigs.MajorGridLines; + let minorGridLines: MinorGridLines = defaultChartConfigs.MinorGridLines; + let majorTickLines: MinorTickLines = defaultChartConfigs.MajorTickLines; + let minorTickLines: MinorTickLines = defaultChartConfigs.MinorTickLines; + let labelStyle: ChartAxisLabelProps = defaultChartConfigs.LabelStyle; + let titleStyle: ChartAxisTitleProps = defaultChartConfigs.TitleStyle; + let stripLines: ChartStripLineProps[] = [...defaultChartConfigs.StripLines]; + childArray.forEach((child: React.ReactNode) => { + if (!React.isValidElement(child)) { return; } + const childProps: Record = + child.props as Record; + if (child.type === ChartMajorGridLines) { + majorGridLines = { + ...defaultChartConfigs.MajorGridLines, + ...childProps + }; + } else if (child.type === ChartMinorGridLines) { + minorGridLines = { + ...defaultChartConfigs.MinorGridLines, + ...childProps + }; + } else if (child.type === ChartMajorTickLines) { + majorTickLines = { + ...defaultChartConfigs.MajorTickLines, + ...childProps + }; + } else if (child.type === ChartMinorTickLines) { + minorTickLines = { + ...defaultChartConfigs.MinorTickLines, + ...childProps + }; + } else if (child.type === ChartAxisLabel) { + labelStyle = { + ...defaultChartConfigs.LabelStyle, + ...childProps + }; + } else if (child.type === ChartAxisTitle) { + titleStyle = { + ...defaultChartConfigs.TitleStyle, + ...childProps + }; + } else if (child.type === ChartStripLines) { + stripLines = processStripLines(child, defaultChartConfigs.StripLines); + } + }); + axisProps.majorGridLines = extend({}, majorGridLines); + axisProps.majorGridLines.width = isNullOrUndefined(axisProps.majorGridLines.width) ? 0 : axisProps.majorGridLines.width; + axisProps.minorGridLines = minorGridLines; + axisProps.majorTickLines = majorTickLines; + axisProps.minorTickLines = minorTickLines; + axisProps.labelStyle = labelStyle; + axisProps.titleStyle = titleStyle; + axisProps.stripLines = stripLines; + axisProps.lineStyle = { ...defaultChartConfigs.SecondaryAxis.lineStyle, ...props.lineStyle }; + if (axisCollection.length > 0 && axisCollection.some((axis: AxisModel) => axis.name === axisProps.name)) { + const index: number = axisCollection.findIndex((axis: AxisModel) => axis.name === axisProps.name); + axisCollection[index as number] = axisProps as AxisModel; + } else { + axisCollection.push(axisProps as AxisModel); + } + }, [serializedProps, childrenPropsSignature]); + return <>; +}; +ChartAxis.displayName = 'ChartAxis'; diff --git a/components/charts/src/chart/chart-axis/Columns.tsx b/components/charts/src/chart/chart-axis/Columns.tsx new file mode 100644 index 0000000..8035ec1 --- /dev/null +++ b/components/charts/src/chart/chart-axis/Columns.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { Column, ColumnsProps } from '../base/interfaces'; +import { isValidElement, useContext, useEffect, useRef } from 'react'; +import { ChartContext } from '../layout/ChartProvider'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartProviderChildProps, ColumnProps } from '../chart-area/chart-interfaces'; + +/** + * ChartColumns component for configuring and rendering multiple column definitions within the chart. + * It collects all valid ChartColumn children, merges them with default configurations, + * and updates the chart context accordingly. + * + * @param {ColumnsProps} props - Props containing ChartColumn children. + * @returns {null} This component does not render any visible output. + */ +export const ChartColumns: React.FC = (props: ColumnsProps) => { + const context: ChartProviderChildProps = useContext(ChartContext); + const previousColumnsRef: React.RefObject = useRef([]); + useEffect(() => { + const columnArray: Column[] = React.Children.map(props.children, (child: React.ReactNode) => { + if (isValidElement(child) && child.type === ChartColumn) { + return { + ...defaultChartConfigs.Column, + ...child.props + }; + } + return null; + })?.filter((column: Column): column is ColumnProps => column !== null) as Column[]; + previousColumnsRef.current = columnArray; + context?.setChartColumns(columnArray); + }, [props.children]); + + return null; +}; + +/** + * ChartColumn component representing a single column configuration within the chart. + * This component is used as a child of ChartColumns and does not render any output directly. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartColumn: React.FC = () => { + return null; +}; + diff --git a/components/charts/src/chart/chart-axis/LabelStyle.tsx b/components/charts/src/chart/chart-axis/LabelStyle.tsx new file mode 100644 index 0000000..0395c71 --- /dev/null +++ b/components/charts/src/chart/chart-axis/LabelStyle.tsx @@ -0,0 +1,13 @@ +import { ChartAxisLabelProps } from './base'; + +/** + * ChartAxisLabel component for configuring the style of axis labels in the chart. + * This component is used as a configuration-only component and does not render any output. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartAxisLabel: React.FC = () => { + return null; +}; + +ChartAxisLabel.displayName = 'ChartAxisLabel'; diff --git a/components/charts/src/chart/chart-axis/MajorGridLines.tsx b/components/charts/src/chart/chart-axis/MajorGridLines.tsx new file mode 100644 index 0000000..38973cb --- /dev/null +++ b/components/charts/src/chart/chart-axis/MajorGridLines.tsx @@ -0,0 +1,13 @@ +import { MajorGridLines } from '../base/interfaces'; + +/** + * ChartMajorGridLines component for configuring the appearance of major grid lines in the chart. + * This is a configuration-only component and does not render any visual output. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartMajorGridLines: React.FC = () => { + return null; +}; + +ChartMajorGridLines.displayName = 'MajorGridLines'; diff --git a/components/charts/src/chart/chart-axis/MajorTickLines.tsx b/components/charts/src/chart/chart-axis/MajorTickLines.tsx new file mode 100644 index 0000000..7aa0ead --- /dev/null +++ b/components/charts/src/chart/chart-axis/MajorTickLines.tsx @@ -0,0 +1,14 @@ +import { MajorTickLines } from '../base/interfaces'; + + +/** + * ChartMajorTickLines component for configuring the appearance of major tick lines on chart axes. + * This is a configuration-only component and does not render any visual output. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartMajorTickLines: React.FC = () => { + return null; +}; + +ChartMajorTickLines.displayName = 'MajorTickLines'; diff --git a/components/charts/src/chart/chart-axis/MinorGridLines.tsx b/components/charts/src/chart/chart-axis/MinorGridLines.tsx new file mode 100644 index 0000000..3f83758 --- /dev/null +++ b/components/charts/src/chart/chart-axis/MinorGridLines.tsx @@ -0,0 +1,14 @@ +import { MajorGridLines } from '../base/interfaces'; + + +/** + * ChartMinorGridLines component for configuring the appearance of minor grid lines in the chart. + * This is a configuration-only component and does not render any visual output. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartMinorGridLines: React.FC = () => { + return null; +}; + +ChartMinorGridLines.displayName = 'MinorGridLines'; diff --git a/components/charts/src/chart/chart-axis/MinorTickLines.tsx b/components/charts/src/chart/chart-axis/MinorTickLines.tsx new file mode 100644 index 0000000..c7c83c6 --- /dev/null +++ b/components/charts/src/chart/chart-axis/MinorTickLines.tsx @@ -0,0 +1,13 @@ +import { MinorTickLines } from '../base/interfaces'; + +/** + * ChartMinorTickLines component for configuring the appearance of minor tick lines on chart axes. + * This is a configuration-only component and does not render any visual output. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartMinorTickLines: React.FC = () => { + return null; +}; + +ChartMinorTickLines.displayName = 'MinorTickLines'; diff --git a/components/charts/src/chart/chart-axis/PrimaryXAxis.tsx b/components/charts/src/chart/chart-axis/PrimaryXAxis.tsx new file mode 100644 index 0000000..2cc7879 --- /dev/null +++ b/components/charts/src/chart/chart-axis/PrimaryXAxis.tsx @@ -0,0 +1,227 @@ +import * as React from 'react'; +import { ChartAxisProps, MajorGridLines, MajorTickLines, MinorGridLines, MinorTickLines } from '../base/interfaces'; +import { ChartContext } from '../layout/ChartProvider'; +import { useContext, useEffect, useMemo } from 'react'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartMajorGridLines } from './MajorGridLines'; +import { ChartMinorGridLines } from './MinorGridLines'; +import { ChartMajorTickLines } from './MajorTickLines'; +import { ChartMinorTickLines } from './MinorTickLines'; +import { ChartAxisLabelProps, ChartAxisTitleProps } from './base'; +import { ChartAxisLabel } from './LabelStyle'; +import { ChartAxisTitle } from './TitleStyle'; +import { AxisModel, ChartProviderChildProps } from '../chart-area/chart-interfaces'; +import { ChartStripLines, ChartStripLine } from './StripLines'; +import { getCircularReplacer } from '../series/Series'; +import { ChartStripLineProps } from '../base/interfaces'; +import { extend, isNullOrUndefined } from '@syncfusion/react-base'; + +/** + * Safely stringifies an object to JSON, handling circular references. + * + * @param {unknown} obj - The object to stringify + * @returns {string} JSON string representation of the object, or empty object if null/undefined + */ +export const safeStringify: (obj: unknown) => string = (obj: unknown) => { + return JSON.stringify(obj, getCircularReplacer()) || '{}'; +}; + +/** + * Safely parses a JSON string to an object. + * + * @param {string} jsonString - The JSON string to parse + * @returns {unknown} Parsed object, or empty object if input is invalid + */ +export const safeParse: (jsonString: string) => unknown = (jsonString: string) => { + return jsonString ? JSON.parse(jsonString) : {}; +}; + +/** + * Processes a React child element into a serialized JSON representation. + * Handles special cases for strip line components. + * + * @param {React.ReactNode} child - The child element to process. + * @param {React.ElementType} ChartStripLines - The strip lines component type to check against. + * @returns {string | null} A JSON string representation of the element, or null if invalid. + */ +export const processChildElement: ( + child: React.ReactNode, + ChartStripLines: React.ElementType +) => string | null = ( + child: React.ReactNode, + ChartStripLines: React.ElementType +): string | null => { + if (React.isValidElement(child)) { + const type: React.ElementType, keyof React.JSX.IntrinsicElements> = + child.type as React.ElementType; + const typeName: string = typeof type === 'string' + ? type // e.g., 'div', 'span' + : (type.displayName as string); + if (type !== ChartStripLines) { + const safeProps: unknown = safeParse(safeStringify(child.props)); + return JSON.stringify({ + type: typeName, + props: safeProps + }); + } + const childProps: Record = child.props as Record; + const stripLineProps: Record = { ...childProps }; + if (childProps.children) { + const stripChildren: { [x: string]: unknown; }[] = React.Children.toArray(childProps.children as React.ReactNode) + .filter((stripChild: React.ReactNode): stripChild is React.ReactElement => + React.isValidElement(stripChild) && + stripChild.type === ChartStripLine) + .map((stripChild: React.ReactElement) => { + const stripChildProps: Record = stripChild.props as Record; + return { ...stripChildProps }; + }); + stripLineProps.processedChildren = stripChildren; + } + const safeProps: unknown = safeParse(safeStringify(stripLineProps)); + return JSON.stringify({ + type: typeName, + props: safeProps + }); + } + return null; +}; + +/** + * Processes strip line configurations from a chart component. + * Extracts and transforms strip line children into proper configuration objects. + * + * @param {React.ReactNode} child - The child element containing strip line configurations. + * @param {ChartStripLineProps[]} defaultStripLines - Default strip line settings to use as fallback. + * @returns {ChartStripLineProps[]} Processed strip line configuration array. + */ +export const processStripLines: ( + child: React.ReactNode, + defaultStripLines: ChartStripLineProps[] +) => ChartStripLineProps[] = ( + child: React.ReactNode, + defaultStripLines: ChartStripLineProps[] +) => { + if (!React.isValidElement(child) || child.type !== ChartStripLines) { + return [...defaultStripLines]; + } + const stripLinesElement: React.ReactElement<{ children?: React.ReactNode }> = child as React.ReactElement<{ + children?: React.ReactNode }>; + const stripLineChildren: React.ReactNode[] = React.Children.toArray(stripLinesElement.props.children || []); + + if (stripLineChildren.length > 0) { + return stripLineChildren + .filter((stripChild: React.ReactNode): stripChild is React.ReactElement => + React.isValidElement(stripChild) && stripChild.type === ChartStripLine) + .map((stripChild: React.ReactElement) => { + const defaultProps: ChartStripLineProps = defaultChartConfigs.StripLines[0]; + const userProps: ChartStripLineProps = stripChild.props as ChartStripLineProps; + const mergedProps: ChartStripLineProps = { + ...defaultProps, + ...userProps, + range: { + ...defaultProps.range, + ...userProps.range + }, + style: { + ...defaultProps.style, + ...userProps.style + }, + text: { + ...defaultProps.text, + ...userProps.text + }, + repeat: { + ...defaultProps.repeat, + ...userProps.repeat + }, + segment: { + ...defaultProps.segment, + ...userProps.segment + } + }; + return mergedProps; + }); + } else { + return [...defaultStripLines]; + } +}; + +/** + * Primary X-Axis component for the chart. + * Renders the vertical axis with customizable properties like labels, grid lines, tick marks, and strip lines. + * A non-rendering component that configures the chart's primary X-axis. + * + * @param {ChartAxisProps} props - The properties for configuring the X-axis + * @returns {null} This component doesn't render any visible elements + */ +export const ChartPrimaryXAxis: React.FC = (props: ChartAxisProps) => { + const context: ChartProviderChildProps = useContext(ChartContext); + const childArray: React.ReactNode[] = React.Children.toArray(props.children); + const childrenPropsSignature: string = childArray + .map((child: React.ReactNode) => processChildElement(child, ChartStripLines)) + .join('|'); // simple delimiter for the string array + const serializedProps: string = useMemo(() => { + const { children, ...rest } = props; + return JSON.stringify(rest); + }, [props]); + useEffect(() => { + const axisProps: Partial = { ...defaultChartConfigs.PrimaryXAxis, ...props }; + let majorGridLines: MajorGridLines = defaultChartConfigs.MajorGridLines; + let minorGridLines: MinorGridLines = defaultChartConfigs.MinorGridLines; + let majorTickLines: MinorTickLines = defaultChartConfigs.MajorTickLines; + let minorTickLines: MinorTickLines = defaultChartConfigs.MinorTickLines; + let labelStyle: ChartAxisLabelProps = defaultChartConfigs.LabelStyle; + let titleStyle: ChartAxisTitleProps = defaultChartConfigs.TitleStyle; + let stripLines: ChartStripLineProps[] = [...defaultChartConfigs.StripLines]; + childArray.forEach((child: React.ReactNode) => { + if (!React.isValidElement(child)) { return; } + const childProps: Record = + child.props as Record; + + if (child.type === ChartMajorGridLines) { + majorGridLines = { + ...defaultChartConfigs.MajorGridLines, + ...childProps + }; + } else if (child.type === ChartMinorGridLines) { + minorGridLines = { + ...defaultChartConfigs.MinorGridLines, + ...childProps + }; + } else if (child.type === ChartMajorTickLines) { + majorTickLines = { + ...defaultChartConfigs.MajorTickLines, + ...childProps + }; + } else if (child.type === ChartMinorTickLines) { + minorTickLines = { + ...defaultChartConfigs.MinorTickLines, + ...childProps + }; + } else if (child.type === ChartAxisLabel) { + labelStyle = { + ...defaultChartConfigs.LabelStyle, + ...childProps + }; + } else if (child.type === ChartAxisTitle) { + titleStyle = { + ...defaultChartConfigs.TitleStyle, + ...childProps + }; + } else if (child.type === ChartStripLines) { + stripLines = processStripLines(child, defaultChartConfigs.StripLines); + } + }); + axisProps.majorGridLines = extend({}, majorGridLines); + axisProps.majorGridLines.width = isNullOrUndefined(axisProps.majorGridLines.width) ? 0 : axisProps.majorGridLines.width; + axisProps.minorGridLines = minorGridLines; + axisProps.majorTickLines = majorTickLines; + axisProps.minorTickLines = minorTickLines; + axisProps.labelStyle = labelStyle; + axisProps.titleStyle = titleStyle; + axisProps.stripLines = stripLines; + axisProps.lineStyle = { ...defaultChartConfigs.PrimaryXAxis.lineStyle, ...props.lineStyle }; + context?.setChartPrimaryXAxis(axisProps as AxisModel); + }, [serializedProps, childrenPropsSignature]); + return null; +}; diff --git a/components/charts/src/chart/chart-axis/PrimaryYAxis.tsx b/components/charts/src/chart/chart-axis/PrimaryYAxis.tsx new file mode 100644 index 0000000..aecd7e0 --- /dev/null +++ b/components/charts/src/chart/chart-axis/PrimaryYAxis.tsx @@ -0,0 +1,101 @@ +import * as React from 'react'; +import { ChartAxisProps, MajorGridLines, MajorTickLines, MinorGridLines, MinorTickLines } from '../base/interfaces'; +import { ChartContext } from '../layout/ChartProvider'; +import { useContext, useEffect, useMemo } from 'react'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartMajorGridLines } from './MajorGridLines'; +import { ChartMinorGridLines } from './MinorGridLines'; +import { ChartMajorTickLines } from './MajorTickLines'; +import { ChartMinorTickLines } from './MinorTickLines'; +import { ChartAxisLabelProps, ChartAxisTitleProps } from './base'; +import { ChartAxisLabel } from './LabelStyle'; +import { ChartAxisTitle } from './TitleStyle'; +import { AxisModel, ChartProviderChildProps } from '../chart-area/chart-interfaces'; +import { ChartStripLines } from './StripLines'; +import { processChildElement, processStripLines } from './PrimaryXAxis'; +import { ChartStripLineProps } from '../base/interfaces'; +import { extend, isNullOrUndefined } from '@syncfusion/react-base'; + +/** + * Primary Y-Axis component for the chart. + * Renders the vertical axis with customizable properties like labels, grid lines, tick marks, and strip lines. + * A non-rendering component that configures the chart's primary Y-axis. + * + * @param {ChartAxisProps} props - The properties for configuring the Y-axis + * @returns {null} This component doesn't render any visible elements + */ +export const ChartPrimaryYAxis: React.FC = (props: ChartAxisProps) => { + const context: ChartProviderChildProps = useContext(ChartContext); + const childArray: React.ReactNode[] = React.Children.toArray(props.children); + const childrenPropsSignature: string = childArray + .map((child: React.ReactNode) => processChildElement(child, ChartStripLines)) + .join('|'); // simple delimiter for the string array + + const serializedProps: string = useMemo(() => { + const { children, ...rest } = props; + return JSON.stringify(rest); + }, [props]); + + useEffect(() => { + const axisProps: Partial = { ...defaultChartConfigs.PrimaryYAxis, ...props }; + let majorGridLines: MajorGridLines = defaultChartConfigs.MajorGridLines; + let minorGridLines: MinorGridLines = defaultChartConfigs.MinorGridLines; + let majorTickLines: MinorTickLines = defaultChartConfigs.MajorTickLines; + let minorTickLines: MinorTickLines = defaultChartConfigs.MinorTickLines; + let labelStyle: ChartAxisLabelProps = defaultChartConfigs.LabelStyle; + let titleStyle: ChartAxisTitleProps = defaultChartConfigs.TitleStyle; + let stripLines: ChartStripLineProps[] = [...defaultChartConfigs.StripLines]; + + childArray.forEach((child: React.ReactNode) => { + if (!React.isValidElement(child)) { return; } + const childProps: Record = + child.props as Record; + + if (child.type === ChartMajorGridLines) { + majorGridLines = { + ...defaultChartConfigs.MajorGridLines, + ...childProps + }; + } else if (child.type === ChartMinorGridLines) { + minorGridLines = { + ...defaultChartConfigs.MinorGridLines, + ...childProps + }; + } else if (child.type === ChartMajorTickLines) { + majorTickLines = { + ...defaultChartConfigs.MajorTickLines, + ...childProps + }; + } else if (child.type === ChartMinorTickLines) { + minorTickLines = { + ...defaultChartConfigs.MinorTickLines, + ...childProps + }; + } else if (child.type === ChartAxisLabel) { + labelStyle = { + ...defaultChartConfigs.LabelStyle, + ...childProps + }; + } else if (child.type === ChartAxisTitle) { + titleStyle = { + ...defaultChartConfigs.TitleStyle, + ...childProps + }; + } else if (child.type === ChartStripLines) { + stripLines = processStripLines(child, defaultChartConfigs.StripLines); + } + }); + axisProps.majorGridLines = extend({}, majorGridLines); + axisProps.majorGridLines.width = isNullOrUndefined(axisProps.majorGridLines.width) ? 1 : axisProps.majorGridLines.width; + axisProps.minorGridLines = minorGridLines; + axisProps.majorTickLines = majorTickLines; + axisProps.minorTickLines = minorTickLines; + axisProps.labelStyle = labelStyle; + axisProps.titleStyle = titleStyle; + axisProps.stripLines = stripLines; + axisProps.lineStyle = { ...defaultChartConfigs.PrimaryYAxis.lineStyle, ...props.lineStyle }; + context?.setChartPrimaryYAxis(axisProps as AxisModel); + }, [serializedProps, childrenPropsSignature]); + + return null; +}; diff --git a/components/charts/src/chart/chart-axis/Rows.tsx b/components/charts/src/chart/chart-axis/Rows.tsx new file mode 100644 index 0000000..47c7a7a --- /dev/null +++ b/components/charts/src/chart/chart-axis/Rows.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { Row } from '../base/interfaces'; +import { useContext, useEffect, useRef } from 'react'; +import { ChartContext } from '../layout/ChartProvider'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartProviderChildProps, RowProps } from '../chart-area/chart-interfaces'; + +/** + * Props for the ChartRows component. + * Contains optional children which are expected to be ChartRow components. + */ +interface RowsProps { + children?: React.ReactNode; +} + +/** + * ChartRows component for configuring multiple row definitions within the chart layout. + * It collects all valid ChartRow children, merges them with default configurations, + * and updates the chart context accordingly. + * + * @param {RowsProps} props - Props containing ChartRow children. + * @returns {null} This component does not render any visible output. + */ +export const ChartRows: React.FC = (props: RowsProps) => { + const context: ChartProviderChildProps = useContext(ChartContext); + const previousRowsRef: React.RefObject = useRef([]); + useEffect(() => { + const rowArray: Row[] = React.Children.map(props.children, (child: React.ReactNode) => { + if (React.isValidElement(child) && child.type === ChartRow) { + return { + ...defaultChartConfigs.Row, + ...child.props + }; + } + return null; + })?.filter((row: Row): row is RowProps => row !== null) as Row[]; + previousRowsRef.current = rowArray; + context?.setChartRows(rowArray); + }, [props.children]); + + return null; +}; + +/** + * ChartRow component representing a single row configuration within the chart layout. + * This component is used as a child of ChartRows and does not render any output directly. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartRow: React.FC = () => { + return null; +}; diff --git a/components/charts/src/chart/chart-axis/StripLines.tsx b/components/charts/src/chart/chart-axis/StripLines.tsx new file mode 100644 index 0000000..ec749b4 --- /dev/null +++ b/components/charts/src/chart/chart-axis/StripLines.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { StripLineProps } from '../chart-area/chart-interfaces'; +import { ChartStripLineProps } from '../base/interfaces'; + +/** + * ChartStripLines component for configuring multiple strip lines in the chart. + * This is a configuration-only component and does not render any visual output. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartStripLines: React.FC = () => { + return null; +}; + +/** + * ChartStripLine component for configuring a single strip line in the chart. + * This is a configuration-only component and does not render any visual output. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartStripLine: React.FC = () => { + return null; +}; + +ChartStripLine.displayName = 'ChartStripLine'; +ChartStripLines.displayName = 'ChartStripLines'; diff --git a/components/charts/src/chart/chart-axis/TitleStyle.tsx b/components/charts/src/chart/chart-axis/TitleStyle.tsx new file mode 100644 index 0000000..57629d7 --- /dev/null +++ b/components/charts/src/chart/chart-axis/TitleStyle.tsx @@ -0,0 +1,14 @@ +import { ChartAxisTitleProps } from './base'; + + +/** + * ChartAxisTitle component for configuring the style of axis titles in the chart. + * This is a configuration-only component and does not render any visual output. + * + * @returns {null} This component does not render any visible output. + */ +export const ChartAxisTitle: React.FC = () => { + return null; +}; + +ChartAxisTitle.displayName = 'TitleStyle'; diff --git a/components/charts/src/chart/chart-axis/base.tsx b/components/charts/src/chart/chart-axis/base.tsx new file mode 100644 index 0000000..070c51f --- /dev/null +++ b/components/charts/src/chart/chart-axis/base.tsx @@ -0,0 +1,291 @@ +import { ChartFontProps } from '../base/interfaces'; +import { AxisLabelPosition, EdgeLabelPlacement, LabelIntersectMode, LabelPlacement, TextOverflow } from '../base/enum'; +import { HorizontalAlignment } from '@syncfusion/react-base'; + + +/** + * Interface for a class Font. + * + * @private + */ +export interface AxisTextStyle { + + /** + * Specifies the style of the text. + * + * @default 'Normal' + */ + fontStyle?: string; + + /** + * Specifies the size of the text. + * + * @default '16px' + */ + fontSize?: string; + + /** + * Specifies the font weight of the text. + * + * @default 'Normal' + */ + fontWeight?: string; + + /** + * Specifies the color of the text. + * + * @default '' + */ + color?: string; + + /** + * Specifies the alignment of the text. + * + * @default 'Center' + */ + textAlign?: HorizontalAlignment; + + /** + * Specifies the font family for the text. + */ + fontFamily?: string; + + /** + * Specifies the opacity level for the text. + * + * @default 1 + */ + opacity?: number; + + /** + * Specifies how the chart title text should handle overflow. + * + * @default 'Wrap' + */ + textOverflow?: TextOverflow; +} + +/** + * Interface for dateFormatOptions + * + * @private + */ +export interface DateFormatOptions { + /** + * Specifies the skeleton for date formatting. + */ + skeleton?: string; + /** + * Specifies the type of date formatting either date, dateTime or time. + */ + type?: string; + /** + * Specifies custom date formatting to be used. + */ + format?: string; + /** + * Specifies the calendar mode other than gregorian + */ + calendar?: string; + /** + * Enable server side date formating. + */ + isServerRendered?: boolean; + /** + * Determines the locale of the date formatting. + */ + locale?: string; +} + +/** + * Defines the style settings for axis labels in a chart. + * Extends basic font properties and includes additional customization options. + */ +export interface ChartAxisLabelProps extends ChartFontProps { + + /** + * Specifies the angle in degrees to rotate the axis label text. + * A positive value rotates the label clockwise, and a negative value rotates it counterclockwise. + * + * @default 0 + */ + rotationAngle?: number; + + /** + * Used to format the axis label. This property accepts global string formats such as `C`, `n1`, `P`, etc. + * It also accepts placeholders like `{value}°C`, where `{value}` represents the axis label (e.g., 20°C). + * + * @default '' + */ + format?: string; + + /** + * Specifies the skeleton format used for processing date-time values. + * + * @default '' + */ + skeleton?: string; + + /** + * The `padding` property adjusts the distance to ensure a clear space between the axis labels and the axis line. + * + * @default 5 + */ + padding?: number; + + /** + * The `position` property determines where the axis labels are rendered in relation to the axis line. + * Available options are: + * - `Inside`: Renders the labels inside the axis line. + * - `Outside`: Renders the labels outside the axis line. + * + * @default 'Outside' + */ + position?: AxisLabelPosition; + + /** + * The `placement` property controls where the category axis labels are rendered in relation to the axis ticks. + * Available options are: + * - `BetweenTicks`: Renders the label between the axis ticks. + * - `OnTicks`: Renders the label directly on the axis ticks. + * + * @default 'BetweenTicks' + */ + placement?: LabelPlacement; + + /** + * Specifies the action to take when axis labels intersect with each other. + * The available options are: + * - `None`: Shows all labels without any modification. + * - `Hide`: Hides the label if it intersects with another label. + * - `Trim`: Trims the label text to fit within the available space. + * - `Wrap`: Wraps the label text to fit within the available space. + * - `MultipleRows`: Displays the label text in multiple rows to avoid intersection. + * - `Rotate45`: Rotates the label text by 45 degrees to avoid intersection. + * - `Rotate90`: Rotates the label text by 90 degrees to avoid intersection. + * + * @default Trim + */ + intersectAction?: LabelIntersectMode; + + /** + * When set to `true`, axis labels will automatically wrap to fit within the width defined by `maxLabelWidth`. + * This helps maintain readability by preventing label overflow, especially when labels are long or the chart has limited space. + * + * @default false + */ + enableWrap?: boolean; + + /** + * When set to `true`, axis labels are trimmed based on the `maxLabelWidth` setting. + * This helps prevent label overflow and ensures a cleaner layout, especially when labels are long or space is limited on the chart. + * + * @default false + */ + enableTrim?: boolean; + + /** + * Specifies the maximum width of an axis label. + * + * @default 34 + */ + maxLabelWidth?: number; + + /** + * The `edgeLabelPlacement` property ensures that labels positioned at the edges of the axis do not overlap with the axis boundaries or other chart elements, offering several options to improve chart readability by managing edge labels effectively. + * Available options are: + * - `None`: No action will be performed on edge labels. + * - `Hide`: Edge labels will be hidden to prevent overlap. + * - `Shift`: Edge labels will be shifted to fit within the axis bounds without overlapping. + * + * @default 'Shift' + */ + edgeLabelPlacement?: EdgeLabelPlacement; + + /** + * Determines the alignment of the text within its container. + * + * Available options: + * - `Left`: Aligns the text to the left. + * - `Center`: Aligns the text to the center. + * - `Right`: Aligns the text to the right. + * + * @default 'Center' + */ + align?: HorizontalAlignment; + + /** + * A callback function that allows for custom rendering of axis labels. + * This function is invoked for each axis label and receives the label's properties as an argument. + * Available arguments are: + * - `value`- The numeric value of the axis label. + * - `text` - The current formatted text of the axis label. + * + * @param {number} value - The numeric value of the axis label. + * @param {string} text - The current formatted text of the axis label. + * @default null + */ + formatter?: (value: number, text: string) => string | boolean; +} + +/** + * A function type that defines how to customize the content of an axis label. + * This function receives the label value and current text, and returns the modified text. + * + * @param {number} value - The numeric value of the axis label. + * @param {string} text - The current formatted text of the axis label. + * @returns {string} The custom text to display for the axis label. + * @private + */ +export type AxisLabelContentFunction = (value: number, text: string) => string | boolean; + +/** + * Defines the style settings for the axis title in a chart. + * Extends basic font properties and includes customization for padding and rotation. + */ +export interface ChartAxisTitleProps extends ChartFontProps { + /** + * Specifies the text content of the axis title. + * + * @default '' + */ + text?: string; + + /** + * Specifies the padding between the axis title and the axis labels. + * + * @default 5 + */ + padding?: number; + + /** + * Defines an angle for rotating the axis title. By default, the angle is calculated based on the position and orientation of the axis. + * + * @default null + */ + rotationAngle?: number; + + /** + * Determines the alignment of the axis title within its container. + * + * Available options: + * - `Left`: Aligns the axis title to the left. + * - `Center`: Aligns the axis title to the center. + * - `Right`: Aligns the axis title to the right. + * + * @default 'Center' + */ + align?: HorizontalAlignment; + + /** + * Controls how the axis title behaves when it overflows its container. + * + * Available options: + * - `Wrap`: Wraps the axis title to the next line. + * - `Trim`: Trims the overflowed axis title. + * - `None`: Displays the axis title even if it overlaps other elements. + * + * @default 'Wrap' + */ + overflow?: TextOverflow; +} diff --git a/components/charts/src/chart/chart-legend/ChartLegend.tsx b/components/charts/src/chart/chart-legend/ChartLegend.tsx new file mode 100644 index 0000000..3924d1b --- /dev/null +++ b/components/charts/src/chart/chart-legend/ChartLegend.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { ChartContext } from '../layout/ChartProvider'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartLegendProps } from '../base/interfaces'; +import { ChartProviderChildProps } from '../chart-area/chart-interfaces'; + +/** + * React component that configures the legend appearance and behavior for the chart. + * This component doesn't render any UI elements but manages legend configuration through context. + * + * @param {ChartLegendProps} props - Legend configuration properties. + * @returns {null} This component doesn't render any elements. + */ +export const ChartLegend: React.FC = (props: ChartLegendProps) => { + const context: ChartProviderChildProps = React.useContext(ChartContext); + + // Memoize the merged legend config + const legendConfig: ChartLegendProps = React.useMemo(() => ({ + ...defaultChartConfigs.ChartLegend, + ...props + }), [ + props.visible, props.height, props.width, props.location, props.position, props.padding, + props.itemPadding, props.align, props.textStyle, props.shapeHeight, props.shapeWidth, props.border, + props.margin, props.containerPadding, props.shapePadding, props.background, props.opacity, + props.toggleVisibility, props.title, props.titleStyle, + props.maxTitleWidth, props.maxLabelWidth, props.enablePages, + props.inversed, props.reverse, props.fixedWidth, props.accessibility, props.titleAlign + ]); + + // Only update context when legendConfig changes + React.useEffect(() => { + context?.setChartLegend(legendConfig); + }, [legendConfig]); + + return null; +}; diff --git a/components/charts/src/chart/chart-tooltip/ChartTooltip.tsx b/components/charts/src/chart/chart-tooltip/ChartTooltip.tsx new file mode 100644 index 0000000..41848d5 --- /dev/null +++ b/components/charts/src/chart/chart-tooltip/ChartTooltip.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { useContext, useEffect } from 'react'; +import { ChartContext } from '../layout/ChartProvider'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartTooltipProps } from '../base/interfaces'; +import { ChartProviderChildProps } from '../chart-area/chart-interfaces'; + +/** + * ChartTooltip component for configuring and setting the tooltip behavior in the chart. + * + * @param {ChartTooltipProps} props - Props used to customize the chart tooltip. + * @returns {null} This component does not render any visible output. + */ +export const ChartTooltip: React.FC = (props: ChartTooltipProps) => { + const context: ChartProviderChildProps = useContext(ChartContext); + + /** + * Updates the chart tooltip configuration in the shared chart context + * whenever relevant props change. + */ + useEffect(() => { + void(context && context.setChartTooltip && context.setChartTooltip({ + ...defaultChartConfigs.ChartTooltip, + ...props + })); + }, [ + props.enable, + props.showMarker, + props.shared, + props.fill, + props.location, + props.headerText, + props.opacity, + props.format, + props.border, + props.textStyle, + props.fadeOutMode, + props.enableAnimation, + props.duration, + props.fadeOutDuration, + props.showNearestPoint, + props.showHeaderLine, + props.showNearestTooltip + ]); + + // This component doesn't render anything directly + return null; +}; + +// Set a display name for the component +ChartTooltip.displayName = 'ChartTooltip'; diff --git a/components/charts/src/chart/common/base.tsx b/components/charts/src/chart/common/base.tsx new file mode 100644 index 0000000..fc292a7 --- /dev/null +++ b/components/charts/src/chart/common/base.tsx @@ -0,0 +1,117 @@ +import { LayoutMap } from '../layout/LayoutContext'; + + +/** + * Defines animation settings for chart series in a React component. + */ +export interface Animation { + + /** + * When set to `false`, animation is disabled during the initial rendering of the chart series. + * + * @default true + */ + enable: boolean; + + /** + * Duration of the animation in milliseconds. + * Controls how long the animation effect lasts. + * + * @default 1000 + */ + duration?: number; + + /** + * Delay before the animation starts, in milliseconds. + * Useful for sequencing animations or staggering effects. + * + * @default 0 + */ + delay?: number; +} + + + +/** + * Represents a single SVG path command used in rendering chart elements. + * + * @private + */ +export interface PathCommand { + /** + * The type of path command (e.g., 'M', 'L', 'C', etc.). + */ + type: string; + + /** + * The numeric parameters associated with the path command. + */ + params: number[]; +} + +/** + * Defines the context type for layout-related operations in the charting system. + * @private + */ +export interface LayoutContextType { + /** + * Updates layout-related values by key. + * + * @param key - The identifier for the layout value. + * @param values - A partial object containing the new layout values. + */ + setLayoutValue: (key: string, values: Partial) => void; + + /** + * Indicates the current phase of layout processing. + * Can be either 'measuring' or 'rendering'. + */ + phase: 'measuring' | 'rendering'; + + /** + * Triggers a re-measurement of layout dimensions and properties. + */ + triggerRemeasure: () => void; + + /** + * Reports that a specific layout measurement has been completed. + * + * @param key - The identifier of the measured layout item. + */ + reportMeasured: (key: string) => void; + + /** + * A mutable reference to the layout map, containing chart layout data. + */ + layoutRef: React.MutableRefObject; + + /** + * Optional flag to disable chart animations. + */ + disableAnimation?: boolean; + + /** + * Optional setter to update the animation disable flag. + * + * @param val - Boolean value to enable or disable animations. + */ + setDisableAnimation?: (val: boolean) => void; + + /** + * The available size for layout rendering, including width and height. + */ + availableSize: { width: number, height: number }; + + /** + * Indicates the current progress of chart animations (0 to 1). + */ + animationProgress: number; + + /** + * Updates the animation progress value. + * + * @param progress - A number between 0 and 1 representing animation progress. + */ + setAnimationProgress: (progress: number) => void; +} + diff --git a/components/charts/src/chart/common/data.tsx b/components/charts/src/chart/common/data.tsx new file mode 100644 index 0000000..abd5598 --- /dev/null +++ b/components/charts/src/chart/common/data.tsx @@ -0,0 +1,45 @@ +import { Query, DataManager } from '@syncfusion/react-data'; + +/** + * Custom hook to initialize a DataManager and Query based on the provided data source and query. + * + * @param {Object | DataManager} dataSource - The data source object or an instance of DataManager. + * @param {Query} query - An optional instance of Query to be used with the data source. + * @returns {Object} An object containing the following: + * - dataManager {DataManager} - The initialized DataManager instance. + * - query {Query} - The initialized Query instance. + * - generateQuery {Function} - Function to generate a new Query. + * - getData {Function} - Function to execute a query and get data. + */ +export const useData: (dataSource?: Object | DataManager, query?: Query) => { + dataManager: DataManager; + query: Query; + generateQuery: () => Query; + getData: (dataQuery: Query) => Promise; +} = (dataSource?: Object | DataManager, query?: Query) => { + const initDataManager: (dataSource: Object | DataManager, query: Query) => { + dataManager: DataManager; + dataQuery: Query; + } = (dataSource: Object | DataManager, query: Query) => { + const dataManager: DataManager = dataSource instanceof DataManager ? dataSource : new DataManager(dataSource); + const dataQuery: Query = query instanceof Query ? query : new Query(); + return { dataManager, dataQuery }; + }; + + const { dataManager, dataQuery } = initDataManager(dataSource || {}, query || new Query()); + + const generateQuery: () => Query = (): Query => { + return dataQuery.clone(); + }; + + const getData: (dataQuery: Query) => Promise = (dataQuery: Query): Promise => { + return dataManager.executeQuery(dataQuery); + }; + + return { + dataManager, + query: dataQuery, + generateQuery, + getData + }; +}; diff --git a/components/charts/src/chart/hooks/useClipRect.tsx b/components/charts/src/chart/hooks/useClipRect.tsx new file mode 100644 index 0000000..7d1e78d --- /dev/null +++ b/components/charts/src/chart/hooks/useClipRect.tsx @@ -0,0 +1,394 @@ +import { JSX, useEffect, useState } from 'react'; +import { Chart, Rect } from '../chart-area/chart-interfaces'; +const clipRectRef: { current: null | ((clipRect: Rect) => void); } = +{ + current: null as null | ((clipRect: Rect) => void) +}; + +export const useRegisterClipRectSetter: () => (fn: (clipRect: Rect) => void) => void = () => { + return (fn: (clipRect: Rect) => void) => { + clipRectRef.current = fn; + }; +}; + +export const useClipRectSetter: () => ((clipRect: Rect) => void) | null = () => { + return clipRectRef.current; +}; + +// For axis render version +interface VersionInfo { + version: number; + id: string; +} + +const axisRenderVersions: {[chartId: string]: number} = {}; +let listeners: ((info: VersionInfo) => void)[] = []; + +export const useRegisterAxisRender: () => (chartId?: string) => void = () => { + return (chartId?: string) => { + const id: string = chartId || 'default'; + + if (!axisRenderVersions[id as string]) { + axisRenderVersions[id as string] = 0; + } + + axisRenderVersions[id as string]++; + listeners.forEach((fn: (info: VersionInfo) => void) => fn({version: axisRenderVersions[id as string], id})); + }; +}; + +export const useAxisRenderVersion: (chartId?: string) => VersionInfo = (chartId?: string) => { + const id: string = chartId || 'default'; + + if (axisRenderVersions[id as string] === undefined) { + axisRenderVersions[id as string] = 0; + } + + const [versionInfo, setVersionInfo] = useState({ + version: axisRenderVersions[id as string] || 0, + id + }); + + useEffect(() => { + const updateVersion: (info: VersionInfo) => void = (info: VersionInfo) => { + // Update when any version changes - components can check the ID + setVersionInfo(info); + }; + + listeners.push(updateVersion); + return () => { + listeners = listeners.filter((fn: (info: VersionInfo) => void) => fn !== updateVersion); + }; + }, []); + + return versionInfo; +}; + +// For series render version +const seriesRenderVersions: {[chartId: string]: number} = {}; +let seriesListeners: ((info: VersionInfo) => void)[] = []; + +export const useRegisterSeriesRender: () => (chartId?: string) => void = () => { + return (chartId?: string) => { + const id: string = chartId || 'default'; + + if (!seriesRenderVersions[id as string]) { + seriesRenderVersions[id as string] = 0; + } + + seriesRenderVersions[id as string]++; + seriesListeners.forEach((fn: (info: VersionInfo) => void) => fn({version: seriesRenderVersions[id as string], id})); + }; +}; + +export const useSeriesRenderVersion: (chartId?: string) => VersionInfo = (chartId?: string) => { + const id: string = chartId || 'default'; + + if (seriesRenderVersions[id as string] === undefined) { + seriesRenderVersions[id as string] = 0; + } + + const [versionInfo, setVersionInfo] = useState({ + version: seriesRenderVersions[id as string] || 0, + id + }); + + useEffect(() => { + const updateVersion: (info: VersionInfo) => void = (info: VersionInfo) => { + // Update when any version changes - components can check the ID + setVersionInfo(info); + }; + + seriesListeners.push(updateVersion); + return () => { + seriesListeners = seriesListeners.filter((fn: (info: VersionInfo) => void) => fn !== updateVersion); + }; + }, []); + + return versionInfo; +}; + +let zoomToolkitVersion: number = 0; +let zoomToolkitListeners: ((version: number) => void)[] = []; + +export const useRegisterZoomToolkitVisibility: () => () => void = () => { + return () => { + zoomToolkitVersion++; + zoomToolkitListeners.forEach((fn: (version: number) => void) => fn(zoomToolkitVersion)); + }; +}; + +export const useZoomToolkitVisibility: () => number = (): number => { + const [version, setVersion] = useState(zoomToolkitVersion); + + useEffect(() => { + zoomToolkitListeners.push(setVersion); + return () => { + zoomToolkitListeners = zoomToolkitListeners.filter((fn: (version: number) => void) => fn !== setVersion); + }; + }, []); + + return version; +}; + +/** + * Represents a generic event handler for chart interactions. + * + * @param {Event} e - The DOM event object triggered by the user interaction. + * @param {Chart} chart - The chart instance associated with the event. + * @param args - Additional optional arguments that may include: + * - `number`, `string`, `boolean`, `null`, or `undefined` + * - An object with optional properties: + * - `x`: The x-coordinate related to the event. + * - `y`: The y-coordinate related to the event. + * - `targetId`: Identifier of the target element involved in the event. + * + * @private + */ +export type ChartEventHandler = ( + e: Event, + chart: Chart, + ...args: (number | string | boolean | null | undefined | { x?: number; y?: number; targetId?: string })[] +) => void; +type EventType = 'click' | 'mouseMove' | 'mouseDown' | 'mouseUp' | 'mouseWheel' | 'mouseLeave'; + +interface EventHandlers { + [key: string]: ChartEventHandler[]; +} + +// Initialize with chart-specific event types +const chartEventHandlers: {[chartId: string]: EventHandlers} = {}; + + +/** + * Registers an event handler for a specific chart event type. + * + * @param {EventType} eventType - The type of chart event to listen for (e.g., 'click', 'hover'). + * @param {ChartEventHandler} handler - The function to handle the event when triggered. + * @param {string} chartId - Optional identifier for the chart instance. Defaults to 'default' if not provided. + * @returns {void} A function that can be called to unregister the event handler. + * @private + */ +export function registerChartEventHandler( + eventType: EventType, + handler: ChartEventHandler, + chartId?: string +): () => void { + const id: string = chartId || 'default'; + + // Initialize handlers for this chart ID if they don't exist + if (!chartEventHandlers[id as string]) { + chartEventHandlers[id as string] = { + click: [], + mouseDown: [], + mouseMove: [], + mouseUp: [], + mouseWheel: [], + mouseLeave: [] + }; + } + + if (Object.prototype.hasOwnProperty.call(chartEventHandlers[id as string], eventType)) { + chartEventHandlers[id as string][eventType as string].push(handler); + return () => { + if (chartEventHandlers[id as string] && Object.prototype.hasOwnProperty.call(chartEventHandlers[id as string], eventType)) { + chartEventHandlers[id as string][eventType as string] = + chartEventHandlers[id as string][eventType as string].filter((h: ChartEventHandler) => h !== handler); + } + }; + } + return () => { /* No-op if eventType is invalid */ }; +} + +/** + * Defines the value type for chart event, loaction and target. + * @private + */ +export type ChartEventArg = number | string | boolean | null | undefined; + +/** + * Represents a location in chart coordinate space. + * @private + */ +export type ChartLocationArg = { x: number; y: number }; + +/** + * Represents a reference to a chart target element. + * @private + */ +export type ChartTargetArg = { targetId: string }; + +/** + * Calls all registered handlers for a specific event type + * + * @param {EventType} eventType - Type of event to trigger + * @param {MouseEvent|TouchEvent|WheelEvent} e - The event object + * @param {Chart} chart - The chart instance + * @param {...any} args - Additional arguments to pass to handlers + * @returns {void} + * @private + */ +export function callChartEventHandlers( + eventType: EventType, + e: MouseEvent | TouchEvent | WheelEvent, + chart: Chart, + ...args: (ChartEventArg | ChartLocationArg | ChartTargetArg)[] +): void { + const chartId: string = chart && chart.element ? chart.element.id : 'default'; + + // Call handlers registered for this specific chart + if (chartEventHandlers[chartId as string] && Object.prototype.hasOwnProperty.call(chartEventHandlers[chartId as string], eventType)) { + for (const handler of chartEventHandlers[chartId as string][eventType as string]) { + handler(e, chart, ...args); + } + } + + // Also call default handlers for backward compatibility + if (chartId !== 'default' && chartEventHandlers['default'] && + Object.prototype.hasOwnProperty.call(chartEventHandlers['default'], eventType)) { + for (const handler of chartEventHandlers['default'][eventType as string]) { + handler(e, chart, ...args); + } + } +} + +// Shorthand for zoom rect setter (keeping for backward compatibility) +let zoomRectSetter: ((rect: JSX.Element | null) => void) | null = null; +/** + * Registers a function to set the zoom rectangle + * + * @param {Function} setter - Function to set the zoom rectangle + * @returns {void} + * @private + */ +export function registerZoomRectSetter(setter: (rect: JSX.Element | null) => void): void { + zoomRectSetter = setter; +} + +/** + * Returns the current zoom rectangle setter + * + * @returns {Function|null} The current zoom rectangle setter or null + * @private + */ +export function getZoomRectSetter(): ((rect: JSX.Element | null) => void) | null { + return zoomRectSetter; +} + +const axisVersion: { [chartId: string]: number } = {}; +let axisListeners: ((info: VersionInfo) => void)[] = []; + +export const useRegisterAxesRender: () => (chartId?: string) => void = () => { + return (chartId?: string) => { + const id: string = chartId || 'default'; + + if (!axisVersion[id as string]) { + axisVersion[id as string] = 0; + } + + axisVersion[id as string]++; + axisListeners.forEach((fn: (info: VersionInfo) => void) => fn({ version: axisVersion[id as string], id })); + }; +}; + +export const useAxesRendereVersion: (chartId?: string) => VersionInfo = (chartId?: string) => { + const id: string = chartId || 'default'; + + if (axisVersion[id as string] === undefined) { + axisVersion[id as string] = 0; + } + + const [versionInfo, setVersionInfo] = useState({ + version: axisVersion[id as string] || 0, + id + }); + + useEffect(() => { + const updateVersion: (info: VersionInfo) => void = (info: VersionInfo) => { + // Update when any version changes - components can check the ID + setVersionInfo(info); + }; + + axisListeners.push(updateVersion); + return () => { + axisListeners = axisListeners.filter((fn: (info: VersionInfo) => void) => fn !== updateVersion); + }; + }, []); + + return versionInfo; +}; + +const legendShapeVersions: {[chartId: string]: number} = {}; +let legendShapeListeners: ((info: VersionInfo) => void)[] = []; + +/** + * Hook to register legend shape change trigger function + * + * @returns {Function} Function to trigger legend shape updates + */ +export const useRegisterLegendShapeRender: () => (chartId?: string) => void = () => { + return (chartId?: string) => { + const id: string = chartId || 'default'; + + if (!legendShapeVersions[id as string]) { + legendShapeVersions[id as string] = 0; + } + + legendShapeVersions[id as string]++; + legendShapeListeners.forEach((fn: (info: VersionInfo) => void) => fn({version: legendShapeVersions[id as string], id})); + }; +}; + +/** + * Hook to subscribe to legend shape changes + * + * @param {string} [chartId] - Optional chart ID + * @returns {VersionInfo} The current version info for legend shape changes + */ +export const useLegendShapeRenderVersion: (chartId?: string) => VersionInfo = (chartId?: string): VersionInfo => { + const id: string = chartId || 'default'; + + if (legendShapeVersions[id as string] === undefined) { + legendShapeVersions[id as string] = 0; + } + + const [versionInfo, setVersionInfo] = useState({ + version: legendShapeVersions[id as string] || 0, + id + }); + + useEffect(() => { + const updateVersion: (info: VersionInfo) => void = (info: VersionInfo) => { + setVersionInfo(info); + }; + + legendShapeListeners.push(updateVersion); + return () => { + legendShapeListeners = legendShapeListeners.filter((fn: (info: VersionInfo) => void) => fn !== updateVersion); + }; + }, []); + + return versionInfo; +}; + +let axisOutSideVersion: number = 0; +let axisOutsideListeres: ((v: number) => void)[] = []; +export const useRegisterAxieOutsideRender: () => () => void = () => { + return () => { + axisOutSideVersion++; + axisOutsideListeres.forEach((fn: (v: number) => void) => fn(axisOutSideVersion)); + }; +}; + +export const useAxisOutsideRendereVersion: () => number = (): number => { + const [version, setVersion] = useState(axisOutSideVersion); + + useEffect(() => { + axisOutsideListeres.push(setVersion); + return () => { + axisOutsideListeres = axisOutsideListeres.filter((fn: (v: number) => void) => fn !== setVersion); + }; + }, []); + + return version; +}; diff --git a/components/charts/src/chart/hooks/useDeepCompare.tsx b/components/charts/src/chart/hooks/useDeepCompare.tsx new file mode 100644 index 0000000..588d447 --- /dev/null +++ b/components/charts/src/chart/hooks/useDeepCompare.tsx @@ -0,0 +1,140 @@ +import { useRef } from 'react'; +import { ChartBorderProps, ChartSeriesProps } from '../base/interfaces'; + +/** + * Type definition for comparable values that can be deeply compared + */ +type ComparableValue = + | string + | number + | boolean + | null + | undefined + | Date + | ChartBorderProps + | ComparableValue[] + | { [key: string]: ComparableValue }; + +/** + * Deep equality comparison function for objects + * More performant than using JSON.stringify for dependency tracking + * + * @param {ComparableValue} objOne - First object to compare + * @param {ComparableValue} objTwo - Second object to compare + * @returns {boolean} True if objects are deeply equal + * @private + */ +export function isEqual(objOne: ComparableValue, objTwo: ComparableValue): boolean { + // Handle primitives + if (objOne === objTwo) {return true; } + + // If either is null or not an object, they're not equal + if (objOne == null || objTwo == null || typeof objOne !== 'object' || typeof objTwo !== 'object') { + return false; + } + + // Handle arrays + if (Array.isArray(objOne) && Array.isArray(objTwo)) { + if (objOne.length !== objTwo.length) {return false; } + + for (let i: number = 0; i < objOne.length; i++) { + if (!isEqual(objOne[i as number], objTwo[i as number])) {return false; } + } + + return true; + } + + // Handle objects + const keysOne: string[] = Object.keys(objOne); + const keysTwo: string[] = Object.keys(objTwo); + + if (keysOne.length !== keysTwo.length) {return false; } + + for (const key of keysOne) { + if (!Object.prototype.hasOwnProperty.call(objTwo, key)) {return false; } + if (!isEqual((objOne as Record)[key as string], + (objTwo as Record)[key as string])) {return false; } + } + + return true; +} + + +/** + * A hook that uses deep comparison to memoize a value + * Constrains the generic type to extend ComparableValue for type safety + * + * @param {T} value - The value to memoize + * @returns {T} The memoized value + * @private + */ +export function useDeepCompare(value: T): T { + const ref: React.RefObject = useRef(value); + + if (!isEqual(value, ref.current)) { + ref.current = value; + } + + return ref.current; +} + +/** + * Custom hook for efficiently tracking data source changes in chart series + * Replaces JSON.stringify for data sources in dependency arrays + * + * @param {ChartSeriesProps[]} seriesList - List of chart series + * @returns {ComparableValue[]} Stable reference that only changes when data sources change + * @private + */ +export function useStableDataSources(seriesList: ChartSeriesProps[]): ComparableValue[] { + return useDeepCompare( + seriesList.map((series: ChartSeriesProps) => series.dataSource as ComparableValue) + ); +} + +/** + * Custom hook for efficiently tracking marker property changes + * Replaces JSON.stringify for marker properties in dependency arrays + * + * @param {ChartSeriesProps[]} series - List of chart series properties + * @returns {ComparableValue[]} Array of marker properties that only changes when marker properties change + * @private + */ +export function useStableMarkerProps(series: ChartSeriesProps[]): ComparableValue[] { + return useDeepCompare( + series.map((s: ChartSeriesProps) => s.marker as ComparableValue) + ); +} + + +/** + * Custom hook for efficiently tracking data label property changes + * Replaces JSON.stringify for data label properties in dependency arrays + * + * @param {ChartSeriesProps[]} series - List of chart series properties + * @returns {ComparableValue[]} Stable reference that only changes when data label properties change + * @private + */ +export function useStableDataLabelProps(series: ChartSeriesProps[]): ComparableValue[] { + return useDeepCompare( + series.map((s: ChartSeriesProps) => { + const { ...safeDataLabel } = s.marker?.dataLabel || {}; + return safeDataLabel as ComparableValue; + }) + ); +} + +/** + * Custom hook for efficiently tracking general series property changes + * Replaces JSON.stringify for series properties in dependency arrays + * + * @param {ChartSeriesProps[]} processedSeriesData - Processed series data for change detection + * @returns {ComparableValue} Stable reference that only changes when series properties change + * @private + */ +export function useStableSeriesProps(processedSeriesData: ChartSeriesProps[]): ComparableValue { + return useDeepCompare(processedSeriesData as ComparableValue); +} + + + diff --git a/components/charts/src/chart/index.ts b/components/charts/src/chart/index.ts new file mode 100644 index 0000000..68f331d --- /dev/null +++ b/components/charts/src/chart/index.ts @@ -0,0 +1,25 @@ +export * from './Chart'; +export * from './chart-area/ChartArea'; +export * from './chart-area/ChartTitle'; +export * from './chart-area/ChartSubTitle'; +export * from './chart-area/ChartStackLabels'; +export * from './chart-axis/Rows'; +export * from './chart-axis/Columns'; +export * from './chart-axis/MajorGridLines'; +export * from './chart-axis/MinorGridLines'; +export * from './chart-axis/MajorTickLines'; +export * from './chart-axis/MinorTickLines'; +export * from './chart-axis/PrimaryXAxis'; +export * from './chart-axis/PrimaryYAxis'; +export * from './chart-axis/LabelStyle'; +export * from './chart-legend/ChartLegend'; +export * from './series/Series'; +export * from './chart-axis/TitleStyle'; +export * from './chart-axis/ChartAxes'; +export * from './chart-tooltip/ChartTooltip'; +export * from './series/Marker'; +export * from './series/DataLabel'; +export * from './zooming/ChartZooming'; +export * from './base/interfaces'; +export * from './base/enum'; +export * from './chart-axis/StripLines'; diff --git a/components/charts/src/chart/layout/ChartProvider.tsx b/components/charts/src/chart/layout/ChartProvider.tsx new file mode 100644 index 0000000..1f51711 --- /dev/null +++ b/components/charts/src/chart/layout/ChartProvider.tsx @@ -0,0 +1,155 @@ +import { createContext, useCallback, useMemo, useState } from 'react'; +import * as React from 'react'; +import { LayoutProvider } from './LayoutContext'; +import { ChartAreaProps, ChartComponentProps, ChartLegendProps, ChartStackLabelsProps, ChartTitleProps, Column, Row, ChartTooltipProps, ChartSeriesProps, ChartZoomSettingsProps } from '../base/interfaces'; +import { defaultChartConfigs } from '../base/default-properties'; +import { AxisModel, ChartProviderChildProps, ElementWithSize } from '../chart-area/chart-interfaces'; + +const defaultContextValue: ChartProviderChildProps = {} as ChartProviderChildProps; +export const ChartContext: React.Context = createContext(defaultContextValue); +interface ChartProviderProps { + props: ChartComponentProps; + parentElement: ElementWithSize; +} + +/** + * Provides chart configuration and state management context to child components. + * + * @component + * @example + * ```tsx + * + * + * + * + * ``` + */ + +export const ChartProvider: React.FC<{ + props: ChartComponentProps, parentElement: ElementWithSize +}> = ({ props, parentElement }: ChartProviderProps) => { + + const chartRender: boolean = React.Children.count(props.children) > 0; + const [chartTitle, setChartTitleState] = useState(defaultChartConfigs.ChartTitle as ChartTitleProps); + const [chartSubTitle, setChartSubTitleState] = useState(defaultChartConfigs.ChartSubTitle as ChartTitleProps); + const [chartArea, setChartAreaState] = useState(defaultChartConfigs.ChartArea); + const [chartLegend, setChartLegendState] = useState(defaultChartConfigs.ChartLegend); + const [render, setRender] = useState(!chartRender); + const [columns, setColumns] = useState([defaultChartConfigs.Column]); + const [rows, setRows] = useState([defaultChartConfigs.Row]); + const [primaryXAxis, setPrimaryXAxis] = useState>(defaultChartConfigs.PrimaryXAxis); + const [primaryYAxis, setPrimaryYAxis] = useState>(defaultChartConfigs.PrimaryYAxis); + const [chartTooltip, setChartTooltipState] = useState(defaultChartConfigs.ChartTooltip); + const [chartSeries, setchartSeriesState] = useState([defaultChartConfigs.ChartSeries]); + const [chartAxes, setAxes] = useState([]); + const [chartZoom, setZoom] = useState(defaultChartConfigs.ChartZoom); + const [chartStackLabels, setChartStackLabelsState] = useState(defaultChartConfigs.ChartStackLabels); + const setChartTitle: (titleProps: ChartTitleProps) => void = useCallback((titleProps: ChartTitleProps) => { + setChartTitleState(titleProps); + setRender(true); + }, []); + + const setChartLegend: (legendProps: ChartLegendProps) => void = useCallback((legendProps: ChartLegendProps) => { + setChartLegendState(legendProps); + setRender(true); + }, []); + + const setChartSubTitle: (subtitleProps: ChartTitleProps) => void = useCallback((subtitleProps: ChartTitleProps) => { + setChartSubTitleState(subtitleProps); + setRender(true); + }, []); + + const setChartArea: (areaProps: ChartAreaProps) => void = useCallback((areaProps: ChartAreaProps) => { + setChartAreaState(areaProps); + setRender(true); + }, []); + + const setChartColumns: (columns: Column[]) => void = useCallback((columns: Column[]) => { + setColumns(columns); + setRender(true); + }, []); + + const setChartRows: (rows: Row[]) => void = useCallback((rows: Row[]) => { + setRows(rows); + setRender(true); + }, []); + + const setChartPrimaryXAxis: (xAxis: AxisModel) => void = useCallback((xAxis: AxisModel) => { + setPrimaryXAxis(xAxis); + setRender(true); + }, []); + + const setChartPrimaryYAxis: (yAxis: AxisModel) => void = useCallback((yAxis: AxisModel) => { + setPrimaryYAxis(yAxis); + setRender(true); + }, []); + + const setChartSeries: (series: ChartSeriesProps[]) => void = useCallback((series: ChartSeriesProps[]) => { + setchartSeriesState(series); + setRender(true); + }, []); + + const setChartAxes: (axes: AxisModel[]) => void = useCallback((axes: AxisModel[]) => { + setAxes(axes); + setRender(true); + }, []); + + const setChartZoom: (zoom: ChartZoomSettingsProps) => void = useCallback((zoom: ChartZoomSettingsProps) => { + setZoom(zoom); + setRender(true); + }, []); + + const setChartTooltip: (tooltip: ChartTooltipProps) => void = useCallback((tooltip: ChartTooltipProps) => { + setChartTooltipState(tooltip); + }, []); + + const setChartStackLabels: (stackLabels: ChartStackLabelsProps) => void = useCallback((stackLabels: ChartStackLabelsProps) => { + setChartStackLabelsState(stackLabels); + setRender(true); + }, []); + + // Only recreate context value when setter functions or properties actually change + const chartProps: ChartComponentProps = props; + const contextValue: ChartProviderChildProps = useMemo(() => ({ + chartProps, + chartTitle, + chartSubTitle, + chartArea, + chartLegend, + render, + parentElement, + columns, + rows, + chartSeries, + chartTooltip, + chartStackLabels, + setChartTitle, + setChartSubTitle, + setChartArea, + chartZoom, + setChartLegend, + setChartColumns, + setChartRows, + setChartPrimaryXAxis, + setChartPrimaryYAxis, + setChartSeries, + setChartAxes, + setChartTooltip, + setChartZoom, + setChartStackLabels, + axisCollection: [primaryXAxis as AxisModel, primaryYAxis as AxisModel, ...chartAxes] + }), [chartProps, chartTitle, chartSubTitle, chartArea, render, chartLegend, + setChartLegend, parentElement, columns, rows, chartSeries, chartTooltip, chartStackLabels, + setChartTitle, setChartSubTitle, setChartArea, setChartColumns, + setChartRows, setChartPrimaryXAxis, setChartPrimaryYAxis, setChartSeries, setChartAxes, setChartTooltip, + setChartStackLabels, chartZoom, setChartZoom, + [primaryXAxis as AxisModel, primaryYAxis as AxisModel, ...chartAxes] + ]); + + return ( + + {props.children} + + + ); +}; diff --git a/components/charts/src/chart/layout/LayoutContext.tsx b/components/charts/src/chart/layout/LayoutContext.tsx new file mode 100644 index 0000000..b55e71a --- /dev/null +++ b/components/charts/src/chart/layout/LayoutContext.tsx @@ -0,0 +1,1021 @@ +//LayoutContext.tsx +import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { ChartRenderer } from '../renderer/ChartRenderer'; +import { AxisLabelClickEvent, ChartMouseEvent, PointClickEvent, ResizeEvent, ChartSeriesProps, ChartLocationProps, ChartStripLineProps } from '../base/interfaces'; +import { ChartTitleRenderer } from '../renderer/ChartTitleRenderer'; +import { ChartSubTitleRenderer } from '../renderer/ChartSubtitleRender'; +import { ChartLegendRenderer, CustomLegendRenderer } from '../renderer/LegendRenderer/ChartLegendRenderer'; +import { ChartAreaRenderer } from '../renderer/ChartAreaRender'; +import { AxisRenderer } from '../renderer/AxesRenderer/AxisRender'; +import { SeriesRenderer } from '../renderer/SeriesRenderer/SeriesRenderer'; +import { ChartContext } from './ChartProvider'; +import { doPan, performZoomRedraw, redrawOnZooming, reset, ZoomContent, zoomInOutCalculation } from '../renderer/Zooming/zooming'; +import { ZoomToolkit } from '../renderer/Zooming/zoom-toolbar'; +import { ChartColumnsRender } from '../renderer/AxesRenderer/ChartColumnsRender'; +import { ChartRowsRender } from '../renderer/AxesRenderer/ChartRowsRender'; +import { TooltipRenderer } from '../renderer/TooltipRenderer'; +import { callChartEventHandlers } from '../hooks/useClipRect'; +import { getData, PointData } from '../utils/getData'; +import { Browser, isNullOrUndefined } from '@syncfusion/react-base'; +import TrackballRenderer from '../renderer/TrackballRenderer'; +import { stringToNumber } from '../utils/helper'; +import { AxisOutsideRenderer } from '../renderer/AxesRenderer/AxisOutsideRenderer'; +import ChartStackLabelsRenderer from '../renderer/ChartStackLabelsRenderer'; +import { AxisModel, BaseZoom, Chart, LayoutState, Rect, SeriesProperties, ChartSizeProps } from '../chart-area/chart-interfaces'; +import { ZoomMode } from '../base/enum'; +import { StripLineBeforeRenderer } from '../renderer/AxesRenderer/ChartStripLinesRender'; +import { StripLineAfterRenderer } from '../renderer/AxesRenderer/ChartStripLinesRender'; +import { LayoutContextType } from '../common/base'; + +/** + * Represents a mapping between layout keys and their corresponding layout state or chart instance. + * + * @private + */ +export type LayoutMap = Record; + +/** + * React context for managing layout-related state and operations. + * Provides access to layout phase, animation control, size, and chart references. + */ +const LayoutContext: React.Context = createContext(null); + +/** + * Provides layout context to child components. + * Manages layout phase, animation state, and chart measurement lifecycle. + * + * @returns {Element} The layout context provider component. + */ +export const LayoutProvider: React.FC = () => { + const [phase, setPhase] = useState<'measuring' | 'rendering'>('measuring'); + const { render, chartProps, chartTitle, chartSubTitle, chartArea, chartLegend, chartZoom, + parentElement, rows, columns, chartSeries, chartStackLabels, axisCollection, chartTooltip } = useContext(ChartContext); + const measuredKeysRef: React.RefObject> = useRef>(new Set()); + const layoutRef: React.RefObject = useRef({}); + const striplineVisibility: boolean = axisCollection.some( + (axis: AxisModel) => Array.isArray(axis.stripLines) && + axis.stripLines.some((stripLine: ChartStripLineProps) => stripLine?.visible === true) + ); + const expectedKeys: string[] = useMemo(() => { + const keys: string[] = ['Chart', 'ChartArea', 'ChartAxis']; + if (chartTitle?.text) { + keys.push('ChartTitle'); + } + if (chartSubTitle?.text) { + keys.push('ChartSubTitle'); + } + if (chartLegend.visible) { + keys.push('ChartLegend'); + } + if (chartSeries.length > 0) { + keys.push('ChartSeries'); + } + if (chartZoom.selectionZoom || chartZoom.mouseWheelZoom || chartZoom.pinchZoom) { + keys.push('ChartZoom'); + } + if (chartStackLabels.visible) { + keys.push('ChartStackLabels'); + } + if (striplineVisibility) { + keys.push('ChartStripLinesBehind'); + keys.push('ChartStripLinesOver'); + } + return keys; + }, [ + chartTitle?.text, + chartSubTitle?.text, + chartLegend.visible, + chartSeries.length, + chartZoom.selectionZoom, + chartZoom.mouseWheelZoom, + chartZoom.pinchZoom, + chartStackLabels.visible + ]); + + const setLayoutValue: (key: string, value: Partial) => void = useCallback((key: string, value: Partial) => { + layoutRef.current[key as string] = { ...layoutRef.current[key as string], ...value }; // Update ref + + }, []); + + const titleRef: React.RefObject = useRef(null); + const subtitleRef: React.RefObject = useRef(null); + const seriesRef: React.RefObject = useRef(null); + const legendRef: React.RefObject = useRef(null); + const trackballRef: React.RefObject = useRef(null); + const [animationProgress, setAnimationProgress] = useState(0); + + // Add a ref to track if mouse is currently inside the chart + const [isMouseInside, setIsMouseInside] = useState(false); + + useLayoutEffect(() => { + if (phase === 'rendering') { + const chartContainer: HTMLElement = parentElement.element; + const disableScroll: boolean = (chartZoom.selectionZoom || chartZoom.pinchZoom as boolean); + chartContainer.style.touchAction = disableScroll ? 'none' : 'element'; + } + return undefined; + }, [phase]); + + const reportMeasured: (key: string) => void = useCallback((key: string) => { + measuredKeysRef.current.add(key); + + if (measuredKeysRef.current.size === expectedKeys.length) { + + setPhase('rendering'); + } + }, [expectedKeys]); + const [disableAnimation, setDisableAnimation] = useState(false); + + useEffect(() => { + if (phase === 'rendering') { + triggerRemeasure(); + } + }, [expectedKeys.length]); + + /** + * Triggers a re-measurement of the chart layout. + * Disables animations, clears previously measured keys, resets layout reference, + * and sets the layout phase to 'measuring' to prepare for a fresh layout calculation. + * + * @returns {void} This function does not return a value. + */ + const triggerRemeasure: () => void = useCallback(() => { + setDisableAnimation(true); + measuredKeysRef.current.clear(); + layoutRef.current = {}; + setPhase('measuring'); + }, []); + + /** + * Handles pointer movement over the chart area. + * Retrieves the chart instance and layout dimensions to support dynamic interactions + * such as tooltips, crosshairs, or hover effects. + * + * @param {PointerEvent} event - The pointer event triggered by user movement. + * @returns {void} This function does not return a value. + */ + const handleMouseMove: (event: PointerEvent) => void = useCallback((event: PointerEvent) => { + const chart: Chart = layoutRef.current.chart as Chart; + const rect: DOMRect = parentElement.element.getBoundingClientRect(); + + let pageY: number; + let pageX: number; + if (event.type === 'touchmove') { + const touchArg: TouchEvent = event as unknown as TouchEvent; + pageX = touchArg.changedTouches[0].clientX; + chart.isTouch = true; + pageY = touchArg.changedTouches[0].clientY; + } else { + pageY = event.clientY; + pageX = event.clientX; + if (chart) { + chart.isTouch = event.pointerType === 'touch' || event.pointerType === '2'; + } + } + + const mouseX: number = pageX - rect.left; + const mouseY: number = pageY - rect.top; + void (chart && (chart.mouseX = mouseX, chart.mouseY = mouseY)); + callChartEventHandlers('mouseMove', event, chart, mouseX, mouseY); + if (chartProps.onMouseMove) { + const mouseArgs: ChartMouseEvent = { + target: (event.target as HTMLElement)?.id, + x: event.clientX, + y: event.clientY + }; + chartProps.onMouseMove(mouseArgs); + } + }, [chartProps]); + + /** + * Handles the mouse enter event on the chart area. + * Only triggers when the mouse first enters the chart area, not during movement within the chart. + * Resets when the mouse leaves and re-enters the chart. + * + * @param {MouseEvent} event - The mouse event triggered when the pointer enters the chart. + * @returns {void} This function does not return a value. + */ + const handleMouseEnter: (event: MouseEvent) => void = useCallback((event: MouseEvent) => { + if (!isMouseInside) { + setIsMouseInside(true); + const chart: Chart = layoutRef.current.chart as Chart; + const rect: DOMRect = parentElement.element.getBoundingClientRect(); + + const mouseX: number = event.clientX - rect.left; + const mouseY: number = event.clientY - rect.top; + + // Update chart mouse coordinates + if (chart) { + chart.mouseX = mouseX; + chart.mouseY = mouseY; + } + + // // Call chart event handlers for mouse enter + // callChartEventHandlers('mouseEnter', event, chart, mouseX, mouseY); + + // Trigger the user-defined onMouseEnter callback if provided + if (chartProps.onMouseEnter) { + const mouseArgs: ChartMouseEvent = { + target: (event.target as HTMLElement)?.id, + x: event.clientX, + y: event.clientY + }; + chartProps.onMouseEnter(mouseArgs); + } + } + }, [chartProps, isMouseInside]); + + /** + * Handles mouse click events on the chart area. + * Retrieves the chart instance and identifies the clicked target element. + * Increments the chart's internal click count for interaction tracking. + * + * @param {MouseEvent} event - The mouse event triggered by user interaction. + * @returns {void} This function does not return a value. + */ + const handleMouseClick: (event: MouseEvent) => void = useCallback((event: MouseEvent) => { + const chart: Chart = layoutRef.current.chart as Chart; + const targetId: string = (event.target as HTMLElement)?.id || ''; + chart.clickCount++; + // Call all registered click handlers + callChartEventHandlers('click', event, chart, targetId); + if (chartProps.onClick) { + const mouseArgs: ChartMouseEvent = { + target: (event.target as HTMLElement)?.id, + x: event.clientX, + y: event.clientY + }; + chartProps.onClick(mouseArgs); + } + if (chartProps.onAxisLabelClick) { + triggerAxisLabelClickEvent(event as PointerEvent, layoutRef.current.chart as Chart); + } + + if (chart.clickCount === 1 && chartProps.onPointClick) { + chart.clickCount = 0; + triggerPointClickEvent(event as PointerEvent, chart); + } + removeNavigationStyle(parentElement.element); + removeChartNavigationStyle(); + }, [chartProps]); + + /** + * Handles global keyboard shortcuts for chart navigation. + * Applies navigation styles to the chart container when the user presses Alt + J. + * + * @param {KeyboardEvent} e - The keyboard event triggered by user input. + * @returns {void} This function does not return a value. + */ + const documentKeyHandler: (e: KeyboardEvent) => void = (e: KeyboardEvent) => { + if (e.altKey && e.key === 'j') { // Use 'j' for key instead of keyCode + setNavigationStyle(parentElement.element); + } + }; + + /** + * Applies navigation-related styles to the specified HTML element. + * Sets focus on the element and applies a visible outline and margin + * to indicate keyboard navigation focus, using chart-specific styling. + * + * @param {HTMLElement} element - The target element to style and focus. + * @returns {void} This function does not return a value. + */ + const setNavigationStyle: (element: HTMLElement) => void = (element: HTMLElement) => { + if (element) { + element.focus(); + element.style.outline = `${chartProps.focusOutline?.width || 1.5}px solid ${chartProps.focusOutline?.color || (layoutRef.current.chart as Chart).themeStyle.tabColor}`; + element.style.margin = `${chartProps.focusOutline?.offset || 0}px`; + } + }; + + /** + * Removes navigation-related styles from the specified HTML element. + * Clears the outline and resets margin to ensure a clean visual state, + * typically used after keyboard or focus interactions. + * + * @param {HTMLElement} element - The target element from which styles should be removed. + * @returns {void} This function does not return a value. + */ + const removeNavigationStyle: (element: HTMLElement) => void = (element: HTMLElement) => { + element.style.outline = 'none'; + element.style.margin = `${0}px`; + }; + + /** + * Handles the keydown event for chart keyboard interactions. + * Prevents default behavior for specific keys when the chart is zoomed or when the spacebar is pressed, + * to maintain custom navigation and interaction logic. + * + * @param {KeyboardEvent} e - The keyboard event triggered by user input. + * @returns {boolean} Indicates whether the keydown event was handled. + */ + const chartKeyDown: (e: KeyboardEvent) => boolean = (e: KeyboardEvent) => { + let actionKey: string = ''; + const chart: Chart = layoutRef.current.chart as Chart; + if ((chart.isZoomed && e.code === 'Tab') || e.code === 'Space') { + e.preventDefault(); + } + if (chart.tooltipModule.enable && ((e.code === 'Tab' && chart.previousTargetId.indexOf('Series') > -1) || e.code === 'Escape')) { + actionKey = 'ESC'; + } + if (e.ctrlKey && (e.key === '+' || e.code === 'Equal' || e.key === '-' || e.code === 'Minus')) { + e.preventDefault(); + chart.isZoomed = (chart.zoomSettings.selectionZoom || + chart.zoomSettings.pinchZoom || (chart.zoomSettings.mouseWheelZoom as boolean)); + actionKey = chart.isZoomed ? e.code : ''; + } + else if (e['keyCode'] === 82 && chart.isZoomed) { // KeyCode 82 (R) for reseting + e.preventDefault(); + chart.isZoomed = false; + actionKey = 'R'; + } + else if (e.code.indexOf('Arrow') > -1) { + e.preventDefault(); + actionKey = chart.isZoomed ? e.code : ''; + } + if (e.ctrlKey && (e.key === 'p')) { + e.preventDefault(); + actionKey = 'CtrlP'; + } + if (actionKey !== '') { + chartKeyboardNavigations(e, (e.target as HTMLElement).id, actionKey); + } + if (e.code === 'Tab') { + removeNavigationStyle(parentElement.element); + removeChartNavigationStyle(); + } + return false; + }; + + /** + * Removes navigation-related styles from chart text elements such as title and subtitle. + * Clears any focus-related visual indicators to reset chart accessibility state. + * + * @returns {void} This function does not return a value. + */ + const removeChartNavigationStyle: () => void = () => { + const chartElements: React.RefObject[] = [titleRef, subtitleRef]; + for (let i: number = 0; i < chartElements.length; i++) { + const element: SVGTextElement = chartElements[i as number].current as SVGTextElement; + if (element) { + element.style.outline = 'none'; + element.style.margin = `${0}px`; + } + } + if (legendRef.current) { + const childElements: HTMLCollection = legendRef.current.firstElementChild?.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.outline = 'none'; + element.style.margin = `${0}px`; + } + } + } + if (seriesRef.current) { + const childElements: HTMLCollection = seriesRef.current?.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.outline = 'none'; + element.style.margin = `${0}px`; + if (element.children[1]) { + (element.children[1] as HTMLElement).style.outline = 'none'; + (element.children[1] as HTMLElement).style.margin = `${0}px`; + } + } + for (let j: number = 0; j < element.children?.length; j++) { + const pointElement: HTMLElement = element.children[j as number] as HTMLElement; + if (pointElement) { + pointElement.style.outline = 'none'; + pointElement.style.margin = `${0}px`; + } + } + } + } + }; + + /** + * Handles the keyup event for chart keyboard interactions. + * Determines the action key and target element, and prepares chart-related elements + * for further processing such as marker or group selection. + * + * @param {KeyboardEvent} e - The keyboard event triggered by user input. + * @returns {boolean} Indicates whether the keyup event was handled. + */ + const chartKeyUp: (e: KeyboardEvent) => boolean = (e: KeyboardEvent): boolean => { + let actionKey: string = ''; + let targetId: string = (e.target as Element)['id']; + const chart: Chart = layoutRef.current.chart as Chart; + let groupElement: SVGGElement | HTMLElement; + let markerGroup: HTMLElement | null = null; + const targetElement: HTMLElement = e.target as HTMLElement; + const seriesElement: SVGGElement = seriesRef.current as SVGGElement; + const legendElement: SVGGElement = legendRef.current?.firstElementChild as SVGGElement; + if (seriesElement && seriesElement.firstElementChild && seriesElement.firstElementChild.children[1]) { + const firstChild: HTMLElement = seriesElement.firstElementChild.children[1] as HTMLElement; + let className: string = firstChild.getAttribute('class') as string; + if (className && className.indexOf('sf-chart-focused') === -1) { + className = className + ' sf-chart-focused'; + } else if (!className) { + className = 'sf-chart-focused'; + } + firstChild.setAttribute('class', className); + } + if (legendElement && legendElement.firstElementChild) { + const firstChild: HTMLElement = legendElement.firstElementChild as HTMLElement; + let className: string = firstChild.getAttribute('class') as string; + if (className && className.indexOf('sf-chart-focused') === -1) { + className = className + ' sf-chart-focused'; + } + else if (!className) { + className = 'sf-chart-focused'; + } + firstChild.setAttribute('class', className); + } + if (e.code === 'Tab') { + if (!isNullOrUndefined(chart.previousTargetId) && chart.previousTargetId !== '') { + if ((chart.previousTargetId.indexOf('_Series_') > -1 && targetId.indexOf('_Series_') === -1)) { + groupElement = seriesRef.current as SVGGElement; + chart.currentPointIndex = 0; + chart.currentSeriesIndex = 0; + } + else if (chart.previousTargetId.indexOf('_chart_legend_g_') > -1 && targetId.indexOf('_chart_legend_g_') === -1) { + groupElement = legendRef.current?.firstElementChild as SVGGElement; + setTabIndex(groupElement.children[chart.currentLegendIndex] as HTMLElement, + groupElement.firstElementChild as HTMLElement); + } + } + chart.previousTargetId = targetId; + if (targetId.indexOf('SeriesGroup') > -1) { + chart.currentSeriesIndex = +targetId.split('SeriesGroup')[1]; + targetElement.removeAttribute('tabindex'); + targetElement.blur(); + if (targetElement.children[1].id.indexOf('_Point_') === -1) { + markerGroup = document.getElementById(chart.element.id + 'SymbolGroup' + targetId.split('SeriesGroup')[1]) as HTMLElement; + } + targetId = focusChild(markerGroup != null ? markerGroup.children[1] as HTMLElement + : targetElement.children[1] as HTMLElement); + } + if (targetId.indexOf('_ChartTitle') > -1 || targetId.indexOf('_ChartSubTitle') > -1) { + setNavigationStyle(e.target as HTMLElement); + } + actionKey = targetId !== parentElement.element.id ? 'Tab' : ''; + } + else if (e.code.indexOf('Arrow') > -1) { + e.preventDefault(); + if ((targetId.indexOf('_chart_legend_') > -1)) { + const legendElement: HTMLCollection = targetElement?.parentElement?.children as HTMLCollection; + legendElement[chart.currentLegendIndex].removeAttribute('tabindex'); + chart.currentLegendIndex += (e.code === 'ArrowUp' || e.code === 'ArrowRight') ? + 1 : - 1; + chart.currentLegendIndex = getActualIndex(chart.currentLegendIndex, legendElement.length); + const currentLegend: HTMLElement = legendElement[chart.currentLegendIndex] as HTMLElement; + focusChild(currentLegend as HTMLElement); + removeChartNavigationStyle(); + setNavigationStyle(currentLegend); + targetId = currentLegend.children[1].id; + actionKey = ''; + } + else if (targetId.indexOf('_Series_') > -1) { + groupElement = targetElement.parentElement?.parentElement as HTMLElement; + let currentPoint: Element = e.target as Element; + targetElement.removeAttribute('tabindex'); + targetElement.blur(); + if (e.code === 'ArrowRight' || e.code === 'ArrowLeft') { + const seriesIndexes: number[] = []; + for (let i: number = 0; i < groupElement.children.length; i++) { + if (groupElement.children[i as number].id.indexOf('SeriesGroup') > -1) { + seriesIndexes.push(+groupElement.children[i as number].id.split('SeriesGroup')[1]); + } + } + chart.currentSeriesIndex = seriesIndexes.indexOf(chart.currentSeriesIndex) + (e.code === 'ArrowRight' ? 1 : -1); + chart.currentSeriesIndex = seriesIndexes[getActualIndex(chart.currentSeriesIndex, seriesIndexes.length)]; + } + else { + chart.currentPointIndex += e.code === 'ArrowUp' ? 1 : -1; + } + if (targetId.indexOf('_Symbol') > -1) { + chart.currentPointIndex = getActualIndex(chart.currentPointIndex, + (document.getElementById(chart.element.id + 'SymbolGroup' + chart.currentSeriesIndex)?.childElementCount as number) - 2); + currentPoint = document.getElementById(chart.element.id + '_Series_' + chart.currentSeriesIndex + '_Point_' + + chart.currentPointIndex + '_Symbol0') as HTMLElement; + } + else if (targetId.indexOf('_Point_') > -1) { + chart.currentPointIndex = getActualIndex(chart.currentPointIndex, + (document.getElementById(chart.element.id + 'SeriesGroup' + chart.currentSeriesIndex)?.childElementCount as number) - 1); + currentPoint = document.getElementById(chart.element.id + '_Series_' + chart.currentSeriesIndex + '_Point_' + + chart.currentPointIndex) as HTMLElement; + } + targetId = focusChild(currentPoint as HTMLElement); + actionKey = 'ArrowMove'; + } + } + else if ((e.code === 'Enter' || e.code === 'Space') && ((targetId.indexOf('_chart_legend_') > -1) || + (targetId.indexOf('_Point_') > -1))) { + targetId = (targetId.indexOf('_chart_legend_page') > -1) ? targetId : ((targetId.indexOf('_chart_legend_') > -1) ? + targetElement.children[1]?.id : targetId); + actionKey = 'Enter'; + } + if (actionKey !== '') { + chartKeyboardNavigations(e, targetId, actionKey); + } + return false; + }; + + /** + * Handles keyboard navigation events within the chart. + * Dispatches the appropriate chart event based on the action key and updates internal chart state. + * + * @param {KeyboardEvent} e - The keyboard event triggered by user interaction. + * @param {string} targetId - The ID of the target element receiving the keyboard action. + * @param {string} actionKey - The key representing the intended chart navigation action. + * @returns {void} This function does not return a value. + */ + const chartKeyboardNavigations: (e: KeyboardEvent, targetId: string, actionKey: string) => void = + (e: KeyboardEvent, targetId: string, actionKey: string) => { + const chart: Chart = layoutRef.current.chart as Chart; + chart.isLegendClicked = false; + const zoom: BaseZoom = layoutRef.current?.chartZoom as BaseZoom; + removeChartNavigationStyle(); + if (actionKey !== 'Enter' && actionKey !== 'Space') { + setNavigationStyle(document.getElementById(targetId) as HTMLElement); + } + const yArrowPadding: number = actionKey === 'ArrowUp' ? 10 : (actionKey === 'ArrowDown' ? -10 : 0); + const xArrowPadding: number = actionKey === 'ArrowLeft' ? -10 : (actionKey === 'ArrowRight' ? 10 : 0); + switch (actionKey) { + case 'Tab': + case 'ArrowMove': + if (targetId?.indexOf('_Point_') > -1) { + const seriesIndex: number = +(targetId.split('_Series_')[1].split('_Point_')[0]); + const pointIndex: number = +(targetId.split('_Series_')[1].replace('_Symbol0', '').split('_Point_')[1]); + const pointRegion: ChartLocationProps = chart.visibleSeries[seriesIndex as number]?.points[pointIndex as number]?. + symbolLocations?.[0] as ChartLocationProps; + const seriesType: string = chart.visibleSeries[seriesIndex as number]?.type as string; + chart.mouseX = pointRegion.x + chart.chartAxislayout.initialClipRect.x - + (seriesType.indexOf('StackingBar') > -1 ? + chart.visibleSeries[seriesIndex as number].marker?.height as number / 2 : 0); + chart.mouseY = pointRegion.y + chart.chartAxislayout.initialClipRect.y + + (seriesType.indexOf('StackingColumn') > -1 ? + chart.visibleSeries[seriesIndex as number].marker?.height as number / 2 : 0); + + if (chart.tooltipModule?.enable) { + const mouseEvent: MouseEvent = new MouseEvent('mousemove', { + bubbles: true, + cancelable: true, + view: window, + clientX: chart.mouseX, + clientY: chart.mouseY + }); + callChartEventHandlers('mouseMove', mouseEvent, chart, chart.mouseX, chart.mouseY); + } + } + break; + case 'Enter': + case 'Space': + if (targetId?.indexOf('_chart_legend_') > -1) { + chart.isLegendClicked = true; + (chartSeries as unknown as SeriesProperties).visible = false; + + const clickEvent: MouseEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + + document.getElementById(targetId)?.dispatchEvent(clickEvent); + focusChild(document.getElementById(targetId)?.parentElement as HTMLElement); + setNavigationStyle(document.getElementById(targetId)?.parentElement as HTMLElement); + + } else { + setNavigationStyle(e.target as HTMLElement); + } + break; + case 'ESC': + chart.tooltipRef?.current?.fadeOut(); + if (trackballRef && trackballRef.current) { + const childElements: HTMLCollection = trackballRef.current.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.display = 'none'; + } + } + } + break; + case 'Equal': + case 'Minus': + chart.isZoomed = chart.performedUI = true; + zoom.isPanning = chart.isChartDrag = false; + if (actionKey === 'Equal') { + zoomInOutCalculation(1, chart, chart.axisCollection, chart.zoomSettings.mode as ZoomMode); + } + else { + zoomInOutCalculation(-1, chart, chart.axisCollection, chart.zoomSettings.mode as ZoomMode); + } + performZoomRedraw(chart); + break; + case 'ArrowUp': + case 'ArrowDown': + case 'ArrowLeft': + case 'ArrowRight': + zoom.isPanning = chart.isChartDrag = true; + chart.isChartDrag = true; + doPan(chart, chart.axisCollection, xArrowPadding, yArrowPadding); + performZoomRedraw(chart); + redrawOnZooming(chart, true); + break; + case 'R': + reset(chart, layoutRef.current?.chartZoom as BaseZoom); + break; + } + }; + + /** + * Handles the pointer up event on the chart area. + * Retrieves chart and layout dimensions to assist with interaction logic, + * such as determining pointer position relative to the chart. + * + * @param {PointerEvent} event - The pointer event triggered when the user releases the mouse or touch input. + * @returns {void} This function does not return a value. + */ + const handleMouseUp: (event: PointerEvent) => void = useCallback((event: PointerEvent) => { + const chart: Chart = layoutRef.current.chart as Chart; + const rect: DOMRect = parentElement.element.getBoundingClientRect(); + let pageY: number; + let pageX: number; + if (event.type === 'touchend') { + const touchArg: TouchEvent = event as unknown as TouchEvent; + pageX = touchArg.changedTouches[0].clientX; + chart.isTouch = true; + pageY = touchArg.changedTouches[0].clientY; + } else { + pageY = event.clientY; + pageX = event.clientX; + chart.isTouch = event.pointerType === 'touch' || event.pointerType === '2'; + } + + const mouseX: number = pageX - rect.left; + const mouseY: number = pageY - rect.top; + + chart.mouseX = mouseX; + chart.mouseY = mouseY; + chart.isChartDrag = false; + callChartEventHandlers('mouseUp', event, chart, mouseX, mouseY); + if (chart.isTouch) { + chart.threshold = new Date().getTime() + 300; + } + }, [chartProps]); + + /** + * Handles the mouse down event on the chart area. + * Captures the chart instance and calculates layout-related dimensions + * such as the bounding rectangles of the chart container and SVG element. + * Also applies device-specific offset logic. + * + * @param {MouseEvent} event - The mouse event triggered by user interaction. + * @returns {void} This function does not return a value. + */ + const handleMouseDown: (event: MouseEvent) => void = useCallback((event: MouseEvent) => { + const chart: Chart = layoutRef.current.chart as Chart; + const rect: DOMRect = parentElement.element.getBoundingClientRect(); + const svgElement: SVGSVGElement = parentElement.element.querySelector('svg') as SVGSVGElement; + const svgRect: DOMRect = svgElement?.getBoundingClientRect(); + const offset: number = Browser.isDevice ? 20 : 30; + let pageX: number; let pageY: number; + if (event.type === 'touchstart') { + const touchEvent: TouchEvent = event as unknown as TouchEvent; + chart.isTouch = true; + pageX = touchEvent.changedTouches[0].clientX; + pageY = touchEvent.changedTouches[0].clientY; + } else { + chart.isTouch = false; + pageX = event.clientX; + pageY = event.clientY; + } + const mouseDownX: number = (pageX - rect.left) - Math.max((svgRect?.left || 0) - rect.left, 0); + const mouseDownY: number = (pageY - rect.top) - Math.max((svgRect?.top || 0) - rect.top, 0); + + chart.mouseDownX = chart.previousMouseMoveX = mouseDownX; + chart.mouseDownY = chart.previousMouseMoveY = mouseDownY; + chart.mouseX = mouseDownX; + chart.mouseY = mouseDownY; + + //Handle double tap detection for touch events + if (chart.isTouch) { + const target: Element = event.target as Element; + chart.isDoubleTap = (new Date().getTime() < chart.threshold && target.id.indexOf(chart.element.id + '_Zooming_') === -1 && + (chart.mouseDownX - offset >= chart.mouseX || chart.mouseDownX + offset >= chart.mouseX) && + (chart.mouseDownY - offset >= chart.mouseY || chart.mouseDownY + offset >= chart.mouseY) && + (chart.mouseX - offset >= chart.mouseDownX || chart.mouseX + offset >= chart.mouseDownX) && + (chart.mouseY - offset >= chart.mouseDownY || chart.mouseY + offset >= chart.mouseDownY)); + } + callChartEventHandlers('mouseDown', event, chart, mouseDownX, mouseDownY); + }, [chartProps]); + + /** + * Handles the mouse wheel event on the chart area. + * Dispatches the 'mouseWheel' event to chart event handlers if the chart instance is available. + * + * @param {WheelEvent} event - The wheel event triggered by user interaction. + * @returns {void} This function does not return a value. + */ + const handleMouseWheel: (event: WheelEvent) => void = useCallback((event: WheelEvent) => { + const chart: Chart = layoutRef.current.chart as Chart; + if (chart) { + callChartEventHandlers('mouseWheel', event, chart); + } + }, []); + + /** + * Triggers a click event for the data point under the cursor. + * + * @param {MouseEvent | PointerEvent} e - The mouse or pointer event that triggered the click + * @param {Chart} chart - The chart instance containing the clicked point + * @returns {void} + */ + function triggerPointClickEvent(e: MouseEvent | PointerEvent, chart: Chart): void { + triggerPointEvent(chart, e); + } + + /** + * Triggers a specific point event with the provided parameters. + * + * @param {Chart} chart - The chart instance containing the point + * @param {MouseEvent | PointerEvent} evt - The original DOM event that triggered this action + * @returns {void} + */ + function triggerPointEvent( + chart: Chart, + evt: PointerEvent | MouseEvent + ): void { + // Get data for the point under cursor + const pointData: PointData = getData(chart); + const eventArgs: PointClickEvent = { + seriesIndex: pointData.series?.index as number, + pointIndex: pointData.point?.index as number, + x: chart.mouseX, + y: chart.mouseY, + pageX: evt.pageX, + pageY: evt.pageY + }; + if (pointData.series && pointData.point) { + if (chart.chartProps.onPointClick) { + chart.chartProps.onPointClick(eventArgs); + } + } + } + + /** + * Handles the mouse leave event on the chart area. + * Dispatches the 'mouseLeave' event to chart event handlers and resets drag state. + * + * @param {MouseEvent} event - The mouse event triggered when the pointer leaves the chart. + * @returns {void} This function does not return a value. + */ + const handleMouseLeave: (event: MouseEvent) => void = useCallback((event: MouseEvent) => { + setIsMouseInside(false); + const chart: Chart = layoutRef.current.chart as Chart; + const element: Element = event.target as Element; + const mouseArgs: ChartMouseEvent = { + target: element.id, + x: chart?.mouseX || 0, + y: chart?.mouseY || 0 + }; + if (chartProps.onMouseLeave) { + chartProps.onMouseLeave(mouseArgs); + } + callChartEventHandlers('mouseLeave', event, chart); + void (chart && (chart.isChartDrag = false)); + }, [chartProps, parentElement]); + + /** + * Handles chart resizing logic and disables animation during the resize event. + * Constructs and dispatches a resize event to notify chart components. + * + * @returns {boolean} Indicates whether the resize operation was initiated. + */ + const chartResize: () => boolean = () => { + const chart: Chart = layoutRef.current.chart as Chart; + chart.animateSeries = false; + const arg: ResizeEvent = { + currentSize: { + width: 0, + height: 0 + }, + previousSize: { + width: availableSize.width, + height: availableSize.height + } + }; + + if (chart.resizeTo) { + clearTimeout(chart.resizeTo); + } + + chart.resizeTo = +setTimeout(() => { + chart.availableSize = { + height: stringToNumber(chartProps.height, parentElement.element.clientHeight) || 450, + width: stringToNumber(chartProps.width, parentElement.element.clientWidth) || parentElement.element.clientWidth + }; + parentElement.availableSize = arg.currentSize = chart.availableSize; + triggerRemeasure(); + chartProps.onResize?.(arg); + }, 500); + + return false; + }; + + // Attach events to the chart container + useEffect(() => { + const chartContainer: HTMLElement = parentElement.element; + chartContainer.addEventListener('mousemove', handleMouseMove as EventListener); + chartContainer.addEventListener('click', handleMouseClick); + chartContainer.addEventListener('mousedown', handleMouseDown); + chartContainer.addEventListener('mouseenter', handleMouseEnter); + chartContainer.addEventListener('mouseup', handleMouseUp as EventListener); + chartContainer.addEventListener('mouseleave', handleMouseLeave); + chartContainer.addEventListener('wheel', handleMouseWheel); + chartContainer.addEventListener('touchstart', handleMouseDown as EventListener); + chartContainer.addEventListener('touchmove', handleMouseMove as EventListener); + chartContainer.addEventListener('touchend', handleMouseUp as EventListener); + window.addEventListener('keydown', documentKeyHandler); + chartContainer.addEventListener('keydown', chartKeyDown); + chartContainer.addEventListener('keyup', chartKeyUp); + window.addEventListener('resize', chartResize); + return () => { + // Cleanup event listeners + chartContainer.removeEventListener('mousemove', handleMouseMove as EventListener); + chartContainer.removeEventListener('click', handleMouseClick); + chartContainer.removeEventListener('mousedown', handleMouseDown); + chartContainer.removeEventListener('mouseenter', handleMouseEnter); + chartContainer.removeEventListener('mouseup', handleMouseUp as EventListener); + chartContainer.removeEventListener('mouseleave', handleMouseLeave); + chartContainer.removeEventListener('wheel', handleMouseWheel); + chartContainer.removeEventListener('touchstart', handleMouseDown as EventListener); + chartContainer.removeEventListener('touchmove', handleMouseMove as EventListener); + chartContainer.removeEventListener('touchend', handleMouseUp as EventListener); + window.removeEventListener('keydown', documentKeyHandler); + chartContainer.removeEventListener('keydown', chartKeyDown); + chartContainer.removeEventListener('keyup', chartKeyUp); + window.removeEventListener('resize', chartResize); + }; + }, [parentElement, handleMouseMove]); + + useEffect(() => { + if (phase === 'rendering') { + triggerRemeasure(); + } + }, [parentElement?.availableSize?.height, parentElement?.availableSize?.width]); + + if (layoutRef.current.chart as Chart) { + (layoutRef.current.chart as Chart).trackballRef = trackballRef; + } + + const availableSize: ChartSizeProps = parentElement?.availableSize; + return ( + render && + + + {chartTitle.text && + + } + {chartSubTitle.text && + + } + {chartLegend.visible && + + } + + + + + {striplineVisibility && + + } + {(chartSeries.length > 0) && + + } + + {striplineVisibility && + + } + {chartStackLabels.visible && + + } + {(chartZoom.selectionZoom || chartZoom.mouseWheelZoom || chartZoom.pinchZoom) && + <> + + + + } + {chartLegend.visible && + < CustomLegendRenderer ref={legendRef} {...chartLegend}> + } + {chartTooltip.enable && + + } + {chartTooltip.enable && + + } + + + ); +}; + +/** + * Custom React hook to access the layout context. + * Provides layout-related state and functions such as phase tracking, + * animation control, and chart measurement utilities. + * + * @returns {LayoutContextType} The current layout context object. + */ +export const useLayout: () => LayoutContextType = () => { + const ctx: LayoutContextType = useContext(LayoutContext) as LayoutContextType; + return ctx; +}; + +/** + * Triggers a click event on an axis label within a chart. + * + * @param {PointerEvent | TouchEvent} e - The event object representing the pointer or touch interaction. + * @param {Chart} chart - The chart instance to which the axis label belongs. + * @returns {void} + * @private + */ +export function triggerAxisLabelClickEvent(e: PointerEvent | TouchEvent, chart: Chart): void { + const targetElement: Element = e.target as Element; + const clickEvt: PointerEvent = e as PointerEvent; + if (targetElement.id.indexOf('_AxisLabel_') !== -1) { + const index: string[] = targetElement.id.split('_AxisLabel_'); + const axisIndex: number = +index[0].slice(-1); + const labelIndex: number = +index[1]; + const currentAxis: AxisModel = chart.axisCollection[axisIndex as number]; + if (currentAxis.visible) { + const argsData: AxisLabelClickEvent = { + axisName: currentAxis.name as string, + text: currentAxis.visibleLabels[labelIndex as number].text as string, + index: labelIndex, + location: { x: clickEvt.pageX, y: clickEvt.pageY }, + value: currentAxis.visibleLabels[labelIndex as number].value + }; + chart.chartProps.onAxisLabelClick?.(argsData); + } + } +} + +/** + * Returns a valid index within the bounds of a collection. + * If the index exceeds the upper bound, it wraps to 0. + * If the index is below 0, it wraps to the last item. + * + * @param {number} index - The desired index to validate. + * @param {number} totalLength - The total number of items in the collection. + * @returns {number} A valid index within the range [0, totalLength - 1]. + */ +export const getActualIndex: (index: number, totalLength: number) => number = (index: number, totalLength: number) => { + return index > totalLength - 1 ? 0 : (index < 0 ? totalLength - 1 : index); +}; + +/** + * Updates the tabindex attributes to manage focus between two HTML elements. + * Removes the tabindex from the previously focused element and sets it on the current one + * to ensure proper keyboard navigation and accessibility. + * + * @param {HTMLElement} previousElement - The element that previously had focus. + * @param {HTMLElement} currentElement - The element to receive focus. + * @returns {void} This function does not return a value. + */ + +export const setTabIndex: (previousElement: HTMLElement, currentElement: HTMLElement) => void = + (previousElement: HTMLElement, currentElement: HTMLElement) => { + if (previousElement) { + previousElement.removeAttribute('tabindex'); + } + if (currentElement) { + currentElement.setAttribute('tabindex', '0'); + } + }; + +/** + * Sets focus on the specified HTML element and applies a focus-related CSS class. + * Ensures the element is keyboard-focusable and visually marked as focused. + * + * @param {HTMLElement} element - The target HTML element to focus. + * @returns {string} The ID of the focused element. + */ +export const focusChild: (element: HTMLElement) => string = (element: HTMLElement) => { + element?.setAttribute('tabindex', '0'); + let className: string = element?.getAttribute('class') as string; + element?.setAttribute('tabindex', '0'); + if (className && className.indexOf('sf-chart-focused') === -1) { + className = 'sf-chart-focused ' + className; + } else if (!className) { + className = 'sf-chart-focused'; + } + element?.setAttribute('class', className); + element?.focus(); + return element?.id; +}; diff --git a/components/charts/src/chart/renderer/AxesRenderer/AxisOutsideRenderer.tsx b/components/charts/src/chart/renderer/AxesRenderer/AxisOutsideRenderer.tsx new file mode 100644 index 0000000..60dc33f --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/AxisOutsideRenderer.tsx @@ -0,0 +1,84 @@ +import { useEffect, useState } from 'react'; +import { useLayout } from '../../layout/LayoutContext'; +import { useAxisOutsideRendereVersion } from '../../hooks/useClipRect'; +import { AxisModel, Chart } from '../../chart-area/chart-interfaces'; + + +/** + * Renders axis elements that are positioned outside the main chart area. + * This includes labels, tick lines, and borders for axes that are not rendered within the chart bounds. + * + * @returns {Element} - Returns the JSX elements representing axis labels and tick lines. + */ +export const AxisOutsideRenderer: React.FC = () => { + const { layoutRef, phase } = useLayout(); + const chart: Chart = layoutRef.current.chart as Chart; + + const [, setTrigger] = useState(0); + const axesChanged: number = useAxisOutsideRendereVersion(); + + /** + * React effect hook that triggers an update to the `trigger` state + * whenever the `axesChanged` dependency changes, but only if the current phase is not 'measuring'. + * + * @remarks + * This is typically used to re-render or recalculate chart layout or measurements + * after axis changes, excluding the measuring phase to avoid unnecessary updates. + * + * @dependency axesChanged - A flag or value indicating that the chart axes have changed. + */ + useEffect(() => { + if (phase !== 'measuring') { + setTrigger((prev: number) => prev + 1); + } + }, [axesChanged]); + // Don't render while measuring + if (phase === 'measuring') { + return null; + } + return ( + + {chart.axisCollection.map((axis: AxisModel, idx: number) => { + return ( + + + {axis.visible && axis.orientation === 'Vertical' && ( + <> + + + + + + {axis.labelStyle.position === 'Inside' && axis.labelElement} + {axis.tickPosition === 'Inside' && axis.majorTickLineElement} + {axis.tickPosition === 'Inside' && axis.minorTickLineElement} + {axis.labelStyle.position === 'Inside' && axis.borderElement} + + )} + + {axis.visible && axis.orientation === 'Horizontal' && ( + <> + + + + + + {axis.labelStyle.position === 'Inside' && axis.labelElement} + {axis.tickPosition === 'Inside' && axis.majorTickLineElement} + {axis.tickPosition === 'Inside' && axis.minorTickLineElement} + {axis.labelStyle.position === 'Inside' && axis.borderElement} + + )} + + ); + })} + + ); +}; diff --git a/components/charts/src/chart/renderer/AxesRenderer/AxisRender.tsx b/components/charts/src/chart/renderer/AxesRenderer/AxisRender.tsx new file mode 100644 index 0000000..98e6cf7 --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/AxisRender.tsx @@ -0,0 +1,571 @@ +import * as React from 'react'; +import { useCallback, useEffect, useLayoutEffect, useState } from 'react'; +import { useLayout } from '../../layout/LayoutContext'; +import { ChartSeriesProps, Column, Row } from '../../base/interfaces'; +import { drawXAxisLabels, drawYAxisLabels, drawAxisTitle, drawBottomLines, drawMajorGridLines, drawXAxisMinorGridLines, drawXAxisMinorTicks, drawXAxisTickLines, drawYAxisMinorGridLines, drawYAxisMinorTicks, drawYAxisTickLines, measureAxis, renderAxis, drawAxisLine } from './CartesianLayoutRender'; +import { extend } from '@syncfusion/react-base'; +import { calculateStackValues, pushCategoryData } from '../SeriesRenderer/ProcessData'; +import { useAxisRenderVersion, useClipRectSetter, useRegisterAxesRender, useRegisterAxieOutsideRender } from '../../hooks/useClipRect'; +import { AxisModel, Chart, ColumnProps, Rect, RowProps, SeriesProperties, VisibleRangeProps } from '../../chart-area/chart-interfaces'; +import { isSecondaryAxis } from '../LegendRenderer/CommonLegend'; + +/** + * Represents the properties required to configure chart axes. + * + * @private + */ +export type ChartAxesProps = { + /** + * An array of axis models that define the configuration for each axis in the chart. + */ + axes: AxisModel[]; +}; + +// cubic easing function for smooth animation +const ease: (t: number) => number = (t: number) => 1 - Math.pow(1 - t, 3); + +/** + * A React functional component responsible for rendering chart axes. + * + * @param {AxisModel[]} axes - An array of AxisModel objects representing the chart's axes. + * @returns {Element} The rendered axis elements. + */ +export const AxisRenderer: React.FC = ({ axes }: { axes: AxisModel[] }) => { + const { layoutRef, phase, setLayoutValue, reportMeasured } = useLayout(); + const chart: Chart = layoutRef.current.chart as Chart; + const setClipRect: ((clipRect: Rect) => void) | null = useClipRectSetter(); + const [axisVisibleRanges, setAxisVisibleRanges] = useState(() => { + const initial: { [key: string]: VisibleRangeProps } = {}; + chart?.axisCollection?.forEach((axis: AxisModel) => { + initial[axis.name as string] = axis.visibleRange; + }); + return initial; + }); + + const axisInfo: { version: number; id: string } = useAxisRenderVersion(); + // Animate from old axis range to new axis range; + const animateYAxis: (axes: AxisModel[]) => void = useCallback((axes: AxisModel[]) => { + const duration: number = !chart.delayRedraw ? 300 : 1000; + const start: number = performance.now(); + const step: (now: number) => void = (now: number) => { + const elapsed: number = now - start; + const progress: number = Math.min(1, elapsed / duration); + const eased: number = ease(progress); + const visibleRangeCollection: { [key: string]: VisibleRangeProps; } = {}; + for (let i: number = 0; i < axes.length; i++) { + const axis: AxisModel = axes[i as number]; + const newAxisRange: VisibleRangeProps = axis.visibleRange; + const oldAxisRange: VisibleRangeProps = extend({}, axisVisibleRanges[axis.name as string]) as VisibleRangeProps; + const xAxis: VisibleRangeProps = { + ...oldAxisRange, minimum: oldAxisRange.minimum + (newAxisRange.minimum - oldAxisRange.minimum) * eased, + maximum: oldAxisRange.maximum + (newAxisRange.maximum - oldAxisRange.maximum) * eased, + interval: oldAxisRange.interval + (newAxisRange.interval - oldAxisRange.interval) * eased + }; + visibleRangeCollection[axis.name as string] = xAxis; + } + setAxisVisibleRanges(visibleRangeCollection); + if (progress < 1) { + requestAnimationFrame(step); + } + }; + if (chart.isGestureZooming) { + const progress: number = 1; + const eased: number = ease(progress); + const visibleRangeCollection: { [key: string]: VisibleRangeProps; } = {}; + for (let i: number = 0; i < axes.length; i++) { + const axis: AxisModel = axes[i as number]; + const newAxisRange: VisibleRangeProps = axis.visibleRange; + const oldAxisRange: VisibleRangeProps = extend({}, axisVisibleRanges[axis.name as string]) as VisibleRangeProps; + const xAxis: VisibleRangeProps = { + ...oldAxisRange, minimum: oldAxisRange.minimum + (newAxisRange.minimum - oldAxisRange.minimum) * eased, + maximum: oldAxisRange.maximum + (newAxisRange.maximum - oldAxisRange.maximum) * eased, + interval: oldAxisRange.interval + (newAxisRange.interval - oldAxisRange.interval) * eased + }; + visibleRangeCollection[axis.name as string] = xAxis; + } + setAxisVisibleRanges(visibleRangeCollection); + } + else { + requestAnimationFrame(step); + } + + }, [axisVisibleRanges]); + + // Measuring phase: compute layout but render nothing. + useLayoutEffect(() => { + if (phase === 'measuring') { + const chart: Chart = layoutRef.current.chart as Chart; + measure(chart, chart.axisCollection); + calculateStackValues(chart); + measureAxis(chart.clipRect, chart); + chart.clipRect = chart.chartAxislayout.seriesClipRect; + renderAxis(chart); + setLayoutValue('ChartAxis', {}); + reportMeasured('ChartAxis'); + setAxisVisibleRanges(() => { + const initial: { [key: string]: VisibleRangeProps } = {}; + chart?.axisCollection?.forEach((axis: AxisModel) => { + initial[axis.name as string] = axis.visibleRange; + }); + return initial; + }); + } + }, [phase, layoutRef, setLayoutValue, reportMeasured]); + + + const memoizedSetClipRect: (rect: Rect) => void = useCallback((rect: Rect) => { + if (setClipRect) { + setClipRect(rect); + } + }, [setClipRect]); + + const axisReRender: (isLegendClicked?: boolean) => void = (isLegendClicked?: boolean) => { + const chart: Chart = layoutRef.current.chart as Chart; + if (!chart) { + return; + } + // Recompute axis collection + chart.axisCollection = !chart.delayRedraw || isLegendClicked ? chart.axisCollection : calculateVisibleAxis( + chart.requireInvertedAxis, + axes, + chart.visibleSeries + ); + (layoutRef.current.chart as Chart).visibleSeries?.map((series: SeriesProperties) => { + refreshAxisLabel(series); + }); + const chartRows: RowProps[] = chart.rows.map((r: Row) => { + const newRow: RowProps = extend({}, r) as RowProps; + newRow.axes = []; + return newRow; + }); + + const chartColumns: ColumnProps[] = chart.columns.map((c: Column) => { + const newColumn: ColumnProps = extend({}, c) as ColumnProps; + newColumn.axes = []; + return newColumn; + }); + chart.rows = chartRows; chart.columns = chartColumns; + measure(chart, chart.axisCollection); + calculateStackValues(chart); + measureAxis(chart.chartAreaRect, chart); + chart.clipRect = chart.chartAxislayout.seriesClipRect; + renderAxis(chart); + animateYAxis(chart.axisCollection); + if (setClipRect) { + memoizedSetClipRect({ + x: chart.clipRect.x, y: chart.clipRect.y, + width: chart.clipRect.width, height: chart.clipRect.height + }); + } + }; + if (chart) { + chart.axisRender = axisReRender; + } + // Rendering phase: recalc axes and trigger animation on prop changes + useEffect(() => { + if (phase !== 'measuring') { + axisReRender(); + const triggerSeriesRender: (chartId?: string) => void = useRegisterAxesRender(); + triggerSeriesRender(chart?.element?.id); + } + }, [ + ...axes.flatMap((axis: AxisModel) => [ + axis.minimum, + axis.maximum, + axis.interval, + axis.titleStyle?.fontSize, + axis.labelStyle?.fontSize, + axis.valueType, + axis.majorTickLines.height, + axis.titleStyle.text, + axis.plotOffset, + axis.rangePadding, + axis.intervalType, + axis.labelStyle?.intersectAction, + axis.labelStyle?.padding, + axis.titleStyle?.padding, + axis.titleStyle?.text, + axis.labelStyle.position, + axis.labelStyle.rotationAngle, + axis.labelStyle.maxLabelWidth, + axis.majorTickLines.height, + axis.minorTickLines.height, + axis.lineStyle?.width, + axis.zoomFactor, + axis.zoomPosition, + axis.inverted, + axis.opposedPosition, + axis.labelStyle.placement, + axis.labelStyle.align, + axis.labelStyle.format, + axis.indexed + ]) + ]); + + useEffect(() => { + if (phase !== 'measuring' && axisInfo.id === (layoutRef.current.chart as Chart)?.element.id) { + axisReRender(true); + } + }, [ + axisInfo.version + ]); + + useEffect(() => { + if (phase !== 'measuring') { + axisReRender(); + } + }, [ + ...axes.flatMap((axis: AxisModel) => [ + axis.labelStyle.edgeLabelPlacement + ]) + ]); + + // Don't render while measuring + if (phase === 'measuring') { + return null; + } + + /** + * Calculates the pixel Y-coordinate on the chart for a given data value, + * based on the axis visible range and chart area. + * + * @param {number} value - The numeric data value to convert. + * @param {Rect} rect - The rectangle representing the chart's drawing area. + * @param {AxisModel} axis - The axis model used for computing the scaling. + * @returns {number} - The computed Y-coordinate in pixels. + */ + const yScale: (value: number, rect: Rect, axis: AxisModel) => number = ( + value: number, rect: Rect, axis: AxisModel) => { + const visibleRange: VisibleRangeProps = axisVisibleRanges[axis.name as string]; // yAxisRange; + const range: number = ((value - visibleRange.minimum) / (visibleRange.maximum - visibleRange.minimum)); + return ( + axis.isAxisInverse ? (rect.y + range * rect.height) : (rect.y + rect.height - range * rect.height) + ); + }; + + /** + * Calculates the scaled x-coordinate position of a given value based on the axis's visible range + * and the provided rendering rectangle. This function respects axis inversion settings. + * + * @param {number} value - The data value to be converted to a scaled x-coordinate. + * @param {Rect} rect - The bounding rectangle within which the axis is rendered. + * @param {AxisModel} axis - The X axis model containing configuration and inversion settings. + * @returns {number} - The pixel position (x-coordinate) corresponding to the input value within the given rectangle. + */ + const xScale: (value: number, rect: Rect, axis: AxisModel) => number = ( + value: number, rect: Rect, axis: AxisModel) => { + const visibleRange: VisibleRangeProps = axisVisibleRanges[axis.name as string]; + const range: number = ((value - visibleRange.minimum) / (visibleRange.maximum - visibleRange.minimum)); + return ( + axis.isAxisInverse ? (rect.x + rect.width - range * rect.width) : (rect.x + range * rect.width) + ); + }; + const secondaryAxes: AxisModel[] = axes.slice(2); + return ( + + {chart.axisCollection.map((axis: AxisModel, idx: number) => { + let currentAxis: AxisModel = axis; + if (axis.name === 'primaryXAxis') { + currentAxis = axes[0]; + } else if (axis.name === 'primaryYAxis') { + currentAxis = axes[1]; + } else { + const secondaryAxis: AxisModel[] = secondaryAxes; + const foundAxis: AxisModel | undefined = secondaryAxis.find( + (secondaryAxis: AxisModel) => secondaryAxis.name === axis.name + ); + if (foundAxis) { + currentAxis = foundAxis; + } + } + if (axis.labelStyle.position === 'Inside' || axis.tickPosition === 'Inside') { + const triggerSeriesRender: () => void = useRegisterAxieOutsideRender(); + triggerSeriesRender(); + } + return ( + + + {axis.visible && axis.internalVisibility && axis.orientation === 'Vertical' && ( + <> + + + + + + {drawYAxisLabels(axis, idx, axis.updatedRect, chart, yScale)} + {drawMajorGridLines(idx, axis, chart, currentAxis, yScale)} + {drawYAxisTickLines(axis, idx, yScale, chart, currentAxis)} + {drawYAxisMinorGridLines(axis, idx, chart, yScale, currentAxis)} + {drawYAxisMinorTicks(axis, idx, yScale, chart, currentAxis)} + {drawAxisTitle(axis, chart, idx, currentAxis)} + {drawAxisLine(axis, currentAxis, chart)} + + )} + + {axis.visible && axis.internalVisibility && axis.orientation === 'Horizontal' && ( + <> + + + + + + {drawXAxisLabels(axis, idx, axis.updatedRect, chart, xScale)} + {drawMajorGridLines(idx, axis, chart, currentAxis, xScale)} + {drawXAxisTickLines(axis, idx, xScale, chart, currentAxis)} + {drawXAxisMinorGridLines(axis, idx, chart, xScale, currentAxis)} + {drawXAxisMinorTicks(axis, idx, xScale, chart, currentAxis)} + {drawAxisTitle(axis, chart, idx, currentAxis)} + {drawAxisLine(axis, currentAxis, chart)} + + )} + + ); + })} + {drawBottomLines(chart)} + + ); +}; + + +/** + * Custom hook that calculates and returns the visible axes based on the chart's orientation + * and the series that are currently visible. + * + * @param {boolean} requireInvertedAxis - Indicates whether the chart orientation is inverted (e.g., horizontal chart). + * @param {AxisModel[]} axisCollection - The full collection of axis models defined in the chart. + * @param {SeriesProperties[]} visibleSeries - The list of series that are currently visible in the chart. + * @returns {AxisModel[]} An array of axis models that are determined to be visible based on the current chart state. + * @private + */ +export function calculateVisibleAxis( + requireInvertedAxis: boolean, axisCollection: AxisModel[], visibleSeries: SeriesProperties[]): AxisModel[] { + const axisCollections: AxisModel[] = []; + const calculatedAxes: AxisModel[] = []; + axisCollection.map((axis: AxisModel) => { + calculatedAxes.push(extend({}, axis) as AxisModel); + }); + calculatedAxes.forEach((axis: AxisModel) => { + axis.series = []; + axis.labels = []; + axis.indexLabels = {}; + axis.axisMajorGridLineOptions = []; + axis.axisMajorTickLineOptions = []; + axis.axisMinorGridLineOptions = []; + axis.axisMinorTickLineOptions = []; + axis.axislabelOptions = []; + axis.axisTitleOptions = []; + axis.internalVisibility = true; + axis.actualRange = { minimum: 0, maximum: 0, interval: 0, delta: 0 }; + axis.doubleRange = { start: 0, end: 0, delta: 0, median: 0 }; + axis.intervalDivs = [10, 5, 2, 1]; + axis.visibleRange = { minimum: 0, maximum: 0, interval: 0, delta: 0 }; + axis.visibleLabels = []; + axis.startLabel = ''; + axis.endLabel = ''; + axis.maxLabelSize = { height: 0, width: 0 }; + axis.rect = { x: 0, y: 0, width: 0, height: 0 }; + axis.updatedRect = { x: 0, y: 0, width: 0, height: 0 }; + axis.axisLineOptions = { + id: '', + d: '', + stroke: '', + strokeWidth: 0, + dashArray: '', + fill: '' + }; + axis.series = []; + axis.paddingInterval = 0; + axis.maxPointLength = 0; + axis.isStack100 = false; + axis.titleCollection = []; + axis.titleSize = { height: 0, width: 0 }; + for (const series of visibleSeries) { + initAxis(requireInvertedAxis, series, axis, true); + } + if (axis.orientation != null) { + axisCollections.push(axis); + } + const secondaryAxes: AxisModel[] = axisCollection.slice(2); + if (isSecondaryAxis(axis, secondaryAxes)) { + axis.internalVisibility = axis.series.some((value: ChartSeriesProps) => (value.visible)); + } + }); + return axisCollections; +} + +/** + * Measures the dimensions and computes necessary positioning for chart axes. + * Adjusts axis positions and calculates specific layout measurements required for rendering. + * + * @param {Chart} chart - The chart instance containing the configuration and elements to be measured. + * @param {AxisModel[]} axes - An array of axis models to be measured and processed for layout. + * @returns {void} This function performs measurements in-place and does not return a value. + * @private + */ +export function measure(chart: Chart, axes: AxisModel[]): void { + for (const axis of axes) { + let actualIndex: number; + let span: number; + let definition: RowProps | ColumnProps; + if (axis.orientation === 'Vertical') { + chart.verticalAxes.push(axis); + actualIndex = getActualRow(chart.rows, axis); + const row: RowProps = chart.rows[actualIndex as number]; + pushAxis(row, axis); + span = Math.min(actualIndex + (axis.span as number), chart.rows.length); + for (let j: number = actualIndex + 1; j < span; j++) { + definition = { ...chart.rows[j as number] }; + definition.axes = [...(definition.axes || [])]; + definition.axes[row.axes.length - 1] = { ...axis }; + chart.rows[j as number] = definition; + } + chart.rows[actualIndex as number] = row; + } else { + actualIndex = getActualColumn(chart.columns, axis); + const column: ColumnProps = chart.columns[actualIndex as number]; + pushAxis(chart.columns[actualIndex as number], axis); + chart.horizontalAxes.push(axis); + span = Math.min(actualIndex + (axis.span as number), chart.columns.length); + for (let j: number = actualIndex + 1; j < span; j++) { + definition = { ...chart.columns[j as number] }; + definition.axes = [...(definition.axes || [])]; + definition.axes[column.axes.length - 1] = { ...axis }; + chart.columns[j as number] = definition; + } + chart.columns[actualIndex as number] = column; + } + axis.isRTLEnabled = chart.enableRtl || false; + setIsInversedAndOpposedPosition(axis); + } +} + +/** + * Initializes the orientation of the axis for a given series, determining if it should be vertical or horizontal. + * Sets the axis orientation based on the necessity to invert the axis and the association with the series. + * + * @param {boolean} requireInvertedAxis - Indicates if the axis should be set to a vertical orientation. + * @param {SeriesProperties} series - The data series which might dictate the axis orientation. + * @param {AxisModel} axis - The axis model to be initialized and oriented. + * @param {boolean} isSeries - Specifies if the axis is related to the series or not. + * @returns {void} This function modifies the axis directly and does not return a value. + * @private + */ +function initAxis(requireInvertedAxis: boolean, series: SeriesProperties, axis: AxisModel, isSeries: boolean): void { + if (series.xAxisName === axis.name || (series.xAxisName == null && axis.name === 'primaryXAxis')) { + axis.orientation = requireInvertedAxis ? 'Vertical' : 'Horizontal'; + series.xAxis = axis; + if (isSeries) { axis.series.push(series); } + } else if (series.yAxisName === axis.name || (series.yAxisName == null && axis.name === 'primaryYAxis')) { + axis.orientation = requireInvertedAxis ? 'Horizontal' : 'Vertical'; + series.yAxis = axis; + if (isSeries) { axis.series.push(series); } + } +} + +/** + * Determines the actual column index for an axis within a collection of columns. + * Validates that the column index is within the acceptable range of the supplied columns array. + * + * @param {ColumnProps[]} columns - The array of column definitions in which to locate the axis. + * @param {AxisModel} axis - The axis for which to find the correct column index. + * @returns {number} The actual column index ensuring it falls within the valid range of columns. + * @private + */ +function getActualColumn(columns: ColumnProps[], axis: AxisModel): number { + const position: number = axis.columnIndex as number; + return position >= columns.length ? columns.length - 1 : Math.max(0, position); +} + +/** + * Determines the actual row index for an axis within a collection of rows. + * Ensures the row index is within valid bounds of the provided rows array. + * + * @param {RowProps[]} rows - The array of row definitions in which to locate the axis. + * @param {AxisModel} axis - The axis for which to find the correct row index. + * @returns {number} The actual row index ensuring it falls within the valid range of rows. + * @private + */ +function getActualRow(rows: RowProps[], axis: AxisModel): number { + const position: number = axis.rowIndex as number; + return position >= rows.length ? rows.length - 1 : Math.max(0, position); +} + +/** + * Adds an axis to the specified definition object, which could represent either a row or a column. + * Finds the first empty slot in the axes array of the definition and assigns the axis to that slot. + * + * @param {RowProps | ColumnProps} definition - The row or column definition to which the axis will be added. + * @param {AxisModel} axis - The axis instance to be inserted into the definition. + * @returns {void} This function modifies the definition directly and does not return any value. + * @private + */ +function pushAxis(definition: RowProps | ColumnProps, axis: AxisModel): void { + for (let i: number = 0; i <= definition.axes.length; i++) { + if (!definition.axes[i as number]) { + definition.axes[i as number] = axis; + break; + } + } +} + +/** + * Refreshes the axis labels for category axes + * + * @param {SeriesProperties} series - The series for which to refresh axis labels + * @returns {void} + * @private + */ +export function refreshAxisLabel(series: SeriesProperties): void { + if (!series.xAxis || (series.xAxis.valueType !== 'Category')) { + return; + } + series.xAxis.labels = []; + series.xAxis.indexLabels = {}; + + for (const item of series.xAxis.series) { + if (item.visible) { + item.xMin = Infinity; + item.xMax = -Infinity; + + if (item.points) { + for (const point of item.points) { + pushCategoryData(point, point.x as string, item, point.index); + item.xMin = Math.min(item.xMin, point.xValue as number); + item.xMax = Math.max(item.xMax, point.xValue as number); + } + } + } + } +} + +/** + * Configures the axis position properties such as inverseness and whether it is in an opposed position. + * Determines the orientation to set properties that influence how the axis is displayed. + * + * @param {AxisModel} axis - The axis model to configure inverseness and opposed position properties. + * @returns {void} This function modifies axis properties directly and does not return any value. + * @private + */ +function setIsInversedAndOpposedPosition(axis: AxisModel): void { + const isVertical: boolean = axis.orientation === 'Vertical'; + const isHorizontal: boolean = axis.orientation === 'Horizontal'; + + // Determine if the axis should be opposed + axis.isAxisOpposedPosition = (axis.opposedPosition || false) || ((axis.isRTLEnabled || false) && isVertical); + if (axis.opposedPosition && axis.isRTLEnabled && isVertical) { + axis.isAxisOpposedPosition = false; + } + + // Determine if the axis should be inverted + axis.isAxisInverse = (axis.inverted || (axis.isRTLEnabled && isHorizontal)) || false; + if (axis.inverted && axis.isRTLEnabled && isHorizontal) { + axis.isAxisInverse = false; + } +} diff --git a/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/AxisUtils.tsx b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/AxisUtils.tsx new file mode 100644 index 0000000..dc87fcc --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/AxisUtils.tsx @@ -0,0 +1,294 @@ +import { isNullOrUndefined } from '@syncfusion/react-base'; +import { LabelIntersectMode, TextOverflow } from '../../../base/enum'; +import { AxisTextStyle } from '../../../chart-axis/base'; +import { getMaxRotatedTextSize, getRotatedTextSize, getTitle, isBreakLabel, isZoomSet, measureText, useTextWrap, valueToCoefficient } from '../../../utils/helper'; +import { createDoubleRange } from './DoubleAxisRenderer'; +import { AxisModel, Chart, ColumnProps, RowProps, ChartSizeProps, TextStyleModel, VisibleLabel } from '../../../chart-area/chart-interfaces'; + +/** + * Calculates the maximum width of visible labels on the specified chart axis. + * + * @param {Chart} chart - The chart instance containing the axis. + * @param {AxisModel} axis - The axis model whose visible labels' width is being calculated. + * @returns {void} This function does not return any value; it performs the calculation on the provided axis. + * @private + */ +export function getMaxLabelWidth(chart: Chart, axis: AxisModel): void { + const visibleLabels: VisibleLabel[] = axis.visibleLabels; + const action: LabelIntersectMode = axis.labelStyle.intersectAction as LabelIntersectMode; + let isIntersect: boolean = false; + let pointX: number; let previousEnd: number = 0; + axis.angle = (axis.labelStyle.rotationAngle as number) % 360; + axis.maxLabelSize = { width: 0, height: 0 }; + visibleLabels.forEach((label: VisibleLabel, i: number) => { + const isAxisLabelBreak: boolean = isBreakLabel(label.originalText); + const labelStyle: Required = label.labelStyle as Required; + if (isAxisLabelBreak) { + label.size = getMaxRotatedTextSize([label.originalText.replace(/
/g, ' ')], 0, labelStyle, chart.themeStyle.axisLabelFont); + label.breakLabelSize = getMaxRotatedTextSize([axis.labelStyle.enableTrim ? ((label.text as string[]).join('
')) : label.originalText], 0, + labelStyle, chart.themeStyle.axisLabelFont); + } else if (axis.labelStyle.enableWrap) { + const maximumLabelHeight: number = chart.chartAxislayout.initialClipRect.height / visibleLabels.length; + label.text = useTextWrap( + label.text as string, + axis.labelStyle.maxLabelWidth as number, + labelStyle, + chart.enableRtl, + chart.themeStyle.axisLabelFont, + false, + (axis.orientation === 'Vertical' ? maximumLabelHeight as number : null) as number + ); + let maxTextWidth: number = 0; + let maxTextHeight: number = 0; + label.text.forEach((textLine: string) => { + const textSize: ChartSizeProps = measureText(textLine, labelStyle, chart.themeStyle.axisLabelFont); + maxTextWidth = Math.max(maxTextWidth, textSize.width); + maxTextHeight += textSize.height; + }); + label.size.width = maxTextWidth; + label.size.height = maxTextHeight; + } else { + if ((axis.angle === -90 || axis.angle === 90 || axis.angle === 270 || axis.angle === -270) && axis.orientation === 'Vertical') { + label.size = getRotatedTextSize(label.text as string, labelStyle, axis.angle, chart.themeStyle.axisLabelFont); + } else { + label.size = measureText(label.text as string, labelStyle, chart.themeStyle.axisLabelFont); + } + } + + const labelWidth: number = isAxisLabelBreak ? label.breakLabelSize.width : label.size.width; + if (labelWidth > axis.maxLabelSize.width) { + axis.maxLabelSize.width = labelWidth; + axis.rotatedLabel = label.text as string; + } + const labelHeight: number = isAxisLabelBreak ? label.breakLabelSize.height : label.size.height; + if (labelHeight > axis.maxLabelSize.height) { + axis.maxLabelSize.height = labelHeight; + } + if (isAxisLabelBreak) { + label.text = axis.labelStyle.enableTrim ? label.text : label.originalText.split('
'); + } + if (action !== 'None' && axis.orientation === 'Horizontal' && axis.rect.width > 0 && !isIntersect) { + const newwidth: number = isAxisLabelBreak ? label.breakLabelSize.width : label.size.width; + pointX = (valueToCoefficient(label.value, axis) * axis.rect.width) + axis.rect.x; + pointX -= newwidth / 2; + + if (axis.labelStyle.edgeLabelPlacement === 'Shift') { + if (i === 0 && pointX < axis.rect.x) { + pointX = axis.rect.x; + } + if (i === visibleLabels.length - 1 && ((pointX + newwidth) > (axis.rect.x + axis.rect.width))) { + pointX = axis.rect.x + axis.rect.width - newwidth; + } + } + let height: number; + switch (action) { + case 'Hide': + case 'Trim': + break; + case 'MultipleRows': + if (i > 0) { + findMultiRows(i, axis, pointX, label, isAxisLabelBreak); + } + break; + + case 'Rotate45': + case 'Rotate90': + if (i > 0 && (!axis.isAxisInverse ? pointX <= previousEnd : pointX + newwidth >= previousEnd)) { + axis.angle = action === 'Rotate45' ? 45 : 90; + isIntersect = true; + } + break; + + default: + if (isAxisLabelBreak) { + let result: string[]; + const result1: string[] = []; + for (let index: number = 0; index < label.text.length; index++) { + result = useTextWrap( + label.text[index as number], + axis.rect.width / visibleLabels.length, + labelStyle, + chart.enableRtl, + chart.themeStyle.axisLabelFont, + false + ); + if (result.length > 1) { + result1.push(...result); + } else { + result1.push(result[0]); + } + } + label.text = result1; + } else { + label.text = useTextWrap( + label.text as string, + axis.rect.width / visibleLabels.length, + labelStyle, + chart.enableRtl, + chart.themeStyle.axisLabelFont, + false + ); + } + height = label.size.height * label.text.length; + if (height > axis.maxLabelSize.height) { + axis.maxLabelSize.height = height; + } + break; + } + previousEnd = axis.isAxisInverse ? pointX : pointX + newwidth; + } + }); + if (axis.angle !== 0 && axis.angle !== undefined && axis.orientation === 'Horizontal') { + const isHorizontalAngle: boolean = axis.angle === -360 || axis.angle === 0 || axis.angle === -180 || + axis.angle === 180 || axis.angle === 360; + if (axis.labelStyle.position === 'Outside' && !isHorizontalAngle && isBreakLabel(axis.rotatedLabel)) { + axis.maxLabelSize = { width: axis.maxLabelSize.height, height: axis.maxLabelSize.width }; + } else { + axis.maxLabelSize = getRotatedTextSize(axis.rotatedLabel, axis.labelStyle as TextStyleModel, + axis.angle, chart.themeStyle.axisLabelFont); + } + } else if (axis.angle !== 0 && axis.angle !== undefined && axis.orientation === 'Vertical') { + axis.rotatedLabel = isNullOrUndefined(axis.rotatedLabel) ? '' : axis.rotatedLabel; + const isHorizontalAngle: boolean = axis.angle === -360 || axis.angle === 0 || axis.angle === -180 || + axis.angle === 180 || axis.angle === 360; + if (axis.labelStyle.position === 'Outside' && !isHorizontalAngle && isBreakLabel(axis.rotatedLabel)) { + axis.maxLabelSize = { width: axis.maxLabelSize.height, height: axis.maxLabelSize.width }; + } else { + axis.maxLabelSize = getRotatedTextSize(axis.rotatedLabel, axis.labelStyle as TextStyleModel, + axis.angle, chart.themeStyle.axisLabelFont); + } + } +} + +/** + * Finds the maximum size required for axis labels, incorporating inner padding. + * + * @param {AxisModel} axis - The axis model containing label information. + * @param {number} innerPadding - The padding inside the chart area contributing to the label size. + * @param {RowProps | ColumnProps} definition - The row or column model defining layout properties. + * @param {Chart} chart - The chart instance containing the axis. + * @returns {number} The determined size for the axis labels considering padding and layout. + * @private + */ +export function findLabelSize(axis: AxisModel, innerPadding: number, definition: RowProps | ColumnProps, chart: Chart): number { + let titleSize: number = 0; + const isHorizontal: boolean = axis.orientation === 'Horizontal'; + if (axis.titleStyle.text) { + if (axis.titleStyle.rotationAngle === undefined) { + axis.titleSize = measureText(axis.titleStyle.text, axis.titleStyle as TextStyleModel, chart.themeStyle.axisTitleFont); + titleSize = axis.titleSize.height + innerPadding; + } + else { + axis.titleSize = getRotatedTextSize(axis.titleStyle.text, axis.titleStyle as TextStyleModel, + axis.titleStyle.rotationAngle, chart.themeStyle.axisTitleFont); + titleSize = (axis.orientation === 'Vertical' ? axis.titleSize.width : axis.titleSize.height) + innerPadding; + } + if (axis.rect.width || axis.rect.height) { + const length: number = isHorizontal ? axis.rect.width : axis.rect.height; + axis.titleCollection = getTitle( + axis.titleStyle.text, axis.titleStyle as TextStyleModel, length, chart.enableRtl, + chart.themeStyle.axisTitleFont, axis.titleStyle?.overflow as TextOverflow); + titleSize *= axis.titleCollection.length; + } + } + + const labelSize: number = titleSize + innerPadding + (axis.titleStyle.padding as number) + (axis.labelStyle.padding as number) + + (axis.orientation === 'Vertical' ? axis.maxLabelSize.width : axis.maxLabelSize.height); + const computedTitlePadding: number = ((axis.titleStyle.text !== '' && axis.titleStyle.padding !== 5) ? axis.titleStyle.padding as number : 0); + if (axis.isAxisOpposedPosition) { + definition.insideFarSizes.push(labelSize); + } else { + definition.insideNearSizes.push(labelSize); + } + + if (axis.labelStyle.position === 'Inside') { + if ((axis.isAxisOpposedPosition && definition.farSizes.length < 1) || + (!axis.isAxisOpposedPosition && definition.nearSizes.length < 1)) { + innerPadding = (axis.labelStyle.position === 'Inside' && (chart.axes.indexOf(axis) > -1)) ? -5 : 5; + return titleSize + innerPadding + computedTitlePadding; + } else { + return titleSize + innerPadding + computedTitlePadding + (axis.labelStyle.padding || 0) + + (axis.orientation === 'Vertical' ? axis.maxLabelSize.width : axis.maxLabelSize.height); + } + } + return labelSize; +} + +/** + * Refreshes the axis by resetting its graphical rectangle dimensions. + * + * @param {Chart} chart - The chart instance containing the collection of axes to be refreshed. + * @returns {void} This function does not return a value; it modifies the chart's axes in place. + * @private + */ +export function refreshAxis(chart: Chart): void { + for (const axis of chart.axisCollection) { + axis.rect = { + x: axis.isAxisOpposedPosition ? -Infinity : Infinity, + y: axis.isAxisOpposedPosition ? Infinity : -Infinity, width: 0, height: 0 + }; + axis.isStack100 = false; + } +} + +/** + * Determines the arrangement of axis labels over multiple rows, considering label breaks and alignment. + * + * @param {number} length - The total number of labels to consider for multi-row arrangement. + * @param {AxisModel} axis - The axis model containing label information and properties. + * @param {number} currentX - The current X position of the label being processed. + * @param {VisibleLabel} currentLabel - The current label being evaluated for row placement. + * @param {boolean} isBreakLabels - Indicates if the labels are allowed to break or wrap. + * @returns {void} This function does not return a value; it adjusts the label positions on the axis. + * @private + */ +function findMultiRows(length: number, axis: AxisModel, currentX: number, currentLabel: VisibleLabel, isBreakLabels: boolean): void { + const store: number[] = []; + let label: VisibleLabel; + let isMultiRows: boolean; + let pointX: number; let width2: number; + for (let i: number = length - 1; i >= 0; i--) { + label = axis.visibleLabels[i as number]; + width2 = isBreakLabels ? label.breakLabelSize.width : label.size.width; + pointX = (valueToCoefficient(label.value, axis) * axis.rect.width) + axis.rect.x; + isMultiRows = !axis.isAxisInverse ? currentX < (pointX + width2 * 0.5) : + currentX + currentLabel.size.width > (pointX - width2 * 0.5); + if (isMultiRows) { + store.push(label.index); + currentLabel.index = (currentLabel.index > label.index) ? currentLabel.index : label.index + 1; + } else { + currentLabel.index = store.indexOf(label.index) > - 1 ? currentLabel.index : label.index; + } + } + const height: number = ((isBreakLabels ? currentLabel.breakLabelSize.height : currentLabel.size.height) * currentLabel.index) + + (5 * (currentLabel.index - 1)); + if (height > axis.maxLabelSize.height) { + axis.maxLabelSize.height = height; + } +} + +/** + * Calculate the visible range for the axis. + * + * @param {AxisModel} axis - The axis model containing label information and properties. + * @returns {void} + * @private + */ +export function calculateVisibleRangeOnZooming(axis: AxisModel): void { + if (isZoomSet(axis)) { + let start: number; + let end: number; + if (!axis.isAxisInverse) { + start = axis.actualRange.minimum + (axis.zoomPosition as number) * axis.actualRange.delta; + end = start + (axis.zoomFactor as number) * axis.actualRange.delta; + } else { + start = axis.actualRange.maximum - ((axis.zoomPosition as number) * axis.actualRange.delta); + end = start - ((axis.zoomFactor as number) * axis.actualRange.delta); + } + axis.doubleRange = createDoubleRange(start, end); + axis.visibleRange = { + minimum: axis.doubleRange.start, maximum: axis.doubleRange.end, + delta: axis.doubleRange.delta, interval: axis.visibleRange.interval + }; + } +} diff --git a/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/CategoryAxisRenderer.tsx b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/CategoryAxisRenderer.tsx new file mode 100644 index 0000000..0913a90 --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/CategoryAxisRenderer.tsx @@ -0,0 +1,132 @@ +import { extend, isNullOrUndefined } from '@syncfusion/react-base'; +import { ChartFontProps } from '../../../base/interfaces'; +import { getActualDesiredIntervalsCount, withIn } from '../../../utils/helper'; +import { calculateRange, calculateVisibleRange, createDoubleRange, initializeDoubleRange, triggerLabelRender, DoubleRange } from './DoubleAxisRenderer'; +import { getMaxLabelWidth } from './AxisUtils'; +import { AxisModel, Chart, SeriesProperties, ChartSizeProps} from '../../../chart-area/chart-interfaces'; +import { ChartSeriesType } from '../../../base/enum'; + +/** + * Calculates the range and interval for a Category axis within a chart. + * This involves adjusting the axis properties based on the chart size and configuration. + * + * @param {ChartSizeProps} size - The dimensions of the chart area, which influence axis calculations. + * @param {AxisModel} axis - The axis model to be calculated, containing data and settings. + * @param {Chart} chart - The chart instance that includes the axis and other relevant properties. + * @returns {void} This function modifies axis properties related to range and interval without returning a value. + * @private + */ +export function calculateCategoryAxis(size: ChartSizeProps, axis: AxisModel, chart: Chart): void { + calculateRangeAndInterval(size, axis, chart); +} + +/** + * The function to calculate the range and labels for the axis. + * + * @private + * @returns {void} + */ + +/** + * Calculates the range and interval for the category axis within a chart. + * This involves adjusting the axis properties based on the chart size and configuration. + * + * @param {Size} size - The dimensions of the chart area, which influence axis calculations. + * @param {AxisModel} axis - The axis model to be calculated, containing data and settings. + * @param {Chart} chart - The chart instance that includes the axis and other relevant properties. + * @returns {void} This function modifies axis properties related to range and interval without returning a value. + * @private + */ +function calculateRangeAndInterval(size: ChartSizeProps, axis: AxisModel, chart: Chart): void { + const categoryRange: DoubleRange = { + min: null, + max: null + }; + calculateRange(axis, chart, categoryRange as Required); + getActualRange(axis, size, categoryRange as Required); + applyRangePadding(axis, size); + calculateVisibleLabels(axis); +} + +/** + * Calculates the actual range for the category axis. + * + * @param {AxisModel} axis - The axis model containing axis settings. + * @param {Size} size - The size of the chart area. + * @param {DoubleRange} categoryRange - The calculated range for categories. + * @returns {void} Does not return a value. + * @private + */ +export function getActualRange(axis: AxisModel, size: ChartSizeProps, categoryRange: DoubleRange): void { + initializeDoubleRange(axis, categoryRange as Required); + if (!axis.interval) { + axis.actualRange.interval = Math.max(1, Math.floor(axis.doubleRange.delta / getActualDesiredIntervalsCount(size, axis))); + } else { + axis.actualRange.interval = Math.ceil(axis.interval); + } + axis.actualRange.minimum = axis.doubleRange.start; + axis.actualRange.maximum = axis.doubleRange.end; + axis.actualRange.delta = axis.doubleRange.delta; +} + +/** + * Padding for the axis. + * + * @param {AxisModel} axis - The axis for which padding is applied. + * @param {ChartSizeProps} size - The size of the chart area. + * @returns {void} + * @private + */ +export function applyRangePadding(axis: AxisModel, size: ChartSizeProps): void { + let hasColumnOrBarSeries: boolean = false; + const AXIS_OFFSET: number = 0.5; + axis.series.forEach((element: SeriesProperties) => { + if (!hasColumnOrBarSeries) { hasColumnOrBarSeries = ((element.type as Required).indexOf('Column') > -1 || (element.type as Required).indexOf('Bar') > -1) && !(axis.zoomFactor as Required < 1 || axis.zoomPosition as Required > 0) && isNullOrUndefined(axis.minimum) && isNullOrUndefined(axis.maximum); } + }); + const shouldOffsetTicks: boolean = axis.labelStyle.placement === 'BetweenTicks' || hasColumnOrBarSeries; + const ticks: number = shouldOffsetTicks ? AXIS_OFFSET : 0; + if (ticks > 0) { + axis.actualRange.minimum -= ticks; + axis.actualRange.maximum += ticks; + } else { + axis.actualRange.maximum += axis.actualRange.maximum ? 0 : AXIS_OFFSET; + } + axis.doubleRange = createDoubleRange(axis.actualRange.minimum, axis.actualRange.maximum); + axis.actualRange.delta = axis.doubleRange.delta; + calculateVisibleRange(axis, size); +} + + +/** + * Calculates and generates visible labels for the axis. + * + * @param {AxisModel} axis - The axis for which labels are generated. + * @returns {void} Does not return a value. + * @private + */ +export function calculateVisibleLabels(axis: AxisModel): void { + /** Generate axis labels */ + axis.visibleLabels = []; + axis.visibleRange.interval = axis.visibleRange.interval < 1 ? 1 : axis.visibleRange.interval; + let tempInterval: number = Math.ceil(axis.visibleRange.minimum); + let labelStyle: ChartFontProps; + if (axis.zoomFactor as Required < 1 || axis.zoomPosition as Required > 0) { + tempInterval = axis.visibleRange.minimum - (axis.visibleRange.minimum % axis.visibleRange.interval); + } + let position: number; + axis.startLabel = axis.labels[Math.round(axis.visibleRange.minimum)]; + axis.endLabel = axis.labels[Math.floor(axis.visibleRange.maximum)]; + for (; tempInterval <= axis.visibleRange.maximum; tempInterval += axis.visibleRange.interval) { + labelStyle = (extend({}, axis.labelStyle, undefined, true)); + if (withIn(tempInterval, axis.visibleRange) && axis.labels.length > 0) { + position = Math.round(tempInterval); + triggerLabelRender( + position, + axis.labels[position as number] ? axis.labels[position as number].toString() : '', + labelStyle, axis + ); + } + } + getMaxLabelWidth(axis.chart, axis); +} + diff --git a/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/DateTimeAxisRenderer.tsx b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/DateTimeAxisRenderer.tsx new file mode 100644 index 0000000..cad242b --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/DateTimeAxisRenderer.tsx @@ -0,0 +1,661 @@ +import { getDateFormat, getDateParser } from '@syncfusion/react-base/src/internationalization'; +import { ChartFontProps } from '../../../base/interfaces'; +import { DateFormatOptions } from '../../../chart-axis/base'; +import { calculateNumericNiceInterval, calculateRange, createDoubleRange, getRangePadding, triggerLabelRender, triggerRangeRender, DoubleRange } from './DoubleAxisRenderer'; +import { DataUtil } from '@syncfusion/react-data/src/util'; +import { firstToLowerCase, isZoomSet, setRange, withIn } from '../../../utils/helper'; +import { ChartRangePadding, IntervalType } from '../../../base/enum'; +import { extend, isNullOrUndefined } from '@syncfusion/react-base'; +import { calculateVisibleRangeOnZooming, getMaxLabelWidth } from './AxisUtils'; +import { AxisModel, Chart, ChartSizeProps, VisibleRangeProps } from '../../../chart-area/chart-interfaces'; + +/** + * Calculates the range and interval for a DateTime axis within a chart. + * This involves adjusting the axis properties based on the chart size and configuration. + * + * @param {Size} size - The dimensions of the chart area, which influence axis calculations. + * @param {AxisModel} axis - The axis model to be calculated, containing data and settings. + * @param {Chart} chart - The chart instance that includes the axis and other relevant properties. + * @returns {void} This function modifies axis properties related to range and interval without returning a value. + * @private + */ +export function calculateDateTimeAxis(size: ChartSizeProps, axis: AxisModel, chart: Chart): void { + calculateRangeAndInterval(size, axis, chart); +} + +/** + * Calculates the range and interval for an axis based on the provided chart size and configuration. + * This adjusts the axis range and interval properties according to the chart's needs. + * + * @param {Size} size - The overall size of the chart, affecting the range and interval calculations. + * @param {AxisModel} axis - The axis model for which the range and interval are calculated. + * @param {Chart} chart - The chart context that contains the axis and its configuration. + * @returns {void} This function updates properties on the axis for range and interval; it does not return a value. + * @private + */ +function calculateRangeAndInterval(size: ChartSizeProps, axis: AxisModel, chart: Chart): void { + const dateTimeRange: DoubleRange = { + min: null, + max: null + }; + calculateRange(axis, chart, dateTimeRange as Required); + getActualRange(axis, size, dateTimeRange as Required); + applyRangePadding(axis, size, dateTimeRange as Required); + calculateVisibleLabels(axis, axis.chart); +} + +/** + * Calculates the actual range for the DateTime axis. + * + * @private + * @param {AxisModel} axis - The axis for which the actual range is calculated. + * @param {Size} size - The size used for calculation. + * @param {DoubleRange} dateTimeRange - The range for datetime calculations. + * @returns {void} + * @private + */ +function getActualRange(axis: AxisModel, size: ChartSizeProps, dateTimeRange: DoubleRange): void { + const option: DateFormatOptions = { + skeleton: 'full', + type: 'dateTime', + locale: axis.chart.locale + }; + const dateParser: Function = getDateParser(option); + const dateFormatter: Function = getDateFormat(option); + // Axis min + if (!isNullOrUndefined(axis.minimum)) { + dateTimeRange.min = Date.parse(dateParser(dateFormatter(new Date( + (DataUtil.parse as Required).parseJson({ val: axis.minimum }).val + )))); + } else if (isNullOrUndefined(dateTimeRange.min) || dateTimeRange.min === Number.POSITIVE_INFINITY) { + dateTimeRange.min = Date.parse(dateParser(dateFormatter(new Date(1970, 1, 1)))); + } + // Axis Max + if (!isNullOrUndefined(axis.maximum)) { + dateTimeRange.max = Date.parse(dateParser(dateFormatter(new Date( + (DataUtil.parse as Required).parseJson({ val: axis.maximum }).val + )))); + } else if (isNullOrUndefined(dateTimeRange.max) || dateTimeRange.max === Number.NEGATIVE_INFINITY) { + dateTimeRange.max = Date.parse(dateParser(dateFormatter(new Date(1970, 5, 1)))); + } + + if (dateTimeRange.min === dateTimeRange.max) { + const MONTH_IN_MILLISECONDS: number = 2592000000; + dateTimeRange.max = dateTimeRange.max as Required + MONTH_IN_MILLISECONDS; + dateTimeRange.min = dateTimeRange.min as Required - MONTH_IN_MILLISECONDS; + } + axis.actualRange = {} as Required; + axis.doubleRange = createDoubleRange(dateTimeRange.min as Required, dateTimeRange.max as Required); + const datetimeInterval: number = calculateDateTimeNiceInterval(axis, size, axis.doubleRange.start, axis.doubleRange.end); + + if (!axis.interval) { + axis.actualRange.interval = datetimeInterval; + } else { + axis.actualRange.interval = axis.interval; + } + axis.actualRange.minimum = axis.doubleRange.start; + axis.actualRange.maximum = axis.doubleRange.end; +} +/** + * Apply padding for the range. + * + * @private + * @param {AxisModel} axis - The axis for which padding is applied. + * @param {Size} size - The size of the chart area. + * @param {DoubleRange} dateTimeRange - The range for which padding is applied. + * @returns {void} + */ +function applyRangePadding(axis: AxisModel, size: ChartSizeProps, dateTimeRange: DoubleRange ): void { + dateTimeRange.min = (axis.actualRange.minimum); dateTimeRange.max = (axis.actualRange.maximum); + let minimum: Date; let maximum: Date; + const interval: number = axis.actualRange.interval; + if (!setRange(axis)) { + const rangePadding: string = getRangePadding(axis, axis.chart); + minimum = new Date(dateTimeRange.min); maximum = new Date(dateTimeRange.max); + const intervalType: IntervalType = axis.actualIntervalType; + if (rangePadding === 'None') { + dateTimeRange.min = minimum.getTime(); + dateTimeRange.max = maximum.getTime(); + } else if (rangePadding === 'Additional' || rangePadding === 'Round') { + switch (intervalType) { + case 'Years': + getYear(minimum, maximum, rangePadding, interval, dateTimeRange); + break; + case 'Months': + getMonth(minimum, maximum, rangePadding, interval, dateTimeRange); + break; + case 'Days': + getDay(minimum, maximum, rangePadding, interval, dateTimeRange); + break; + case 'Hours': + getHour(minimum, maximum, rangePadding, interval, dateTimeRange); + break; + case 'Minutes': { + const minute: number = (minimum.getMinutes() / interval) * interval; + const endMinute: number = maximum.getMinutes() + (minimum.getMinutes() - minute); + if (rangePadding === 'Round') { + dateTimeRange.min = ( + new Date( + minimum.getFullYear(), minimum.getMonth(), minimum.getDate(), + minimum.getHours(), minute, 0 + ) + ).getTime(); + dateTimeRange.max = ( + new Date( + maximum.getFullYear(), maximum.getMonth(), maximum.getDate(), + maximum.getHours(), endMinute, 59 + ) + ).getTime(); + } else { + dateTimeRange.min = ( + new Date( + minimum.getFullYear(), minimum.getMonth(), minimum.getDate(), + minimum.getHours(), minute + (-interval), 0 + ) + ).getTime(); + dateTimeRange.max = ( + new Date( + maximum.getFullYear(), maximum.getMonth(), + maximum.getDate(), maximum.getHours(), endMinute + (interval), 0 + ) + ).getTime(); + } + break; + } + case 'Seconds': { + const second: number = (minimum.getSeconds() / interval) * interval; + const endSecond: number = maximum.getSeconds() + (minimum.getSeconds() - second); + if (rangePadding === 'Round') { + dateTimeRange.min = ( + new Date( + minimum.getFullYear(), minimum.getMonth(), minimum.getDate(), + minimum.getHours(), minimum.getMinutes(), second, 0 + ) + ).getTime(); + dateTimeRange.max = ( + new Date( + maximum.getFullYear(), maximum.getMonth(), maximum.getDate(), + maximum.getHours(), maximum.getMinutes(), endSecond, 0 + ) + ).getTime(); + } else { + dateTimeRange.min = ( + new Date( + minimum.getFullYear(), minimum.getMonth(), minimum.getDate(), + minimum.getHours(), minimum.getMinutes(), second + (-interval), 0 + ) + ).getTime(); + dateTimeRange.max = ( + new Date( + maximum.getFullYear(), maximum.getMonth(), maximum.getDate(), + maximum.getHours(), maximum.getMinutes(), endSecond + (interval), 0 + )).getTime(); + } + break; + } + } + } + } + axis.actualRange.minimum = !isNullOrUndefined(axis.minimum) ? dateTimeRange.min : dateTimeRange.min; + axis.actualRange.maximum = !isNullOrUndefined(axis.maximum) ? dateTimeRange.max : dateTimeRange.max; + axis.actualRange.delta = (axis.actualRange.maximum - axis.actualRange.minimum); + axis.doubleRange = createDoubleRange(axis.actualRange.minimum, axis.actualRange.maximum); + calculateVisibleRange(axis, size); +} + +/** + * Calculate visible labels for the axis. + * + * @param {AxisModel} axis axis + * @param {Chart} chart chart + * @returns {void} + * @private + */ +function calculateVisibleLabels(axis: AxisModel, chart: Chart): void { + axis.visibleLabels = []; + let tempInterval: number = axis.visibleRange.minimum; + let labelStyle: ChartFontProps; + let startValue: number = 0; + if (isNullOrUndefined(axis.minimum)) { + tempInterval = alignRangeStart(axis, tempInterval, axis.visibleRange.interval).getTime(); + } + if (startValue && startValue < tempInterval) { + tempInterval = startValue; + } + else { + startValue = tempInterval; + } + while (tempInterval <= axis.visibleRange.maximum) { + labelStyle = (extend({}, axis.labelStyle, undefined, true)); + const option: DateFormatOptions = { + locale: axis.chart.locale, + format: findCustomFormats(axis), + type: firstToLowerCase(axis.skeletonType as string), + skeleton: getSkeleton(axis) + }; + axis.format = getDateFormat(option); + axis.startLabel = axis.format(new Date(axis.visibleRange.minimum)); + axis.endLabel = axis.format(new Date(axis.visibleRange.maximum)); + if (withIn(tempInterval, axis.visibleRange)) { + const interval: number = increaseDateTimeInterval(axis, tempInterval, axis.visibleRange.interval).getTime(); + if (interval > axis.visibleRange.maximum) { + axis.endLabel = axis.format(new Date(tempInterval)); + } + triggerLabelRender(tempInterval, axis.format(new Date(tempInterval)), labelStyle, axis); + } + const actualInterval: number = tempInterval; + tempInterval = increaseDateTimeInterval(axis, tempInterval, axis.visibleRange.interval).getTime(); + if (actualInterval === tempInterval) { + break; + } + } + //tooltip and crosshair formats for 'Months' and 'Days' interval types + if ((axis.actualIntervalType === 'Months' || axis.actualIntervalType === 'Days')) { + const option: DateFormatOptions = { + locale: chart.locale, + format: axis.labelStyle.format || (axis.actualIntervalType === 'Months' && !axis.skeleton ? 'y MMM' : ''), + type: firstToLowerCase(axis.skeletonType as string), + skeleton: axis.skeleton || (axis.actualIntervalType === 'Days' ? 'MMMd' : '') + }; + axis.format = getDateFormat(option); + } + getMaxLabelWidth(chart, axis); + +} +/** + * Aligns the range start based on the axis and interval size. + * + * @param {AxisModel} axis - The axis model containing axis settings and interval type information. + * @param {number} sDate - The start date in milliseconds to be aligned. + * @param {number} intervalSize - The size of the interval used for alignment calculation. + * @returns {Date} The aligned start date based on the interval type and size. + * @private + */ +function alignRangeStart(axis: AxisModel, sDate: number, intervalSize: number): Date { + let sResult: Date = new Date(sDate); + switch (axis.actualIntervalType) { + case 'Years': { + const year: number = Math.floor(Math.floor(sResult.getFullYear() / intervalSize) * intervalSize); + sResult = new Date(year, sResult.getMonth(), sResult.getDate(), 0, 0, 0); + return sResult; + } + case 'Months': { + const month: number = Math.floor(Math.floor((sResult.getMonth()) / intervalSize) * intervalSize); + sResult = new Date(sResult.getFullYear(), month, sResult.getDate(), 0, 0, 0); + return sResult; + } + case 'Days': { + const day: number = Math.floor(Math.floor((sResult.getDate()) / intervalSize) * intervalSize); + sResult = new Date(sResult.getFullYear(), sResult.getMonth(), day, 0, 0, 0); + return sResult; + } + case 'Hours': { + const hour: number = Math.floor(Math.floor((sResult.getHours()) / intervalSize) * intervalSize); + sResult = new Date(sResult.getFullYear(), sResult.getMonth(), sResult.getDate(), hour, 0, 0); + return sResult; + } + case 'Minutes': { + const minutes: number = Math.floor(Math.floor((sResult.getMinutes()) / intervalSize) * intervalSize); + sResult = new Date(sResult.getFullYear(), sResult.getMonth(), sResult.getDate(), sResult.getHours(), minutes, 0, 0); + return sResult; + } + case 'Seconds': { + const seconds: number = Math.floor(Math.floor((sResult.getSeconds()) / intervalSize) * intervalSize); + sResult = new Date( + sResult.getFullYear(), sResult.getMonth(), sResult.getDate(), + sResult.getHours(), sResult.getMinutes(), seconds, 0 + ); + return sResult; + } + } + return sResult; +} + +/** + * Method to calculate numeric datetime interval. + * + * @param {AxisModel} axis - The axis for which to calculate the interval. + * @param {Size} size - The size of the axis. + * @param {number} start - The start value of the axis. + * @param {number} end - The end value of the axis. + * @returns {number} - The calculated numeric datetime interval. + * @private + */ +export function calculateDateTimeNiceInterval(axis: AxisModel, size: ChartSizeProps, start: number, end: number): number { + const oneDay: number = 24 * 60 * 60 * 1000; + const DAYS_IN_YEAR: number = 365; + const DAYS_IN_MONTH: number = 30; + const startDate: Date = new Date(start); + const endDate: Date = new Date(end); + //var axisInterval ; + const totalDays: number = (Math.abs((startDate.getTime() - endDate.getTime()) / (oneDay))); + let interval: number; + axis.actualIntervalType = axis.intervalType as IntervalType; + const type: IntervalType = axis.intervalType as IntervalType; + switch (type) { + case 'Years': + interval = calculateNumericNiceInterval(axis, totalDays / DAYS_IN_YEAR, size); + break; + case 'Months': + interval = calculateNumericNiceInterval(axis, totalDays / DAYS_IN_MONTH, size); + break; + case 'Days': + interval = calculateNumericNiceInterval(axis, totalDays, size); + break; + case 'Hours': + interval = calculateNumericNiceInterval(axis, totalDays * 24, size); + break; + case 'Minutes': + interval = calculateNumericNiceInterval(axis, totalDays * 24 * 60, size); + break; + case 'Seconds': + interval = calculateNumericNiceInterval(axis, totalDays * 24 * 60 * 60, size); + break; + case 'Auto': + interval = calculateNumericNiceInterval(axis, totalDays / DAYS_IN_YEAR, size); + if (interval >= 1) { + axis.actualIntervalType = 'Years'; + return interval; + } + + interval = calculateNumericNiceInterval(axis, totalDays / DAYS_IN_MONTH, size); + if (interval >= 1) { + axis.actualIntervalType = 'Months'; + return interval; + } + + interval = calculateNumericNiceInterval(axis, totalDays / 7, size); + + interval = calculateNumericNiceInterval(axis, totalDays, size); + if (interval >= 1) { + axis.actualIntervalType = 'Days'; + return interval; + } + + interval = calculateNumericNiceInterval(axis, totalDays * 24, size); + if (interval >= 1) { + axis.actualIntervalType = 'Hours'; + return interval; + } + + interval = calculateNumericNiceInterval(axis, totalDays * 24 * 60, size); + if (interval >= 1) { + axis.actualIntervalType = 'Minutes'; + return interval; + } + + interval = calculateNumericNiceInterval(axis, totalDays * 24 * 60 * 60, size); + axis.actualIntervalType = 'Seconds'; + return interval; + } + return interval; +} + +/** + * Calculate visible range for axis. + * + * @private + * @param {AxisModel} axis - The axis for which the visible range is calculated. + * @param {Size} size - The size of the chart area. + * @returns {void} + */ +function calculateVisibleRange(axis: AxisModel, size: ChartSizeProps): void { + + axis.visibleRange = { + minimum: axis.actualRange.minimum, + maximum: axis.actualRange.maximum, + interval: axis.actualRange.interval, + delta: axis.actualRange.delta + }; + //const isLazyLoad : boolean = isNullOrUndefined(axis.zoomingScrollBar) ? false : axis.zoomingScrollBar.isLazyLoad; + if (isZoomSet(axis)) { + calculateVisibleRangeOnZooming(axis); + axis.visibleRange.interval = calculateDateTimeNiceInterval(axis, size, axis.visibleRange.minimum, axis.visibleRange.maximum); + } + axis.dateTimeInterval = increaseDateTimeInterval(axis, axis.visibleRange.minimum, axis.visibleRange.interval).getTime() + - axis.visibleRange.minimum; + triggerRangeRender(axis.visibleRange.minimum, axis.visibleRange.maximum, axis.visibleRange.interval, axis); +} + +/** + * Increase the date-time interval. + * + * @param {AxisModel} axis - The axis for which the interval is increased. + * @param {number} value - The value of the interval. + * @param {number} interval - The interval to increase. + * @returns {Date} - The increased date-time interval. + * @private + */ +function increaseDateTimeInterval(axis: AxisModel, value: number, interval: number): Date { + let result: Date = new Date(value); + if (axis.interval) { + axis.isIntervalInDecimal = (interval % 1) === 0; + axis.visibleRange.interval = interval; + } else { + interval = Math.ceil(interval); + axis.visibleRange.interval = interval; + } + const intervalType: IntervalType = axis.actualIntervalType as IntervalType; + if (axis.isIntervalInDecimal) { + switch (intervalType) { + case 'Years': + result.setFullYear(result.getFullYear() + interval); + return result; + case 'Months': + result.setMonth(result.getMonth() + interval); + return result; + case 'Days': + result.setDate(result.getDate() + interval); + return result; + case 'Hours': + result.setHours(result.getHours() + interval); + return result; + case 'Minutes': + result.setMinutes(result.getMinutes() + interval); + return result; + case 'Seconds': + result.setSeconds(result.getSeconds() + interval); + return result; + } + } else { + result = getDecimalInterval(result, interval, intervalType); + } + return result; +} + +/** + * Calculates the decimal interval based on the given date, interval, and interval type. + * + * @param {Date} result - The initial date to adjust. + * @param {number} interval - The interval value with decimal component. + * @param {IntervalType} intervalType - The type of the interval (Years, Months, Days, Hours, Minutes, Seconds). + * @returns {Date} The adjusted date based on the interval calculation. + * @private + */ +function getDecimalInterval(result: Date, interval: number, intervalType: IntervalType): Date { + const roundValue: number = Math.floor(interval); + const decimalValue: number = interval - roundValue; + switch (intervalType) { + case 'Years': { + const month: number = Math.round(12 * decimalValue); + result.setFullYear(result.getFullYear() + roundValue); + result.setMonth(result.getMonth() + month); + return result; + } + case 'Months': { + const days: number = Math.round(30 * decimalValue); + result.setMonth(result.getMonth() + roundValue); + result.setDate(result.getDate() + days); + return result; + } + case 'Days': { + const hour: number = Math.round(24 * decimalValue); + result.setDate(result.getDate() + roundValue); + result.setHours(result.getHours() + hour); + return result; + } + case 'Hours': { + const min: number = Math.round(60 * decimalValue); + result.setHours(result.getHours() + roundValue); + result.setMinutes(result.getMinutes() + min); + return result; + } + case 'Minutes': { + const sec: number = Math.round(60 * decimalValue); + result.setMinutes(result.getMinutes() + roundValue); + result.setSeconds(result.getSeconds() + sec); + return result; + } + case 'Seconds': { + const milliSec: number = Math.round(1000 * decimalValue); + result.setSeconds(result.getSeconds() + roundValue); + result.setMilliseconds(result.getMilliseconds() + milliSec); + return result; + } + } + return result; +} + +/** + * Calculates the year boundaries based on given parameters for a DateTime axis. + * + * @param {Date} minimum - The minimum date value for the year calculation. + * @param {Date} maximum - The maximum date value for the year calculation. + * @param {ChartRangePadding} rangePadding - The type of range padding to apply ('None', 'Additional', or 'Round'). + * @param {number} interval - The interval value for year calculations. + * @param {DoubleRange} dateTimeRange - The range object to store calculated min and max values in time format. + * @returns {void} Updates the dateTimeRange object with calculated values. + * @private + */ +function getYear(minimum: Date, maximum: Date, rangePadding: ChartRangePadding, interval: number, dateTimeRange: DoubleRange): void { + const startYear: number = minimum.getFullYear(); + const endYear: number = maximum.getFullYear(); + if (rangePadding === 'Additional') { + dateTimeRange.min = (new Date(startYear - interval, 1, 1, 0, 0, 0)).getTime(); + dateTimeRange.max = (new Date(endYear + interval, 1, 1, 0, 0, 0)).getTime(); + } else { + dateTimeRange.min = new Date(startYear, 0, 0, 0, 0, 0).getTime(); + dateTimeRange.max = new Date(endYear, 11, 30, 23, 59, 59).getTime(); + } +} +/** + * Calculates the month boundaries based on given parameters for a DateTime axis. + * + * @param {Date} minimum - The minimum date value for the month calculation. + * @param {Date} maximum - The maximum date value for the month calculation. + * @param {ChartRangePadding} rangePadding - The type of range padding to apply ('None', 'Additional', or 'Round'). + * @param {number} interval - The interval value for month calculations. + * @param {DoubleRange} dateTimeRange - The range object to store calculated min and max values in time format. + * @returns {void} Updates the dateTimeRange object with calculated values. + * @private + */ +function getMonth(minimum: Date, maximum: Date, rangePadding: ChartRangePadding, interval: number, dateTimeRange: DoubleRange): void { + const month: number = minimum.getMonth(); + const endMonth: number = maximum.getMonth(); + if (rangePadding === 'Round') { + dateTimeRange.min = (new Date(minimum.getFullYear(), month, 0, 0, 0, 0)).getTime(); + dateTimeRange.max = ( + new Date( + maximum.getFullYear(), endMonth, + new Date(maximum.getFullYear(), maximum.getMonth(), 0).getDate(), 23, 59, 59 + ) + ).getTime(); + } else { + dateTimeRange.min = (new Date(minimum.getFullYear(), month + (-interval), 1, 0, 0, 0)).getTime(); + dateTimeRange.max = (new Date(maximum.getFullYear(), endMonth + (interval), endMonth === 2 ? 28 : 30, 0, 0, 0)).getTime(); + } +} +/** + * Calculates the day boundaries based on given parameters for a DateTime axis. + * + * @param {Date} minimum - The minimum date value for the day calculation. + * @param {Date} maximum - The maximum date value for the day calculation. + * @param {ChartRangePadding} rangePadding - The type of range padding to apply ('None', 'Additional', or 'Round'). + * @param {number} interval - The interval value for day calculations. + * @param {DoubleRange} dateTimeRange - The range object to store calculated min and max values in time format. + * @returns {void} Updates the dateTimeRange object with calculated values. + * @private + */ +function getDay(minimum: Date, maximum: Date, rangePadding: ChartRangePadding, interval: number, dateTimeRange: DoubleRange): void { + const day: number = minimum.getDate(); + const endDay: number = maximum.getDate(); + if (rangePadding === 'Round') { + dateTimeRange.min = (new Date(minimum.getFullYear(), minimum.getMonth(), day, 0, 0, 0)).getTime(); + dateTimeRange.max = (new Date(maximum.getFullYear(), maximum.getMonth(), endDay, 23, 59, 59)).getTime(); + } else { + dateTimeRange.min = (new Date(minimum.getFullYear(), minimum.getMonth(), day + (-interval), 0, 0, 0)).getTime(); + dateTimeRange.max = (new Date(maximum.getFullYear(), maximum.getMonth(), endDay + (interval), 0, 0, 0)).getTime(); + } +} +/** + * Calculates the hour boundaries based on given parameters for a DateTime axis. + * + * @param {Date} minimum - The minimum date value for the hour calculation. + * @param {Date} maximum - The maximum date value for the hour calculation. + * @param {ChartRangePadding} rangePadding - The type of range padding to apply ('None', 'Additional', or 'Round'). + * @param {number} interval - The interval value for hour calculations. + * @param {DoubleRange} dateTimeRange - The range object to store calculated min and max values in time format. + * @returns {void} Updates the dateTimeRange object with calculated values. + * @private + */ +function getHour(minimum: Date, maximum: Date, rangePadding: ChartRangePadding, interval: number, dateTimeRange: DoubleRange): void { + const hour: number = (minimum.getHours() / interval) * interval; + const endHour: number = maximum.getHours() + (minimum.getHours() - hour); + if (rangePadding === 'Round') { + dateTimeRange.min = (new Date(minimum.getFullYear(), minimum.getMonth(), minimum.getDate(), hour, 0, 0)).getTime(); + dateTimeRange.max = (new Date(maximum.getFullYear(), maximum.getMonth(), maximum.getDate(), endHour, 59, 59)).getTime(); + } else { + dateTimeRange.min = (new Date( + minimum.getFullYear(), minimum.getMonth(), minimum.getDate(), + hour + (-interval), 0, 0 + )).getTime(); + dateTimeRange.max = (new Date( + maximum.getFullYear(), maximum.getMonth(), maximum.getDate(), + endHour + (interval), 0, 0 + )).getTime(); + } +} + +/** + * To get the skeleton for the DateTime axis. + * + * @param {AxisModel} axis - The DateTime axis for which to get the skeleton. + * @returns {string} - The skeleton for the DateTime axis. + * @private + */ +function getSkeleton(axis: AxisModel): string { + let skeleton: string; + const intervalType: IntervalType = axis.actualIntervalType as IntervalType; + if (axis.skeleton) { + return axis.skeleton; + } + if (intervalType === 'Years') { + skeleton = (axis.valueType === 'DateTime' && axis.isIntervalInDecimal) ? 'y' : 'yMMM'; + } else if (intervalType === 'Months') { + skeleton = 'MMMd'; + } else if (intervalType === 'Days') { + skeleton = (axis.valueType === 'DateTime' ? 'MMMd' : 'yMd'); + } else if (intervalType === 'Hours') { + skeleton = (axis.valueType === 'DateTime' ? 'Hm' : 'EHm'); + } else if (intervalType === 'Minutes') { + skeleton = 'Hms'; + } else { + skeleton = 'Hms'; + } + return skeleton; +} + +/** + * Finds the appropriate label format for the DateTime axis. + * + * @param {AxisModel} axis - The axis model containing formatting preferences. + * @returns {string} The determined label format string or empty string if no format is specified. + * @private + */ +function findCustomFormats(axis: AxisModel): string { + let labelFormat: string = axis.labelStyle.format ? axis.labelStyle.format : ''; + if (!axis.skeleton && axis.actualIntervalType === 'Months' && !labelFormat) { + labelFormat = axis.valueType === 'DateTime' ? 'MMM yyyy' : 'yMMM'; + } + return labelFormat; +} diff --git a/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/DoubleAxisRenderer.tsx b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/DoubleAxisRenderer.tsx new file mode 100644 index 0000000..48cd286 --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/DoubleAxisRenderer.tsx @@ -0,0 +1,642 @@ +import { extend, getNumberFormat, isNullOrUndefined, NumberFormatOptions } from '@syncfusion/react-base'; +import { ChartRangePadding } from '../../../base/enum'; +import { getActualDesiredIntervalsCount, getMinPointsDelta, lineBreakLabelTrim, logBase, setRange, useTextTrim, withIn } from '../../../utils/helper'; +import { calculateVisibleRangeOnZooming, getMaxLabelWidth } from './AxisUtils'; +import { AxisLabelContentFunction, AxisTextStyle } from '../../../chart-axis/base'; +import { AxisModel, Chart, DoubleRangeType, SeriesProperties, ChartSizeProps, TextStyleModel } from '../../../chart-area/chart-interfaces'; + +let isColumn: number = 0; +let isStacking: boolean | undefined = false; + +/** + * Represents a numeric range with a minimum and maximum value. + * + * @private + */ +export interface DoubleRange { + /** + * The minimum value of the range. + * Can be a number or null if not set. + */ + min: number | null; + + /** + * The maximum value of the range. + * Can be a number or null if not set. + */ + max: number | null; +} + +/** + * Calculates a "nice" numeric interval for axis rendering, based on the axis model, delta, and size. + * + * @param {AxisModel} axis - The model representing axis data and configurations. + * @param {number} delta - The difference between the maximum and minimum data values. + * @param {ChartSizeProps} size - The size of the chart or axis area to help determine intervals. + * @returns {number} A numeric value representing a nicely calculated interval for rendering. + * @private + */ +export function calculateNumericNiceInterval(axis: AxisModel, delta: number, size: ChartSizeProps): number { + const actualDesiredIntervalsCount: number = getActualDesiredIntervalsCount(size, axis); + let niceInterval: number = delta / actualDesiredIntervalsCount; + if (!isNullOrUndefined(axis.desiredIntervals)) { + if (isAutoIntervalOnBothAxis(axis)) { + return niceInterval; + } + } + + const minInterval: number = Math.pow(10, Math.floor(logBase(niceInterval, 10))); + for (const interval of axis.intervalDivs) { + const currentInterval: number = minInterval * interval; + if (actualDesiredIntervalsCount < (delta / currentInterval)) { + break; + } + niceInterval = currentInterval; + } + return niceInterval; +} + +/** + * Determines whether the auto interval calculation is enabled on both axes. + * This function checks if auto intervals are used based on the zoom settings. + * + * @param {AxisModel} axis - The axis model that includes zoom factors and interval settings. + * @returns {boolean} A boolean value indicating if auto intervals are applied on both axes. + * @private + */ +function isAutoIntervalOnBothAxis(axis: AxisModel): boolean { + return !(((axis.zoomFactor ?? 1) < 1 || (axis.zoomPosition ?? 0) > 0)); +} + +/** + * Calculates the range and interval for a double axis within a chart. + * This involves adjusting the axis properties based on the chart size and configuration. + * + * @param {ChartSizeProps} size - The dimensions of the chart area, which influence axis calculations. + * @param {AxisModel} axis - The axis model to be calculated, containing data and settings. + * @param {Chart} chart - The chart instance that includes the axis and other relevant properties. + * @returns {void} This function modifies axis properties related to range and interval without returning a value. + * @private + */ +export function calculateDoubleAxis(size: ChartSizeProps, axis: AxisModel, chart: Chart): void { + calculateRangeAndInterval(size, axis, chart); +} + +/** + * Calculates the range and interval for an axis based on the provided chart size and configuration. + * This adjusts the axis range and interval properties according to the chart's needs. + * + * @param {ChartSizeProps} size - The overall size of the chart, affecting the range and interval calculations. + * @param {AxisModel} axis - The axis model for which the range and interval are calculated. + * @param {Chart} chart - The chart context that contains the axis and its configuration. + * @returns {void} This function updates properties on the axis for range and interval; it does not return a value. + * @private + */ +function calculateRangeAndInterval(size: ChartSizeProps, axis: AxisModel, chart: Chart): void { + const doubleRange: DoubleRange = { + min: null, + max: null + }; + calculateRange(axis, chart, doubleRange); + getActualRange(axis, size, doubleRange); + applyRangePadding(axis, size, chart); + calculateVisibleLabels(axis, chart); +} + +/** + * Calculates and sets the minimum and maximum range values for a given axis. + * + * @param {AxisModel} axis - The axis model for which the range is being calculated. + * @param {Chart} chart - The chart instance containing relevant context and data for the axis. + * @param {DoubleRange} doubleRange - An object used to capture and update the min and max range values. + * @returns {void} This function modifies the `doubleRange` with calculated min and max values; it does not return a value. + * @private + */ +export function calculateRange(axis: AxisModel, chart: Chart, doubleRange: DoubleRange): void { + doubleRange.min = null; + doubleRange.max = null; + if (!setRange(axis)) { + axis.series.forEach((series: SeriesProperties) => { + if (!series.visible) { + return; + } + axis.paddingInterval = 0; + if (!isNullOrUndefined(series.points)) { + axis.maxPointLength = (series.points?.length || 0); + } + + if (((series.type?.includes('Column') || series.type?.includes('Histogram')) && axis.orientation === 'Horizontal') + || (series.type?.includes('Bar') && axis.orientation === 'Vertical')) { + if ((series.xAxis.valueType === 'Double' || series.xAxis.valueType === 'DateTime') && series.xAxis?.rangePadding === 'Auto') { + axis.paddingInterval = getMinPointsDelta(series.xAxis, axis.series) * 0.5; + } + } + + if (axis.orientation === 'Horizontal') { + if (chart.requireInvertedAxis) { + yAxisRange(series, doubleRange); + } else { + findMinMax((series.xMin as number) - axis.paddingInterval, (series.xMax as number) + axis.paddingInterval, doubleRange); + } + } + + if (axis.orientation === 'Vertical') { + isColumn += (series.type?.includes('Column') || series.type?.includes('Bar')) ? 1 : 0; + isStacking = series.type?.includes('Stacking'); + if (chart.requireInvertedAxis) { + findMinMax((series.xMin as number) - axis.paddingInterval, (series.xMax as number) + axis.paddingInterval, doubleRange); + } else { + yAxisRange(series, doubleRange); + } + } + }); + } +} + +/** + * Prepares and configures the actual range for a given axis based on chart settings. + * This function initializes the double range and adjusts it according to specific conditions. + * + * @param {AxisModel} axis - The axis model to update with the actual range. + * @param {ChartSizeProps} size - The dimensions of the chart affecting calculations. + * @param {DoubleRange} doubleRange - The range to be initialized and updated. + * @returns {void} This function modifies the passed double range and does not return a value. + * @private + */ +function getActualRange(axis: AxisModel, size: ChartSizeProps, doubleRange: DoubleRange): void { + initializeDoubleRange(axis, doubleRange); + if ((!axis.startFromZero) && (isColumn > 0)) { + axis.actualRange.interval = axis.interval || calculateNumericNiceInterval(axis, axis.doubleRange.delta, size); + axis.actualRange.maximum = axis.doubleRange.end + axis.actualRange.interval; + axis.actualRange.minimum = (axis.doubleRange.start - axis.actualRange.interval < 0 && axis.doubleRange.start > 0) + ? 0 + : axis.doubleRange.start - (isStacking ? 0 : axis.actualRange.interval); + } else { + axis.actualRange.interval = axis.interval || calculateNumericNiceInterval(axis, axis.doubleRange.delta, size); + axis.actualRange.minimum = axis.doubleRange.start; + axis.actualRange.maximum = axis.doubleRange.end; + } + +} + +/** + * Initializes the given double range with values from the axis model. + * Sets the minimum of the range based on the axis settings, if provided. + * + * @param {AxisModel} axis - The axis model containing potential minimum and maximum values. + * @param {DoubleRange} doubleRange - The double range to be initialized. + * @returns {void} This function updates the double range and does not return a value. + * @private + */ +export function initializeDoubleRange(axis: AxisModel, doubleRange: DoubleRange): void { + if (!isNullOrUndefined(axis.minimum)) { + doubleRange.min = axis.minimum as number; + } else if (doubleRange.min === null || doubleRange.min === Number.POSITIVE_INFINITY) { + doubleRange.min = 0; + } + if (!isNullOrUndefined(axis.maximum)) { + doubleRange.max = axis.maximum as number; + } else if (doubleRange.max === null || doubleRange.max === Number.NEGATIVE_INFINITY) { + doubleRange.max = 5; + } + + if (doubleRange.min === doubleRange.max) { + doubleRange.max = axis.valueType?.includes('Category') ? doubleRange.max : doubleRange.min + 1; + } + axis.doubleRange = createDoubleRange(doubleRange.min, doubleRange.max); +} + +/** + * Creates a double range object with a given start and end value. + * Ensures the start value is the minimum and the end value is the maximum. + * + * @param {number} start - The starting value of the range. + * @param {number} end - The ending value of the range. + * @returns {DoubleRangeType} An object representing the double range from `start` to `end`. + * @private + */ +export function createDoubleRange(start: number, end: number): DoubleRangeType { + const mStart: number = Math.min(start, end); + const mEnd: number = Math.max(start, end); + + return { + start: mStart, + end: mEnd, + delta: mEnd - mStart, + median: mStart + (mEnd - mStart) / 2 + }; +} + +/** + * Adjusts the y-axis range for a given series and updates the specified double range. + * Takes into account series settings and potential drag adjustments. + * + * @param {SeriesProperties} series - The data series for which the y-axis range is being calculated. + * @param {DoubleRange} doubleRange - The double range to be adjusted for the y-axis. + * @returns {void} This function modifies the provided double range and does not return a value. + */ +function yAxisRange(series: SeriesProperties, doubleRange: DoubleRange): void { + // if (series.dragSettings.enable && chart.dragY) { + // if (chart.dragY >= axis.visibleRange.max) { + // series.yMax = chart.dragY + axis.visibleRange.interval; + // } + // if (chart.dragY <= axis.visibleRange.min) { + // series.yMin = chart.dragY - axis.visibleRange.interval; + // } + // } + + // if (series.type === 'Waterfall') { + // let cumulativeMax = 0; + // let cumulativeValue = 0; + // for (let i = 0; i < series.yData.length; i++) { + // if (!(series.intermediateSumIndexes && series.intermediateSumIndexes.includes(i)) && + // !(series.sumIndexes && series.sumIndexes.includes(i))) { + // cumulativeValue += series.yData[i]; + // } + // if (cumulativeValue > cumulativeMax) { + // cumulativeMax = cumulativeValue; + // } + // } + // findMinMax(series.yMin, cumulativeMax); + // } else { + // findMinMax(series.yMin, series.yMax); + // } + findMinMax(series.yMin as number, series.yMax as number, doubleRange); +} + +/** + * Updates the double range with the minimum and maximum values extracted from the specified inputs. + * Ensures the double range's minimum and maximum are adjusted only when new values are more extreme. + * + * @param {number} minValue - The candidate minimum value for the range. + * @param {number} maxValue - The candidate maximum value for the range. + * @param {DoubleRange} doubleRange - The range object to be updated with new min and max values. + * @returns {void} This function modifies the double range directly and does not return a value. + * @private + */ +function findMinMax(minValue: number, maxValue: number, doubleRange: DoubleRange): void { + doubleRange.min = (doubleRange.min === null || doubleRange.min > minValue) ? minValue : doubleRange.min; + doubleRange.max = (doubleRange.max === null || doubleRange.max < maxValue) ? maxValue : doubleRange.max; + if ((doubleRange.max === doubleRange.min) && doubleRange.max < 0 && doubleRange.min < 0) { + doubleRange.max = 0; + } +} + +/** + * Applies padding to the range of the specified axis based on the chart's dimensions. + * Modifies the range to ensure visual appeal and data accuracy within the chart. + * + * @param {AxisModel} axis - The axis model to which the range padding should be applied. + * @param {ChartSizeProps} size - The size dimensions of the chart affecting the range calculation. + * @param {Chart} chart - The chart object that provides context for axis adjustments. + * @returns {void} This function adjusts the axis range and does not return any value. + * @private + */ +function applyRangePadding(axis: AxisModel, size: ChartSizeProps, chart: Chart): void { + const start: number = axis.actualRange.minimum; + const end: number = axis.actualRange.maximum; + if (!setRange(axis)) { + const interval: number = axis.actualRange.interval; + const padding: ChartRangePadding = getRangePadding(axis, chart); + if (padding === 'Additional' || padding === 'Round') { + findAdditional(axis, start, end, interval, size); + } else if (padding === 'Normal') { + findNormal(axis, start, end, interval, size); + } else { + updateActualRange(axis, start, end, interval); + } + } + axis.actualRange.delta = axis.actualRange.maximum - axis.actualRange.minimum; + calculateVisibleRange(axis, size); +} + +/** + * Calculates additional range limits based on start, end, and interval values. + * Sets up the axis with adjusted minimum and maximum limits for visualization. + * + * @param {AxisModel} axis - The axis model to be updated with new range values. + * @param {number} start - The starting point of the current range. + * @param {number} end - The ending point of the current range. + * @param {number} interval - The interval value used to calculate the range limits. + * @param {ChartSizeProps} size - The size dimensions relevant for axis adjustments. + * @returns {void} This function modifies the axis range directly and does not return any value. + * @private + */ +function findAdditional(axis: AxisModel, start: number, end: number, interval: number, size: ChartSizeProps): void { + let minimum: number = Math.floor(start / interval) * interval; + let maximum: number = Math.ceil(end / interval) * interval; + if (axis.rangePadding === 'Additional') { + minimum -= interval; + maximum += interval; + } + if (!isNullOrUndefined(axis.desiredIntervals)) { + const delta: number = maximum - minimum; + interval = calculateNumericNiceInterval(axis, delta, size); + } + updateActualRange(axis, minimum, maximum, interval); +} + +/** + * Updates the actual range of the specified axis using given minimum, maximum, and interval values. + * Ensures the axis range reflects accurate minimum and maximum limits. + * + * @param {AxisModel} axis - The axis model to update with the new range values. + * @param {number} minimum - The calculated or provided minimum value for the range. + * @param {number} maximum - The calculated or provided maximum value for the range. + * @param {number} interval - The interval value used in setting the range limits. + * @returns {void} This function updates the actualRange property of the axis and does not return a value. + * @private + */ +function updateActualRange(axis: AxisModel, minimum: number, maximum: number, interval: number): void { + axis.actualRange = { + minimum: axis.minimum != null ? axis.minimum as number : minimum, + maximum: axis.maximum != null ? axis.maximum as number : maximum, + interval: axis.interval != null ? axis.interval : interval, + delta: axis.actualRange?.delta + }; +} + +/** + * Determines the range padding for a specified axis based on the current chart settings. + * Returns the resolved padding type, considering custom and default settings. + * + * @param {AxisModel} axis - The axis model from which to derive range padding. + * @param {Chart} chart - The chart object providing context for axis padding determination. + * @returns {ChartRangePadding} The type of range padding applied to the axis. + * @private + */ +export function getRangePadding(axis: AxisModel, chart: Chart): ChartRangePadding { + let padding: ChartRangePadding = axis.rangePadding as ChartRangePadding; + if (padding !== 'Auto') { + return padding; + } + switch (axis.orientation) { + case 'Horizontal': + if (chart.requireInvertedAxis) { + padding = 'Normal'; + } else { + padding = 'None'; + } + break; + case 'Vertical': + if (!chart.requireInvertedAxis) { + padding = 'Normal'; + } else { + padding = 'None'; + } + break; + } + return padding; +} + +/** + * Calculates and updates the normal range for an axis using start, end, and interval values. + * Adjusts the axis to ensure the range aligns with the chart's dimension and interval settings. + * + * @param {AxisModel} axis - The axis model to be modified with the normal range. + * @param {number} start - The starting point used for calculating the range. + * @param {number} end - The endpoint used for calculating the range. + * @param {number} interval - The interval value for range calculation. + * @param {ChartSizeProps} size - The size dimensions that may influence range adjustments. + * @returns {void} Updates the axis range, but does not return a value. + * @private + */ +function findNormal(axis: AxisModel, start: number, end: number, interval: number, size: ChartSizeProps): void { + let remaining: number; + let minimum: number; + let maximum: number; + let startValue: number = start; + if (start < 0) { + startValue = 0; + minimum = start + (start * 0.05); + remaining = interval + (minimum % interval); + if ((0.365 * interval) >= remaining) { + minimum -= interval; + } + if (minimum % interval < 0) { + minimum = (minimum - interval) - (minimum % interval); + } + } else { + minimum = start < ((5.0 / 6.0) * end) ? 0 : (start - (end - start) * 0.5); + if (minimum % interval > 0) { + minimum -= (minimum % interval); + } + } + maximum = (end > 0) ? (end + (end - startValue) * 0.05) : (end - (end - startValue) * 0.05); + remaining = interval - (maximum % interval); + if ((0.365 * interval) >= remaining) { + maximum += interval; + } + if (maximum % interval > 0) { + maximum = (maximum + interval) - (maximum % interval); + } + axis.doubleRange = createDoubleRange(minimum, maximum); + if (minimum === 0 || (minimum < 0 && maximum < 0)) { + interval = calculateNumericNiceInterval(axis, axis.doubleRange.delta, size); + maximum = Math.ceil(maximum / interval) * interval; + } + updateActualRange(axis, minimum, maximum, interval); +} + +/** + * Calculates and updates the visible range for the specified axis. + * + * @param {AxisModel} axis - The axis model for which the visible range is being calculated. + * @param {ChartSizeProps} size - The size of the chart area. + * @returns {void} This function updates the `visibleRange` property on the axis and does not return a value. + * @private + */ +export function calculateVisibleRange(axis: AxisModel, size: ChartSizeProps): void { + axis.visibleRange = { + maximum: axis.actualRange.maximum, minimum: axis.actualRange.minimum, + delta: axis.actualRange.delta, interval: axis.actualRange.interval + }; + + // if (chart.chartAreaType === 'Cartesian') { + //const isLazyLoad = isNullOrUndefined(axis.zoomingScrollBar) ? false : axis.zoomingScrollBar.isLazyLoad; + if ((axis.zoomFactor as number) < 1 || (axis.zoomPosition as number) > 0) { + calculateVisibleRangeOnZooming(axis); + axis.visibleRange.interval = calculateNumericNiceInterval(axis, axis.doubleRange.delta, size); + } + //} + + const rangeDifference: number = (axis.visibleRange.maximum - axis.visibleRange.minimum) % axis.visibleRange.interval; + if (rangeDifference !== 0 && !isNaN(rangeDifference) && axis.valueType === 'Double' && + axis.orientation === 'Vertical' && axis.rangePadding === 'Auto') { + let duplicateTempInterval: number = Infinity; + let tempInterval: number = axis.visibleRange.minimum; + for (; (tempInterval <= axis.visibleRange.maximum) && + (duplicateTempInterval !== tempInterval); tempInterval += axis.visibleRange.interval) { + duplicateTempInterval = tempInterval; + } + if (duplicateTempInterval < axis.visibleRange.maximum) { + axis.visibleRange.maximum = duplicateTempInterval + axis.visibleRange.interval; + } + } + + triggerRangeRender(axis.visibleRange.minimum, axis.visibleRange.maximum, axis.visibleRange.interval, axis); +} + +/** + * Triggers the rendering of the axis range based on the minimum, maximum, and interval values. + * + * @param {number} minimum - The minimum value of the axis range to render. + * @param {number} maximum - The maximum value of the axis range to render. + * @param {number} interval - The interval between values in the axis range. + * @param {AxisModel} axis - The axis model associated with the rendering process. + * @returns {void} This function performs side effects related to range rendering and does not return a value. + * @private + */ +export function triggerRangeRender(minimum: number, maximum: number, interval: number, axis: AxisModel): void { + axis.visibleRange = { + minimum: minimum, maximum: maximum, interval: interval, + delta: maximum - minimum + }; +} + +/** + * Calculates and updates the visible labels on the specified axis for the given chart. + * + * @param {AxisModel} axis - The axis model for which visible labels need to be calculated. + * @param {Chart} chart - The chart instance containing the axis for rendering. + * @returns {void} This function updates the `visibleLabels` property on the axis model and does not return a value. + * @private + */ +function calculateVisibleLabels(axis: AxisModel, chart: Chart): void { + axis.visibleLabels = []; + let tempInterval: number = axis.visibleRange.minimum; + if ((axis.zoomFactor as number) < 1 || (axis.zoomPosition as number) > 0 || axis.paddingInterval) { + tempInterval = axis.visibleRange.minimum - (axis.visibleRange.minimum % axis.visibleRange.interval); + } + const format: string = getFormat(axis); + const isCustom: boolean = format.match('{value}') !== null; + let intervalDigits: number = 0; + let formatDigits: number = 0; + if (axis.labelStyle.format && axis.labelStyle.format.indexOf('n') > -1) { + formatDigits = parseInt(axis.labelStyle.format.substring(1, axis.labelStyle.format.length), 10); + } + const option: NumberFormatOptions = { + locale: axis.chart.locale, + useGrouping: false, + format: isCustom ? '' : format + }; + axis.format = getNumberFormat(option); + + axis.startLabel = axis.format(axis.visibleRange.minimum); + axis.endLabel = axis.format(axis.visibleRange.maximum); + + if (axis.visibleRange.interval && (axis.visibleRange.interval + '').indexOf('.') >= 0) { + intervalDigits = (axis.visibleRange.interval + '').split('.')[1].length; + } + const labelStyle: AxisTextStyle = extend({}, axis.labelStyle, undefined, true); + let duplicateTempInterval: number | null = null; + for (; (tempInterval <= axis.visibleRange.maximum) && (duplicateTempInterval !== tempInterval); + tempInterval += axis.visibleRange.interval) { + duplicateTempInterval = tempInterval; + + if (withIn(tempInterval, axis.visibleRange)) { + triggerLabelRender(tempInterval, formatValue(axis, isCustom, format, tempInterval), labelStyle, axis); + } + } + + if (tempInterval && (tempInterval + '').indexOf('.') >= 0 && (tempInterval + '').split('.')[1].length > 10) { + tempInterval = (tempInterval + '').split('.')[1].length > (formatDigits || intervalDigits) ? + +tempInterval.toFixed(formatDigits || intervalDigits) : tempInterval; + if (tempInterval <= axis.visibleRange.maximum) { + triggerLabelRender(tempInterval, formatValue(axis, isCustom, format, tempInterval), labelStyle, axis); + } + } + getMaxLabelWidth(chart, axis); + +} + + +/** + * Applies a custom content callback to modify the axis label text. + * + * @param {number} value - The raw value associated with the axis label. + * @param {string} text - The original text content of the axis label. + * @param {AxisModel} axis - The axis model to which the label belongs. + * @returns {string} The modified label content after applying the callback. If the callback fails, the original text is returned. + * @private + */ +export function applyLabelContentCallback( + value: number, + text: string, + axis: AxisModel +): string | boolean { + const contentCallback: AxisLabelContentFunction = axis.labelStyle?.formatter as AxisLabelContentFunction; + if (contentCallback && typeof contentCallback === 'function') { + try { + const customProps: string | boolean = contentCallback(value, text); + return customProps; + } catch (error) { + return text; + } + } + return text; +} + +/** + * Triggers an event to render axis labels based on the given parameters. + * + * @param {number} tempInterval - The interval at which the labels are to be calculated/rendered. + * @param {string} text - The text content for display in the label. + * @param {AxisTextStyle} labelStyle - The style attributes to be applied to the label text. + * @param {AxisModel} axis - The axis model containing relevant data and label formatting methods. + * @returns {void} This function does not return a value but triggers a label render event. + * @private + */ +export function triggerLabelRender(tempInterval: number, text: string, labelStyle: AxisTextStyle, axis: AxisModel +): void { + const customText: string | boolean = applyLabelContentCallback(tempInterval, text, axis); + if (typeof customText !== 'boolean') { + const isLineBreakLabels: boolean = text.indexOf('
') !== -1; + const formattedText: string | string[] = (axis.labelStyle.enableTrim) + ? (isLineBreakLabels + ? lineBreakLabelTrim(axis.labelStyle.maxLabelWidth as number, + customText as string, labelStyle, axis.chart.themeStyle.axisLabelFont) + : useTextTrim(axis.labelStyle.maxLabelWidth as number, customText as string, + labelStyle as TextStyleModel, axis.chart.enableRtl, axis.chart.themeStyle.axisLabelFont)) + : customText; + axis.visibleLabels.push({ + text: formattedText, + value: tempInterval, + labelStyle: labelStyle, + size: { width: 0, height: 0 }, + breakLabelSize: { width: 0, height: 0 }, + index: 1, + originalText: customText as string + }); + } +} + +/** + * Determines and returns the appropriate format string for axis labels. + * + * @param {AxisModel} axis - The axis model containing label formatting information. + * @returns {string} A string representing the format for the axis labels. + * @private + */ +export function getFormat(axis: AxisModel): string { + if (axis.labelStyle.format) { + return axis.labelStyle.format; + } + return ''; +} + +/** + * Formats the value for axis labels based on the axis model and formatting rules. + * + * @param {AxisModel} axis - The axis model containing data and formatting methods. + * @param {boolean} isCustom - A flag indicating whether a custom format should be used. + * @param {string} format - The string pattern to format the value, containing placeholders like '{value}'. + * @param {number} tempInterval - The numeric interval used to calculate label values. + * @returns {string} A formatted string for the axis label. + * @private + */ +export function formatValue(axis: AxisModel, isCustom: boolean, format: string, tempInterval: number): string { + const labelValue: number = !(tempInterval % 1) ? tempInterval : Number(tempInterval.toLocaleString('en-US').split(',').join('')); + return isCustom ? format.replace('{value}', axis.format(labelValue)) + : format ? axis.format(tempInterval) : axis.format(labelValue); +} diff --git a/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/LogarithmicAxisRenderer.tsx b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/LogarithmicAxisRenderer.tsx new file mode 100644 index 0000000..813a69a --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/AxisTypeRenderer/LogarithmicAxisRenderer.tsx @@ -0,0 +1,199 @@ +import { extend, getNumberFormat, NumberFormatOptions } from '@syncfusion/react-base'; +import { ChartFontProps } from '../../../base/interfaces'; +import { getActualDesiredIntervalsCount, logBase, withIn } from '../../../utils/helper'; +import { calculateRange, initializeDoubleRange, triggerLabelRender, DoubleRange, getFormat, formatValue, triggerRangeRender } from './DoubleAxisRenderer'; +import { calculateVisibleRangeOnZooming, getMaxLabelWidth } from './AxisUtils'; +import { AxisModel, Chart, ChartSizeProps } from '../../../chart-area/chart-interfaces'; + +/** + * Calculates the range and interval for a Logarithmic axis within a chart. + * This involves adjusting the axis properties based on the chart size and configuration. + * + * @param {Size} size - The dimensions of the chart area (width and height in pixels) + * @param {AxisModel} axis - The logarithmic axis model containing configuration settings + * @param {Chart} chart - The parent chart instance that contains this axis + * @returns {void} Updates the axis model with calculated range, interval, and labeling properties + * @private + */ +export function calculateLogarithmicAxis(size: ChartSizeProps, axis: AxisModel, chart: Chart): void { + calculateRangeAndInterval(size, axis, chart); +} + +/** + * Calculates the range and interval for the logarithmic axis. + * Processes raw data into usable axis information by calculating ranges and intervals. + * + * @param {Size} size - The chart area dimensions that influence axis calculations + * @param {AxisModel} axis - The axis model to update with calculated values + * @param {Chart} chart - The chart instance for accessing related configuration + * @returns {void} Modifies the axis object with calculated range and interval values + * @private + * + * @example + * // Internal calculation of range and interval + * calculateRangeAndInterval({ width: 800, height: 400 }, logarithmicAxis, chart); + * + * @throws Will produce incorrect results if axis.logBase is not properly set + */ +function calculateRangeAndInterval(size: ChartSizeProps, axis: AxisModel, chart: Chart): void { + const logarithmicRange: DoubleRange = { + min: null, + max: null + }; + calculateRange(axis, chart, logarithmicRange as Required); + + getActualRange(axis, size, logarithmicRange as Required); + + calculateVisibleRange(axis, size); + + calculateVisibleLabels(axis, chart); +} + +/** + * Calculates the actual logarithmic range for the axis based on data values. + * Transforms regular values to logarithmic scale and ensures proper range boundaries. + * + * @param {AxisModel} axis - The axis model with logBase and range configurations + * @param {Size} size - The chart dimensions used for range calculations + * @param {DoubleRange} logarithmicRange - Object to store calculated min/max values + * @returns {void} Updates the logarithmicRange object and sets axis.actualRange properties + * @private + * + * @example + * // Calculate actual range for logarithmic axis with base 10 + * const logRange = { min: null, max: null }; + * getActualRange(logAxis, chartSize, logRange); + * // After execution, logRange might contain { min: 0, max: 3 } for values 1-1000 + * + * @throws Will return incorrect results if logarithmicRange contains negative values + */ +function getActualRange(axis: AxisModel, size: ChartSizeProps, logarithmicRange: DoubleRange): DoubleRange { + const LOG_UNIT: number = 1; + initializeDoubleRange(axis, logarithmicRange); + logarithmicRange.min = (logarithmicRange.min as Required) < 0 ? 0 : logarithmicRange.min; + let logStart: number = logBase(logarithmicRange.min as Required, axis.logBase as Required); + logStart = isFinite(logStart) ? logStart : logarithmicRange.min as Required; + let logEnd: number = logarithmicRange.max === 1 ? 1 : logBase(logarithmicRange.max as number, axis.logBase as number); + logEnd = isFinite(logEnd) ? logEnd : logarithmicRange.max as Required; + logarithmicRange.min = Math.floor(logStart / LOG_UNIT); + logarithmicRange.max = Math.ceil(logEnd / LOG_UNIT); + logarithmicRange.max = logarithmicRange.max === logarithmicRange.min ? logarithmicRange.max + 1 : logarithmicRange.max; + axis.actualRange.interval = axis.interval || calculateLogNiceInterval(logarithmicRange.max - logarithmicRange.min, size, axis); + axis.actualRange.minimum = logarithmicRange.min; + axis.actualRange.maximum = logarithmicRange.max; + axis.actualRange.delta = logarithmicRange.max - logarithmicRange.min; + return logarithmicRange; +} + +/** + * Calculates the visible range for the axis, applying zooming factors if needed. + * Handles user zoom interactions by adjusting the visible portion of the axis. + * + * @param {AxisModel} axis - The axis model with zoom settings and actual range + * @param {Size} size - The chart dimensions used for interval calculations + * @returns {void} Updates axis.visibleRange with values representing the currently visible portion + * @private + * + * @example + * // Calculate visible range with 50% zoom factor at 25% position + * axis.zoomFactor = 0.5; + * axis.zoomPosition = 0.25; + * calculateVisibleRange(axis, { width: 800, height: 400 }); + * + * @throws May produce unexpected results if zoom factor or position are outside valid ranges (0-1) + */ +function calculateVisibleRange(axis: AxisModel, size: ChartSizeProps): void { + axis.visibleRange = { + interval: axis.actualRange.interval, maximum: axis.actualRange.maximum, + minimum: axis.actualRange.minimum, delta: axis.actualRange.delta + }; + //const isLazyLoad: boolean = isNullOrUndefined(axis.zoomingScrollBar) ? false : AxisModel.zoomingScrollBar.isLazyLoad; + if ((axis.zoomFactor as number) < 1 || (axis.zoomPosition as number) > 0) { + calculateVisibleRangeOnZooming(axis); + axis.visibleRange.interval = calculateLogNiceInterval(axis.doubleRange.delta, size, axis); + axis.visibleRange.interval = Math.floor(axis.visibleRange.interval) === 0 ? 1 : Math.floor(axis.visibleRange.interval); + triggerRangeRender(axis.visibleRange.minimum, axis.visibleRange.maximum, axis.visibleRange.interval, axis); + } +} + +/** + * Calculates a nicely rounded interval for logarithmic axis labels. + * Uses axis interval divisions to find an appropriate interval that creates readable labels. + * + * @param {number} delta - The difference between axis maximum and minimum values + * @param {Size} size - The chart dimensions used to determine label density + * @param {AxisModel} axis - The axis model containing intervalDivs and other settings + * @returns {number} A calculated interval value that produces visually appealing labels + * @private + * + * @example + * // Calculate nice interval for log axis with delta of 3 (e.g., 10^1 to 10^4) + * const interval = calculateLogNiceInterval(3, { width: 800, height: 400 }, logAxis); + * // Might return 1 for labels at 10, 100, 1000 + * + * @throws Returns potentially incorrect values if axis.intervalDivs is empty or contains invalid values + */ +function calculateLogNiceInterval(delta: number, size: ChartSizeProps, axis: AxisModel): number { + const actualDesiredIntervalsCount: number = getActualDesiredIntervalsCount(size, axis); + let niceInterval: number = delta; + const minInterval: number = Math.pow(axis.logBase as Required, Math.floor(logBase(niceInterval, 10))); + for (let j: number = 0, len: number = axis.intervalDivs.length; j < len; j++) { + const currentInterval: number = minInterval * axis.intervalDivs[j as number]; + if (actualDesiredIntervalsCount < (delta / currentInterval)) { + break; + } + niceInterval = currentInterval; + } + return niceInterval; +} + +/** + * Generates visible labels for the logarithmic axis based on calculated range and interval. + * Creates formatted labels at appropriate positions along the axis. + * + * @param {AxisModel} axis - The axis model with visibleRange and formatting settings + * @param {Chart} chart - The chart instance providing locale and grouping settings + * @returns {void} Populates axis.visibleLabels with formatted label objects at calculated positions + * @private + * + * @example + * // Generate labels for a logarithmic axis with base 10 + * // After calculation, axis.visibleLabels might contain entries for 1, 10, 100, 1000 + * calculateVisibleLabels(logAxis, chart); + * + * @throws May produce incorrect labels if axis.logBase is not properly set + */ +function calculateVisibleLabels(axis: AxisModel, chart: Chart): void { + /** Generate axis labels */ + let tempInterval: number = axis.visibleRange.minimum; + axis.visibleLabels = []; + let labelStyle: ChartFontProps; + let value: number; + if ((axis.zoomFactor as number) < 1 || (axis.zoomPosition as number) > 0) { + tempInterval = axis.visibleRange.minimum - (axis.visibleRange.minimum % axis.visibleRange.interval); + } + const axisFormat: string = getFormat(axis); + const isCustomFormat: boolean = axisFormat.match('{value}') !== null; + const startValue: number = Math.pow(axis.logBase as Required, axis.visibleRange.minimum); + + const option: NumberFormatOptions = { + locale: axis.chart.locale, + useGrouping: false, + format: isCustomFormat ? '' : axisFormat, + maximumFractionDigits: startValue < 1 ? 20 : 3 + }; + axis.format = getNumberFormat(option); + axis.startLabel = axis.format(startValue < 1 ? +startValue.toPrecision(1) : startValue); + axis.endLabel = axis.format(Math.pow(axis.logBase as Required, axis.visibleRange.maximum)); + for (; tempInterval <= axis.visibleRange.maximum; tempInterval += axis.visibleRange.interval) { + labelStyle = (extend({}, axis.labelStyle, undefined, true)); + if (withIn(tempInterval, axis.visibleRange)) { + value = Math.pow(axis.logBase as Required, tempInterval); + triggerLabelRender( + tempInterval, formatValue(axis, isCustomFormat, axisFormat, value < 1 ? +value.toPrecision(1) : value), + labelStyle, axis + ); + } + } + getMaxLabelWidth(chart, axis); +} diff --git a/components/charts/src/chart/renderer/AxesRenderer/CartesianLayoutRender.tsx b/components/charts/src/chart/renderer/AxesRenderer/CartesianLayoutRender.tsx new file mode 100644 index 0000000..cf97608 --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/CartesianLayoutRender.tsx @@ -0,0 +1,1891 @@ +import { extend } from '@syncfusion/react-base'; +import { ChartBorderProps, ChartLocationProps } from '../../base/interfaces'; +import { degreeToRadian, getRotatedTextSize, inside, isBreakLabel, isRotatedRectIntersect, measureText, stringToNumber, useTextTrim, withInBounds } from '../../utils/helper'; +import { findLabelSize, refreshAxis } from './AxisTypeRenderer/AxisUtils'; +import { calculateDoubleAxis } from './AxisTypeRenderer/DoubleAxisRenderer'; +import { JSX } from 'react'; +import { Orientation } from '../../base/enum'; +import { calculateCategoryAxis } from './AxisTypeRenderer/CategoryAxisRenderer'; +import { calculateDateTimeAxis } from './AxisTypeRenderer/DateTimeAxisRenderer'; +import { calculateLogarithmicAxis } from './AxisTypeRenderer/LogarithmicAxisRenderer'; +import { calculateRowSize } from './ChartRowsRender'; +import { calculateColumnSize } from './ChartColumnsRender'; +import { AxisModel, Chart, ChartAxisLayout, ColumnProps, PathOptions, Rect, RowProps, ChartSizeProps, TextOption, TextStyleModel, Thickness, VisibleLabel } from '../../chart-area/chart-interfaces'; + +/** + * Measures and computes the layout of axes within the given chart rectangle. + * + * @param {Rect} rect - The bounding rectangle (`Rect`) available for the chart, typically the full chart area excluding padding. + * @param {Chart} chart - The chart instance (`Chart`) for which the axis layout needs to be measured. + * @returns {void} + * @private + */ +export function measureAxis(rect: Rect, chart: Chart): void { + + const chartAxisLayout: ChartAxisLayout = { + initialClipRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + leftSize: 0, + rightSize: 0, + topSize: 0, + bottomSize: 0, + seriesClipRect: { x: 0, y: 0, width: 0, height: 0 } + }; + const chartAreaWidth: number = chart.chartArea.width ? stringToNumber(chart.chartArea.width, chart.availableSize.width) : 0; + chartAxisLayout.seriesClipRect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; + chart.chartAxislayout = chartAxisLayout; + measureRowAxis(chart, chartAxisLayout.initialClipRect); + chartAxisLayout.initialClipRect = subtractThickness( + chartAxisLayout.initialClipRect, { left: chartAxisLayout.leftSize, right: chartAxisLayout.rightSize, top: 0, bottom: 0 }); + measureColumnAxis(chart, chartAxisLayout.initialClipRect); + chartAxisLayout.initialClipRect = subtractThickness( + chartAxisLayout.initialClipRect, { left: 0, right: 0, top: chartAxisLayout.topSize, bottom: chartAxisLayout.bottomSize }); + calculateAxisSize(chartAxisLayout.initialClipRect, chart); + chartAxisLayout.leftSize = 0; + chartAxisLayout.rightSize = 0; + chartAxisLayout.topSize = 0; + chartAxisLayout.bottomSize = 0; + measureRowAxis(chart, chartAxisLayout.initialClipRect); + chartAxisLayout.seriesClipRect = subtractThickness( + chartAxisLayout.seriesClipRect, { left: chartAxisLayout.leftSize, right: chartAxisLayout.rightSize, top: 0, bottom: 0 }); + measureColumnAxis(chart, chartAxisLayout.seriesClipRect); + chartAxisLayout.seriesClipRect = subtractThickness( + chartAxisLayout.seriesClipRect, { left: 0, right: 0, top: chartAxisLayout.topSize, bottom: chartAxisLayout.bottomSize }); + if (chartAreaWidth) { + calculateFixedChartArea(chart, chartAreaWidth); + } + refreshAxis(chart); + calculateAxisSize(chartAxisLayout.seriesClipRect, chart); +} + +/** + * Measures the row axis on the given chart and adjusts its size according to the specified rectangular area. + * + * @param {Chart} chart - The chart for which the row axis is being measured. + * @param {Rect} rect - The rectangular area used as a reference for measuring the row axis. + * @returns {void} This function does not return any value. + * @private + */ +export function measureRowAxis(chart: Chart, rect: Rect): void { + calculateRowSize(chart, rect); + const chartAxisLayout: ChartAxisLayout = chart.chartAxislayout; + for (const row of chart.rows) { + row.nearSizes = []; + row.farSizes = []; + row.insideNearSizes = []; + row.insideFarSizes = []; + arrangeAxis(row); + measureDefinition(row, chart, { width: chart.availableSize.width, height: row.computedHeight }); + if (chartAxisLayout.leftSize < calculateAxisSum(row.nearSizes)) { + chartAxisLayout.leftSize = calculateAxisSum(row.nearSizes); + } + if (chartAxisLayout.rightSize < calculateAxisSum(row.farSizes)) { + chartAxisLayout.rightSize = calculateAxisSum(row.farSizes); + } + } +} + +/** + * Measures the column axis on the given chart and adjusts its size based on the specified rectangular area. + * + * @param {Chart} chart - The chart whose column axis is being measured. + * @param {Rect} rect - The rectangular area used as a reference to measure the column axis. + * @returns {void} This function does not return any value. + * @private + */ +function measureColumnAxis(chart: Chart, rect: Rect): void { + calculateColumnSize(rect, chart); + const chartAxisLayout: ChartAxisLayout = chart.chartAxislayout; + for (const column of chart.columns) { + column.farSizes = []; + column.nearSizes = []; + column.insideNearSizes = []; + column.insideFarSizes = []; + arrangeAxis(column); + measureDefinition(column, chart, { width: column.computedWidth, height: chart.availableSize.height }); + if (chartAxisLayout.bottomSize < calculateAxisSum(column.nearSizes)) { + chartAxisLayout.bottomSize = calculateAxisSum(column.nearSizes); + } + if (chartAxisLayout.topSize < calculateAxisSum(column.farSizes)) { + chartAxisLayout.topSize = calculateAxisSum(column.farSizes); + } + } +} + +/** + * Computes the sum of an array of numbers. + * + * @param {number[]} values - An array of numbers to be summed. + * @returns {number} The total sum of the numbers in the array. + * @private + */ +export function calculateAxisSum(values: number[]): number { + return values.reduce((acc: number, value: number) => acc + value, 0); +} + +/** + * Arranges the axes for the given definition, preparing an axis collection. + * + * @param {RowProps | ColumnProps} definition - The definition containing the axis properties to be arranged. + * @returns {void} This function does not return a value. + * @private + */ +function arrangeAxis(definition: RowProps | ColumnProps): void { + const axisCollection: AxisModel[] = []; + + for (let i: number = 0, len: number = definition.axes.length; i <= len; i++) { + if (definition.axes[i as number]) { + axisCollection.push(definition.axes[i as number]); + } + } + + definition.axes = axisCollection; +} + +/** + * Measures the given definition to adjust axis sizes based on the chart and size provided. + * + * @param {RowProps | ColumnProps} definition - The axis-related definition to be measured. + * @param {Chart} chart - The chart to which the axes belong. + * @param {ChartSizeProps} size - The size object used for measurement reference. + * @returns {void} This function does not return a value. + * @private + */ +function measureDefinition(definition: RowProps | ColumnProps, chart: Chart, size: ChartSizeProps): void { + const axisPadding: number = 10; + for (const axis of definition.axes) { + axis.chart = chart; + switch (axis.valueType) { + case 'Double': + calculateDoubleAxis(size, axis, chart); + break; + case 'DateTime': + calculateDateTimeAxis(size, axis, chart); + break; + case 'Category': + calculateCategoryAxis(size, axis, chart); + break; + case 'Logarithmic': + calculateLogarithmicAxis(size, axis, chart); + break; + } + computeSize(axis, definition, chart); + } + + if (definition.farSizes.length > 0) { + definition.farSizes[definition.farSizes.length - 1] -= axisPadding; + } + if (definition.nearSizes.length > 0) { + definition.nearSizes[definition.nearSizes.length - 1] -= axisPadding; + } +} + +/** + * Computes and assigns the size for the given axis based on its definition. + * + * @param {AxisModel} axis - The axis model object containing details about the axis. + * @param {RowProps | ColumnProps} definition - The definition of the row or column model affecting layout. + * @param {Chart} chart - The chart instance to which this axis belongs and is being rendered for. + * @returns {void} + * @private + */ +export function computeSize(axis: AxisModel, definition: RowProps | ColumnProps, chart: Chart): void { + let width: number = 0; + const innerPadding: number = 5; + + if (axis.visible && axis.internalVisibility) { + width = findTickSize(axis) + findLabelSize(axis, innerPadding, definition, chart) + + ((axis.lineStyle?.width as number) * 0.5); + } + if (axis.isAxisOpposedPosition) { + definition.farSizes.push(width); + } else { + definition.nearSizes.push(width); + } +} + +/** + * Modifies the given rectangle by reducing its size based on the provided thickness. + * + * @param {Rect} rect - The rectangle that will be adjusted in place by subtracting thickness. + * @param {Thickness} thickness - The thickness values to subtract from each side of the rectangle. + * @returns {Rect} - The modified rectangle with updated dimensions. + * @private + */ +export function subtractThickness(rect: Rect, thickness: Thickness): Rect { + rect.x += thickness.left; + rect.y += thickness.top; + rect.width -= thickness.left + thickness.right; + rect.height -= thickness.top + thickness.bottom; + return rect; +} + +/** + * Computes the offset value for an axis based on given position values and plot offset. + * + * @param {number} position1 - The primary position value. + * @param {number} position2 - The secondary position value. + * @param {number} plotOffset - The default plot offset value. + * @returns {number} The calculated offset for the axis. + * @private + */ +function getAxisOffsetValue(position1: number, position2: number, plotOffset: number): number { + return position1 ? (position1 + (position2 ? position2 : plotOffset)) : (position2 ? position2 + plotOffset : 2 * plotOffset); +} + +/** + * Returns a new array containing the elements from the start of the input array up to the specified index. + * + * @param {number[]} values - The input array of numbers. + * @param {number} index - The index up to which elements are included in the new array. + * @returns {number[]} A new array containing elements from the start up to the specified index. + * @private + */ +function subArray(values: number[], index: number): number[] { + return values.slice(0, index); +} + +/** + * Calculates the size of the axis for a given chart based on the provided rectangular area. + * + * @param {Rect} rect - The rectangular area to calculate the axis size within. + * @param {Chart} chart - The chart for which the axis size is calculated. + * @returns {void} This function does not return a value. + * @private + */ +function calculateAxisSize(rect: Rect, chart: Chart): void { + calculateRowSize(chart, rect); + let x: number; + let y: number; + for (let i: number = 0; i < chart.rows.length; i++) { + const row: RowProps = chart.rows[i as number]; + let nearCount: number = 0; + let farCount: number = 0; + + for (let j: number = 0; j < row.axes.length; j++) { + const axis: AxisModel = row.axes[j as number]; + const axisOffset: number = 0; + + if (axis.rect.height === 0) { + axis.rect.height = row.computedHeight; + let size: number = 0; + for (let k: number = i + 1, len: number = i + (axis.span as number); k < len; k++) { + size += chart.rows[k as number].computedHeight; + } + axis.rect.y = (row.computedTop - size) + (axis.plotOffset?.top || axisOffset); + axis.rect.height += size - getAxisOffsetValue( + axis.plotOffset?.top as number, axis.plotOffset?.bottom as number, 0); + axis.rect.width = 0; + } + const ticksHeight: number = axis.majorTickLines.height as number; + const axisLabelPadding: number = axis.labelStyle.padding as number; + if (axis.isAxisOpposedPosition) { + if (axis.labelStyle.position === 'Inside' && axis.orientation === 'Vertical') { + if (farCount > 0) { + x = rect.x + rect.width + calculateAxisSum(subArray(row.farSizes, farCount)) + + axis.maxLabelSize.width + (axis.tickPosition === 'Inside' ? ticksHeight : 0) + axisLabelPadding; + } + else { + x = rect.x + rect.width - calculateAxisSum(subArray(row.insideFarSizes, farCount)); + } + } + else { + x = rect.x + rect.width + calculateAxisSum(subArray(row.farSizes, farCount)); + + } + axis.rect.x = axis.rect.x >= x ? axis.rect.x : x; + farCount++; + } else { + if (axis.labelStyle.position === 'Inside' && axis.orientation === 'Vertical') { + if (nearCount > 0) { + x = rect.x - calculateAxisSum(subArray(row.nearSizes, nearCount)) - axis.maxLabelSize.width - + (axis.tickPosition === 'Inside' ? ticksHeight : 0) - axisLabelPadding; + } + else { + x = rect.x + calculateAxisSum(subArray(row.insideNearSizes, nearCount)); + } + } + else { + x = rect.x - calculateAxisSum(subArray(row.nearSizes, nearCount)); + } + axis.rect.x = axis.rect.x <= x ? axis.rect.x : x; + nearCount++; + } + } + } + + calculateColumnSize(rect, chart); + + for (let i: number = 0; i < chart.columns.length; i++) { + const column: ColumnProps = chart.columns[i as number]; + let nearCount: number = 0; + let farCount: number = 0; + + for (let j: number = 0; j < column.axes.length; j++) { + const axis: AxisModel = column.axes[j as number]; + const axisOffset: number = 0; + + if (axis.rect.width === 0) { + for (let k: number = i, len: number = (i + (axis.span as number)); k < len; k++) { + axis.rect.width += chart.columns[k as number].computedWidth; + } + axis.rect.x = column.computedLeft + (axis.plotOffset?.left || axisOffset); + axis.rect.width -= getAxisOffsetValue( + axis.plotOffset?.left as number, axis.plotOffset?.right as number, 0); + axis.rect.height = 0; + } + const ticksHeight: number = axis.majorTickLines.height as number; + const axisLabelPadding: number = axis.labelStyle.padding as number; + if (axis.isAxisOpposedPosition) { + if (axis.labelStyle.position === 'Inside' && axis.orientation === 'Horizontal') { + if (farCount > 0) { + y = rect.y - calculateAxisSum(subArray(column.farSizes, farCount)) - axis.maxLabelSize.height + - (axis.tickPosition === 'Inside' ? ticksHeight : 0) - axisLabelPadding; + } + else { + y = rect.y + calculateAxisSum(subArray(column.insideFarSizes, farCount)); + } + } + else { + y = rect.y - calculateAxisSum(subArray(column.farSizes, farCount)); + } + axis.rect.y = axis.rect.y <= y ? axis.rect.y : y; + farCount++; + } else { + if (axis.labelStyle.position === 'Inside' && axis.orientation === 'Horizontal') { + if (nearCount > 0) { + y = rect.y + rect.height + calculateAxisSum(subArray(column.nearSizes, nearCount)) + axis.maxLabelSize.height + + (axis.tickPosition === 'Inside' ? ticksHeight : 0) + axisLabelPadding; + } + else { + y = rect.y + rect.height - calculateAxisSum(subArray(column.insideNearSizes, nearCount)); + } + } + else { + y = rect.y + rect.height + calculateAxisSum(subArray(column.nearSizes, nearCount)); + } + axis.rect.y = axis.rect.y >= y ? axis.rect.y : y; + nearCount++; + } + } + } +} + +/** + * Renders the axes for the given chart control. + * + * @param {Chart} control - The chart control containing the axis collection to be rendered. + * @returns {void} This function does not return a value. + * @private + */ +export function renderAxis(control: Chart): void { + for (let i: number = 0; i < control.axisCollection.length; i++) { + const axis: AxisModel = control.axisCollection[i as number]; + drawAxis(axis, i, control); + } + drawPaneLines(control); +} + +/** + * Draws the pane lines for the specified chart. + * + * @param {Chart} chart - The chart instance for which the pane lines need to be drawn. + * @returns {void} - This function does not return a value; it updates the chart's visualization directly. + * @private + */ +function drawPaneLines(chart: Chart): void { + for (let j: number = 0, len: number = chart.rows.length; j < len; j++) { + const row: RowProps = chart.rows[j as number]; + if (row.border.color) { + drawBottomLine(chart, row, j, true); + } + } + for (let j: number = 0, len: number = chart.columns.length; j < len; j++) { + const column: ColumnProps = chart.columns[j as number]; + if (column.border.color) { + drawBottomLine(chart, column, j, false); + } + } +} + +/** + * Draws the bottom line for a chart's row or column based on the given definition. + * + * @param {Chart} chart - The chart instance for which the bottom line is drawn. + * @param {RowProps | ColumnProps} definition - The model definition used to specify dimensions and properties for drawing. + * @param {number} index - The index of the row or column for which the line is drawn. + * @param {boolean} isRow - Flag indicating whether the line is drawn for a row (true) or a column (false). + * @returns {void} - This function does not return a value; it modifies the chart directly. + * @private + */ +export function drawBottomLine(chart: Chart, definition: RowProps | ColumnProps, index: number, isRow: boolean): void { + let x1: number; let x2: number; + let y1: number; let y2: number; + let definitionName: string; + if (isRow) { + definition = definition as RowProps; + y1 = y2 = definition.computedTop + definition.computedHeight; + x1 = chart.chartAxislayout.seriesClipRect.x; + x2 = x1 + chart.chartAxislayout.seriesClipRect.width; + definitionName = 'Row'; + } else { + x1 = x2 = (definition as ColumnProps).computedLeft; + y1 = chart.chartAxislayout.seriesClipRect.y; + y2 = y1 + chart.chartAxislayout.seriesClipRect.height; + definitionName = 'Column'; + } + const optionsLine: PathOptions = { + id: chart.element.id + '_AxisBottom_' + definitionName + index, + x1: x1, + y1: y1, + x2: x2, + y2: y2, + strokeWidth: definition.border.width as number, + stroke: definition.border.color + }; + chart.paneLineOptions.push(optionsLine); +} + +/** + * Draws the specified axis on the chart by updating its cross value and checking its visibility status. + * + * @param {AxisModel} axis - The axis model representing the axis to draw, which includes properties like visibility. + * @param {number} index - The index of the axis in the chart's collection, used for order and identification. + * @param {Chart} control - The chart instance that manages rendering and holds configuration for axes and other components. + * @returns {void} This function does not return a value. + * @private + */ +function drawAxis(axis: AxisModel, index: number, control: Chart): void { + axis.updatedRect = axis.rect; + const isVisible: boolean = (axis.visible as boolean && axis.internalVisibility); + const lineWidth: number = axis.lineStyle?.width as number; + if (axis.orientation === 'Horizontal') { + if (isVisible && lineWidth > 0) { + calculateAxisLineOptions( + control, axis, index, 0, 0, 0, 0, axis.plotOffset?.left as number, + axis.plotOffset?.right as number, axis.updatedRect + ); + } + calculateXAxisTitleOptions(axis, index, axis.updatedRect, control); + } else { + if (isVisible && lineWidth > 0) { + calculateAxisLineOptions(control, axis, index, 0, 0, axis.plotOffset?.bottom as number, + axis.plotOffset?.top as number, 0, 0, axis.updatedRect); + } + calculateYAxisTitleOptions(axis, index, axis.updatedRect, control); + } +} + +/** + * Draws the axis line on the chart based on the provided axis model and plotting area dimensions. + * + * @param {Chart} chart - The chart instance where the axis line will be drawn. + * @param {AxisModel} axis - The axis model that defines properties such as color, width, and style of the axis line. + * @param {number} index - The index of the axis in the chart's axis collection. + * @param {number} plotX - The X-coordinate for the plot area reference point. + * @param {number} plotY - The Y-coordinate for the plot area reference point. + * @param {number} plotBottom - The bottom boundary of the plot area. + * @param {number} plotTop - The top boundary of the plot area. + * @param {number} plotLeft - The left boundary of the plot area. + * @param {number} plotRight - The right boundary of the plot area. + * @param {Rect} rect - The rectangle defining the exact space allocated for the axis. + * @returns {void} This function does not return any value. + * @private + */ +function calculateAxisLineOptions( + chart: Chart, axis: AxisModel, index: number, plotX: number, plotY: number, plotBottom: number, plotTop: number, plotLeft: number, + plotRight: number, rect: Rect): void { + + const optionsLine: PathOptions = { + 'id': chart.element.id + 'AxisLine_' + index, + 'd': 'M ' + (rect.x - plotX - plotLeft) + ' ' + (rect.y - plotY - plotTop) + + ' L ' + (rect.x + rect.width + plotX + plotRight) + ' ' + (rect.y + rect.height + plotY + plotBottom), + strokeDasharray: axis.lineStyle?.dashArray || '', + strokeWidth: axis.lineStyle?.width as number, + 'stroke': axis.lineStyle?.color || chart.themeStyle.axisLine, + dashArray: axis.lineStyle?.dashArray || '' + }; + axis.axisLineOptions = optionsLine; +} + +/** + * Renders the labels for the X-axis of the chart. + * + * @param {AxisModel} axis - The axis model containing label configuration and styling for the X-axis. + * @param {number} index - The index of the axis in the axis collection. + * @param {Rect} rect - The rectangular area where the axis is being rendered. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @param {Function} xScale - A scaling function used to map axis values to pixel positions. + * @returns {JSX.Element[]} An array of JSX elements representing the X-axis labels. + * @private + */ +export function drawXAxisLabels(axis: AxisModel, index: number, rect: Rect, chart: Chart, xScale: Function): JSX.Element { + const labelElements: JSX.Element[] = []; + let pointX: number = 0; + let pointY: number = 0; + let previousLabel: number = 0; + const labelSpace: number = axis.labelStyle.padding as number; + let labelHeight: number; + let elementSize: ChartSizeProps; let labelPadding: number; let anchor: string; const pixel: number = 10; + const islabelInside: boolean = axis.labelStyle.position === 'Inside'; + const isOpposed: boolean = axis.isAxisOpposedPosition; + const tickSpace: number = axis.labelStyle.position === axis.tickPosition ? (axis.majorTickLines.height as number) : 0; + let padding: number = tickSpace + labelSpace + (axis.lineStyle?.width as number) * 0.5; + const angle: number = axis.angle % 360; + const isHorizontalAngle: boolean = (angle === 0 || angle === -180 || angle === 180); + let labelWidth: number; + const isInverse: boolean = axis.isAxisInverse; + let isLeft: boolean = false; + let previousEnd: number = isInverse ? (rect.x + rect.width) : rect.x; + let width: number = 0; const length: number = axis.visibleLabels.length; + let intervalLength: number; let label: VisibleLabel; let isAxisBreakLabel: boolean; + const scrollBarHeight: number = 0; + + const textPoints: ChartLocationProps[][] = []; + let rotatedLabelSize: ChartSizeProps = { height: 0, width: 0 }; + padding += (angle === 90 || angle === 270 || angle === -90 || angle === -270) ? (islabelInside ? 5 : -5) : 0; + const isLabelUnderAxisLine: boolean = ((!isOpposed && !islabelInside) || (isOpposed && islabelInside)); + const newPoints: ChartLocationProps[][] = []; + const legendWidth: number = 0; + const isEndAnchor: boolean = isLabelUnderAxisLine ? + ((360 >= angle && angle >= 180) || (-1 >= angle && angle >= -180)) : + ((1 <= angle && angle <= 180) || (-181 >= angle && angle >= -360)); + for (let i: number = 0, len: number = length; i < len; i++) { + extend({}, axis.rect, undefined, true) as Rect; + label = extend({}, axis.visibleLabels[i as number], undefined, true) as VisibleLabel; + isAxisBreakLabel = isBreakLabel(label.originalText) || (axis.labelStyle.intersectAction === 'Wrap' && label.text.length > 1); + pointX = xScale(label.value, rect, axis); + elementSize = label.size; + if (axis.labelStyle.enableWrap) { + elementSize.height = measureText(label.text as string, axis.labelStyle as TextStyleModel, + chart.themeStyle.axisLabelFont).height; + } + intervalLength = rect.width / length; + labelWidth = isAxisBreakLabel ? label.breakLabelSize.width : elementSize.width; + width = ((axis.labelStyle.intersectAction === 'Trim' || axis.labelStyle.intersectAction === 'Wrap') && angle === 0 && + labelWidth > intervalLength) ? intervalLength : labelWidth; + labelHeight = elementSize.height / 4; + pointX -= (isAxisBreakLabel || angle !== 0) ? 0 : (width / 2); + + // label X value adjustment for label rotation (Start) + if (angle !== 0) { + if (isAxisBreakLabel) { + pointX -= 0; + } else { + pointX -= (angle === -90 || angle === 270 ? -labelHeight : (angle === 90 || angle === -270) ? labelHeight : 0); + } + } + // label X value adjustment for label rotation (End) + + if (axis.labelStyle?.align === 'Right') { + pointX = pointX + width - pixel; + } else if (axis.labelStyle?.align === 'Left') { + pointX = pointX - width + pixel; + } + + const paddingForBreakLabel: number = isAxisBreakLabel ? + (isHorizontalAngle ? (axis.opposedPosition || islabelInside ? 0 : elementSize.height) : + (label.breakLabelSize.width / 2)) : 0; + padding = isAxisBreakLabel ? (tickSpace + labelSpace + (axis.lineStyle?.width as number) * 0.5) : padding; + + // label Y value adjustment (Start) + if (islabelInside && angle) { + if (isAxisBreakLabel) { + pointY = isOpposed ? (rect.y + padding + (paddingForBreakLabel)) : (rect.y - padding - (paddingForBreakLabel)); + } else { + pointY = isOpposed ? (rect.y + padding + labelHeight) : (rect.y - padding - labelHeight); + } + } else { + if (isAxisBreakLabel) { + labelPadding = !isLabelUnderAxisLine ? -(padding + scrollBarHeight + (paddingForBreakLabel)) : + padding + scrollBarHeight + (angle ? paddingForBreakLabel : (3 * labelHeight)); + } else { + labelPadding = !isLabelUnderAxisLine ? + -(padding + scrollBarHeight + (angle ? labelHeight : (label.index > 1 ? (2 * labelHeight) : 0))) : + padding + scrollBarHeight + ((angle ? 1 : 3) * labelHeight); + } + pointY = (rect.y + (labelPadding * label.index)); + } + // label Y value adjustment (End) + + if (isAxisBreakLabel) { + anchor = 'middle'; + } else { + anchor = (chart.enableRtl) ? ((isEndAnchor) ? '' : 'end') : (chart.enableRtl || isEndAnchor) ? 'end' : ''; + } + const options: TextOption = { + id: `${chart.element.id}_${index}_AxisLabel_${i}`, + x: pointX, + y: pointY, + anchor: anchor, + text: getLabelText(label, axis, intervalLength, chart), + transform: 'rotate(' + angle + ',' + pointX + ',' + pointY + ')', + labelRotation: angle, + fill: label.labelStyle?.color || chart.themeStyle.axisLabelFont.color, + fontSize: label.labelStyle?.fontSize || chart.themeStyle.axisLabelFont.fontSize, + fontFamily: label.labelStyle?.fontFamily || chart.themeStyle.axisLabelFont.fontFamily, + fontWeight: label.labelStyle?.fontWeight || chart.themeStyle.axisLabelFont.fontWeight, + fontStyle: label.labelStyle?.fontStyle || chart.themeStyle.axisLabelFont.fontStyle, + opacity: axis.labelStyle?.opacity as number, + baseLine: '', + XPositionWidth: width, + isAxisBreakLabel: isAxisBreakLabel + }; + + if (angle !== 0) { + rotatedLabelSize = getRotatedTextSize(label.originalText, label.labelStyle as TextStyleModel, + angle, chart.themeStyle.axisLabelFont); + isLeft = ((angle < 0 && angle > -90) || (angle < -180 && angle > -270) || + (angle > 90 && angle < 180) || (angle > 270 && angle < 360)); + } + switch (axis.labelStyle.edgeLabelPlacement) { + case 'None': + break; + case 'Hide': + if (((i === 0 || (isInverse && i === len - 1)) && + (anchor === '' ? options.x + labelWidth / 2 < rect.x : options.x < rect.x)) || + ((i === len - 1 || (isInverse && i === 0)) && + (options.x + (angle === 0 ? width : rotatedLabelSize.width) > rect.x + rect.width))) { + continue; + } + break; + case 'Shift': + if (i === len - 2 && axis.labelStyle.intersectAction !== 'MultipleRows') { + if (anchor === 'start' || anchor === '') { + previousLabel = options.x + width; // For start anchor + } else if (anchor === 'middle') { + previousLabel = options.x + (width / 2); // For middle anchor + } else { + previousLabel = options.x; // For end anchor + } + } + if ((i === 0 || (isInverse && i === len - 1)) && (options.x < rect.x || (angle !== 0 && isLeft && options.x < rect.x) || + (options.x - (label.size.width / label.text.length) / 2 < rect.x && angle === 0))) { + intervalLength -= (rect.x - options.x); + if (anchor === '') { + if (options.x <= 0) { pointX = options.x = 0; } + else { pointX = options.x; } + intervalLength = rect.width / length; + } + else if (isLeft && angle !== 0) { + intervalLength = rect.width / length; + if (rect.x + intervalLength > options.x + rotatedLabelSize.width) { + options.x = pointX = rect.x + padding; + } + else { + options.x = pointX = rect.x + intervalLength - padding; + } + } + else if (isAxisBreakLabel && axis.labelStyle.placement === 'OnTicks' && angle === 0) { + let maxWidth: number = 0; + for (let i: number = 0; i < label.text.length; i++) { + const breakLabelWidth: number = measureText( + label.text[i as number] as string, + axis.labelStyle as TextStyleModel, chart.themeStyle.axisLabelFont).width; + if (breakLabelWidth > maxWidth) { + maxWidth = breakLabelWidth; + } + } + options.x = pointX = rect.x + maxWidth / 2; + } + else if (!(anchor === 'start' && options.x > 0)) { + options.x = pointX = !isHorizontalAngle ? rect.x + padding : rect.x; + } + } else if ( + (i === len - 1 || (isInverse && i === 0)) && + ( + ((options.x + width) > chart.availableSize.width - chart.border.width - legendWidth && (anchor === 'start' || anchor === '') && angle === 0) || + ((anchor === 'start') && angle !== 0 && !isLeft && (options.x + rotatedLabelSize.width) > chart.availableSize.width - chart.border.width - legendWidth) || + (anchor === 'middle' && angle !== 0 && !isLeft && (options.x + rotatedLabelSize.width / 2) > chart.availableSize.width - chart.border.width - legendWidth) || + (anchor === 'end' && angle !== 0 && !isLeft && options.x > chart.availableSize.width - chart.border.width - legendWidth) || + (anchor === 'end' && options.x > chart.availableSize.width - chart.border.width - legendWidth && angle === 0) || + (anchor === 'middle' && (options.x + width / 2) > chart.availableSize.width - chart.border.width - legendWidth && angle === 0) + )) { + const axisLabelWidth: number = angle !== 0 ? rotatedLabelSize.width : width; + let shiftedXValue: number; + //Apply a default 5px padding between the edge label and the chart container + const padding: number = 5; + if (anchor === 'start' || anchor === '') { + shiftedXValue = options.x - ((options.x + axisLabelWidth) - + chart.availableSize.width + chart.border.width + padding + legendWidth); + } else if (anchor === 'middle') { + shiftedXValue = options.x - ((options.x + axisLabelWidth / 2) - + chart.availableSize.width + chart.border.width + padding + legendWidth); + } else { + shiftedXValue = options.x - (options.x - (chart.availableSize.width + chart.border.width + padding + + legendWidth)); + } + + // Check for overlap with previous label + if (previousLabel !== 0 && shiftedXValue < previousLabel) { + const maxAvailableWidth: number = chart.availableSize.width - previousLabel; + label.text = useTextTrim(maxAvailableWidth, label.originalText, axis.labelStyle as TextStyleModel, + chart.isRtlEnabled, chart.themeStyle.axisLabelFont); + } else { + options.x = pointX = shiftedXValue; + } + } + break; + } + options.text = getLabelText(label, axis, intervalLength, chart); + options.labelRotation = angle; + // ------- Hide Calculation (Start) ------------- + // Currect label actual start value (Start) + let xValue: number; let xValue2: number; + if (isAxisBreakLabel && angle === 0) { + xValue = (options.x - (width / 2)); xValue2 = options.x + (width / 2); + + } else { + xValue = options.x; xValue2 = options.x + width; + } + // Currect label actual start value (End) + + if (angle === 0 && axis.labelStyle.intersectAction === 'Hide' && i !== 0 && + (!isInverse ? xValue <= previousEnd : xValue2 >= previousEnd)) { + continue; + } + + // Previous label actual end value (Start) + if (isAxisBreakLabel) { + previousEnd = isInverse ? (options.x - (width / 2)) : options.x + (width / 2); + } else { + previousEnd = isInverse ? options.x : options.x + width; + } + + if (angle !== 0) { + let height: number; let rect: Rect; + if (isAxisBreakLabel) { + let xAdjustment: number = 0; let yAdjustment: number = 0; + height = (label.breakLabelSize.height); + yAdjustment = (label.breakLabelSize.height) - 4; // 4 for label bound correction + + // xAdjustment (Start) + xAdjustment = -(label.breakLabelSize.width / 2); + + if (isLabelUnderAxisLine) { + yAdjustment = (label.breakLabelSize.height) / (options.text.length + 1); + } + rect = { x: options.x + xAdjustment, y: options.y - (yAdjustment), width: label.breakLabelSize.width, height: height }; + } else { + height = (pointY) - (options.y - ((label.size.height / 2))); + rect = { x: options.x, y: options.y - ((label.size.height / 2) - 5), width: label.size.width, height: height }; + } + const rectCoordinates: ChartLocationProps[] = getRectanglePoints(rect); + const rectCenterX: number = isAxisBreakLabel ? rect.x + (rect.width / 2) : pointX; + const rectCenterY: number = isAxisBreakLabel ? rect.y + (rect.height / 2) : (pointY - (height / 2)); + if (isAxisBreakLabel) { + options.transform = 'rotate(' + angle + ',' + rectCenterX + ',' + rectCenterY + ')'; + } else { + options.transform = 'rotate(' + angle + ',' + pointX + ',' + pointY + ')'; + } + newPoints.push(getRotatedRectangleCoordinates(rectCoordinates, rectCenterX, rectCenterY, angle)); + if (axis.labelStyle.intersectAction !== 'None') { + for (let index: number = i; index > 0; index--) { + if (newPoints[i as number] && newPoints[index - 1] && + isRotatedRectIntersect(newPoints[i as number], newPoints[index - 1])) { + newPoints[i as number] = []; + break; + } + } + } + const rotateAngle: boolean = ((angle > 0 && angle < 90) || (angle > 180 && angle < 270) || + (angle < -90 && angle > -180) || (angle < -270 && angle > -360)); + const textRect: Rect = { + x: options.x, y: options.y - (elementSize.height / 2 + padding / 2), + width: label.size.width, height: height + }; + const textRectCoordinates: ChartLocationProps[] = getRectanglePoints(textRect); + const rectPoints: ChartLocationProps[] = []; + const axisPadding: number = 5; + rectPoints.push({ x: rotateAngle ? chart.availableSize.width : axisPadding, y: axis.rect.y }); + rectPoints.push({ + x: rotateAngle ? chart.availableSize.width : + axisPadding, y: axis.rect.y + axis.maxLabelSize.height + }); + textPoints.push(getRotatedRectangleCoordinates(textRectCoordinates, rectCenterX, rectCenterY, angle)); + const newRect: Rect = { x: 0, y: axis.rect.y, width: chart.availableSize.width, height: axis.maxLabelSize.height * 2 }; + for (let k: number = 0; k < textPoints[i as number].length; k++) { + if (!axis.opposedPosition && !withInBounds(textPoints[i as number][k as number].x, textPoints[i as number][k as number].y, newRect) && typeof options.text === 'string') { + const interSectPoint: ChartLocationProps = calculateIntersection( + textPoints[i as number][0], textPoints[i as number][1], rectPoints[0], rectPoints[1]); + const rectPoint1: number = rotateAngle ? chart.availableSize.width - pointX : pointX; + const rectPoint2: number = interSectPoint.y - axis.rect.y; + const trimValue: number = Math.sqrt((rectPoint1 * rectPoint1) + (rectPoint2 * rectPoint2)); + options.text = useTextTrim(trimValue, label.text as string, label.labelStyle as TextStyleModel, + chart.enableRtl, chart.themeStyle.axisLabelFont); + } + } + } + axis.axislabelOptions.push(options); + labelElements.push( + + {typeof options.text !== 'string' && options.text.length > 1 + ? options.text.map((line: string, index: number) => ( + + {line} + + )) + : options.text + } + + ); + } + const labelElement: JSX.Element = <> + + + + + + + {labelElements} + + ; + axis.labelElement = labelElement; + if (axis.labelStyle.position === 'Outside') { + return labelElement; + } else { + return <>; + } +} + +/** + * Get rect coordinates + * + * @param {Rect} rect rect + * @returns {ChartLocationProps[]} rectangle points + * @private + */ +function getRectanglePoints(rect: Rect): ChartLocationProps[] { + const point1: ChartLocationProps = { x: rect.x, y: rect.y }; + const point2: ChartLocationProps = { x: rect.x + rect.width, y: rect.y }; + const point3: ChartLocationProps = { x: rect.x + rect.width, y: rect.y + rect.height }; + const point4: ChartLocationProps = { x: rect.x, y: rect.y + rect.height }; + return [point1, point2, point3, point4]; +} + +/** + * Get the coordinates of a rotated rectangle. + * + * @param {ChartLocationProps[]} actualPoints - The coordinates of the original rectangle. + * @param {number} centerX - The x-coordinate of the center of rotation. + * @param {number} centerY - The y-coordinate of the center of rotation. + * @param {number} angle - The angle of rotation in degrees. + * @returns {ChartLocationProps[]} - The coordinates of the rotated rectangle. + * @private + */ +export function getRotatedRectangleCoordinates( + actualPoints: ChartLocationProps[], centerX: number, centerY: number, angle: number +): ChartLocationProps[] { + const coordinatesAfterRotation: ChartLocationProps[] = []; + for (let i: number = 0; i < 4; i++) { + const point: ChartLocationProps = actualPoints[i as number]; + // translate point to origin + const tempX: number = point.x - centerX; + const tempY: number = point.y - centerY; + // now apply rotation + const rotatedX: number = tempX * Math.cos(degreeToRadian(angle)) - tempY * Math.sin(degreeToRadian(angle)); + const rotatedY: number = tempX * Math.sin(degreeToRadian(angle)) + tempY * Math.cos(degreeToRadian(angle)); + // translate back + point.x = rotatedX + centerX; + point.y = rotatedY + centerY; + coordinatesAfterRotation.push({ x: point.x, y: point.y }); + } + return coordinatesAfterRotation; +} + +/** + * Retrieves the text for a given label on the axis, potentially modifying it according to axis settings. + * + * @param {VisibleLabel} label - The label object containing text and formatting details. + * @param {AxisModel} axis - The axis model related to the label. + * @param {number} intervalLength - The interval length for spacing labels along the axis. + * @param {Chart} chart - The chart instance that provides context for the axis and its labels. + * @returns {string} The final text of the label, after processing and any necessary modifications. + * @private + */ +export function getLabelText( + label: VisibleLabel, + axis: AxisModel, + intervalLength: number, + chart: Chart +): string | string[] { + if (isBreakLabel(label.originalText)) { + const result: string[] = []; + for (let index: number = 0; index < label.text.length; index++) { + const str: string = findAxisLabel(axis, label.text[index as number], intervalLength, chart); + result.push(str); + } + return result; + } else { + return findAxisLabel(axis, label.text as string, intervalLength, chart); + } +} + +/** + * Determines the appropriate label for an axis by trimming it if necessary. + * + * @param {AxisModel} axis - The axis model containing details for the axis. + * @param {string} label - The original label of the axis. + * @param {number} width - The available width for the label. + * @param {Chart} chart - The chart instance which contains configuration for trimming. + * @returns {string} The processed label, trimmed if necessary to fit the specified width. + * @private + */ +export function findAxisLabel(axis: AxisModel, label: string, width: number, chart: Chart): string { + return (axis.labelStyle.intersectAction === 'Trim' && !axis.labelStyle.enableWrap ? + ((axis.angle % 360 === 0 && !axis.labelStyle.enableTrim) ? + useTextTrim(width, label, axis.labelStyle as TextStyleModel, chart.enableRtl, chart.themeStyle.axisLabelFont) + : label) : label); +} + +/** + * Draws the Y-axis labels for a chart based on the provided axis and scaling function. + * + * @param {AxisModel} axis - The axis model that defines the parameters for the labels. + * @param {number} index - The index position for the label in the axis label collection. + * @param {Rect} rect - The rectangular area where the labels will be drawn. + * @param {Chart} chart - The chart object for which the labels are being drawn. + * @param {Function} yScale - The scaling function used to position the labels correctly. + * @returns {JSX.Element[]} An array of JSX elements representing each Y-axis label. + * @private + */ +export function drawYAxisLabels(axis: AxisModel, index: number, rect: Rect, chart: Chart, yScale: Function): JSX.Element { + const labelElements: JSX.Element[] = []; + let label: VisibleLabel; + let pointX: number = 0; + let pointY: number = 0; + let elementSize: ChartSizeProps; + const labelSpace: number = axis.labelStyle.padding as number; + let isAxisBreakLabel: boolean; + const isLabelInside: boolean = axis.labelStyle.position === 'Inside'; + const isOpposed: boolean = axis.isAxisOpposedPosition; + let RotatedWidth: number; + const tickSpace: number = axis.labelStyle.position === axis.tickPosition ? (axis.majorTickLines.height as number) : 0; + let padding: number = tickSpace + labelSpace + (axis.lineStyle?.width as number) * 0.5; + const angle: number = axis.angle % 360; + const isVerticalAngle: boolean = (angle === -90 || angle === 90 || angle === 270 || angle === -270); + padding += (isVerticalAngle) ? (isLabelInside ? 5 : -5) : 0; + padding = (isOpposed) ? padding : -padding; + const scrollBarHeight: number = 0; + let textHeight: number; let textPadding: number; let maxLineWidth: number; const pixel: number = 10; + const isInverse: boolean = axis.isAxisInverse; + let previousEnd: number = isInverse ? rect.y : (rect.y + rect.height); + let labelPadding: number; let intervalLength: number; let labelHeight: number; let yAxisLabelX: number; + const isLabelOnAxisLineLeft: boolean = ((!isOpposed && !isLabelInside) || (isOpposed && isLabelInside)); + if (isLabelInside) { + labelPadding = !isLabelOnAxisLineLeft ? -padding : padding; + } else { + labelPadding = !isLabelOnAxisLineLeft ? -padding + (chart.enableRtl ? -scrollBarHeight : scrollBarHeight) : + padding + (chart.enableRtl ? -scrollBarHeight : scrollBarHeight); + } + const sizeWidth: number[] = []; const breakLabelSizeWidth: number[] = []; + axis.visibleLabels.map((item: VisibleLabel) => { + sizeWidth.push(item.size['width']); + breakLabelSizeWidth.push(item.breakLabelSize['width']); + }); + const LabelMaxWidth: number = Math.max(...sizeWidth); + const breakLabelMaxWidth: number = Math.max(...breakLabelSizeWidth); + RotatedWidth = LabelMaxWidth; + if (angle >= -45 && angle <= 45 && angle !== 0) { + RotatedWidth = LabelMaxWidth * Math.cos(angle * Math.PI / 180); + if (RotatedWidth < 0) { RotatedWidth = - RotatedWidth; } + } + for (let i: number = 0, len: number = axis.visibleLabels.length; i < len; i++) { + label = axis.visibleLabels[i as number]; + isAxisBreakLabel = isBreakLabel(label.originalText); + elementSize = isAxisBreakLabel ? axis.visibleLabels[i as number].breakLabelSize : label.size; + pointY = yScale(label.value, axis.updatedRect, axis); + textHeight = ((elementSize.height / 8) * axis.visibleLabels[i as number].text.length / 2); + textPadding = (chart.requireInvertedAxis && axis.labelStyle.position === 'Inside') ? 0 : ((elementSize.height / 4) * 3) + 3; + intervalLength = rect.height / axis.visibleLabels.length; + labelHeight = ((axis.labelStyle.intersectAction === 'Trim' || axis.labelStyle.intersectAction === 'Wrap') && angle !== 0 && + elementSize.width > intervalLength) ? intervalLength : elementSize.width; + pointY = (isAxisBreakLabel ? (axis.labelStyle.position === 'Inside' ? (pointY - (elementSize.height / 2) - textHeight + textPadding) + : (pointY - textHeight)) : (axis.labelStyle.position === 'Inside' ? pointY + textPadding : pointY)); + if (axis.labelStyle.position === 'Inside' && ((i === 0 && !axis.inverted) || (i === len - 1 && axis.inverted))) { + pointY -= (textPadding - ((chart.requireInvertedAxis && axis.labelStyle.position === 'Inside') ? 0 : (axis.opposedPosition ? -padding : padding))); + } + const gridWidth: number = axis.majorGridLines.width as number; + const tickWidth: number = axis.majorTickLines.width as number; + if (gridWidth > tickWidth) { + maxLineWidth = gridWidth; + } else { + maxLineWidth = tickWidth; + } + + if (axis.labelStyle?.align === 'Right') { + pointY = pointY - maxLineWidth - pixel; + } else if (axis.labelStyle?.align === 'Left') { + pointY = pointY + maxLineWidth + pixel; + } + + // label X value adjustment (Start) + if (isLabelInside) { + yAxisLabelX = labelPadding + ((angle === 0 ? elementSize.width : + (isAxisBreakLabel ? breakLabelMaxWidth : LabelMaxWidth)) / 2); + } else { + yAxisLabelX = labelPadding - ((angle === 0 ? elementSize.width : + (isAxisBreakLabel ? breakLabelMaxWidth : RotatedWidth)) / 2); + } + if (axis.labelStyle.enableWrap && chart.requireInvertedAxis && angle && ((!axis.opposedPosition && axis.labelStyle.position === 'Inside') || (axis.opposedPosition && axis.labelStyle.position === 'Outside'))) { + yAxisLabelX = axis.opposedPosition ? yAxisLabelX - LabelMaxWidth / 2 : yAxisLabelX + LabelMaxWidth / 2; + } + pointX = isOpposed ? (rect.x - yAxisLabelX) : (rect.x + yAxisLabelX); + if (isVerticalAngle) { + pointX += (isOpposed) ? 5 : -5; + } + yAxisLabelX = labelPadding; + const options: TextOption = { + id: `${chart.element.id}${index}_AxisLabel_${i}`, + x: pointX, + y: pointY, + anchor: 'middle', + text: label.text, + transform: `rotate(${angle},${pointX},${pointY})`, + labelRotation: angle, + fill: label.labelStyle?.color || chart.themeStyle.axisLabelFont.color, + fontSize: label.labelStyle?.fontSize || chart.themeStyle.axisLabelFont.fontSize, + fontFamily: label.labelStyle?.fontFamily || chart.themeStyle.axisLabelFont.fontFamily, + fontWeight: label.labelStyle?.fontWeight || chart.themeStyle.axisLabelFont.fontWeight, + fontStyle: label.labelStyle?.fontStyle || chart.themeStyle.axisLabelFont.fontStyle, + opacity: axis.labelStyle?.opacity as number, + baseLine: 'middle' + }; + switch (axis.labelStyle.edgeLabelPlacement) { + case 'None': + break; + case 'Hide': + if (((i === 0 || (isInverse && i === len - 1)) && options.y > rect.y) || + (((i === len - 1) || (isInverse && i === 0)) && options.y - elementSize.height * 0.5 < rect.y)) { + options.text = ''; + } + break; + case 'Shift': + if ((i === 0 || (isInverse && i === len - 1)) && options.y > rect.y + rect.height) { + options.y = pointY = rect.y + rect.height; + } else if (((i === len - 1) || (isInverse && i === 0)) && + (options.y <= 0)) { + options.y = pointY = rect.y + elementSize.height * 0.5; + } + break; + } + + // ------- Hide Calculation (Start) ------------- + let previousYValue: number = options.y; let currentYValue: number = options.y - labelHeight; + if (isAxisBreakLabel) { + previousYValue = (options.y - (labelHeight / 2)); currentYValue = options.y + (labelHeight / 2); + } + if ((angle === 90 || angle === 270) && axis.labelStyle.intersectAction === 'Hide' && i !== 0 && + (!isInverse ? previousYValue >= previousEnd : currentYValue <= previousEnd)) { + continue; + } + previousEnd = isInverse ? previousYValue : currentYValue; + // ------- Hide Calculation (End) -------------; + + axis.axislabelOptions.push(options); + labelElements.push( + + {typeof options.text !== 'string' && options.text.length > 1 + ? options.text.map((line: string, index: number) => ( + + {line} + + )) + : options.text + } + + ); + } + const axislabelElement: JSX.Element = <> + + {labelElements} + + ; + axis.labelElement = axislabelElement; + if (axis.labelStyle.position === 'Outside') { + return axislabelElement; + } else { + return <>; + } +} + +/** + * Draws the title for the Y-axis of the chart. + * + * @param {AxisModel} axis - The axis model containing details for the axis. + * @param {number} index - The index position of the axis in its collection. + * @param {Rect} rect - The rectangle area where the axis title will be drawn. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @returns {void} + * @private + */ +function calculateYAxisTitleOptions(axis: AxisModel, index: number, rect: Rect, chart: Chart): void { + if (!axis.titleStyle.text) { return; } + + let isRotated: boolean = false; + const labelPadding: number = 5; + const isOpposed: boolean = axis.isAxisOpposedPosition; + const labelRotation: number = (axis.titleStyle.rotationAngle === undefined ? + (isOpposed ? 90 : -90) : axis.titleStyle.rotationAngle) % 360; + let padding: number = + (axis.tickPosition === 'Inside' ? 0 : (axis.majorTickLines.height as number) + (axis.titleStyle.padding as number)) + + (axis.labelStyle.position === 'Inside' ? 0 : axis.maxLabelSize.width + labelPadding); + + padding = + axis.tickPosition !== 'Outside' && (axis.tickPosition === 'Inside' || axis.labelStyle.position === 'Inside') + ? (axis.titleStyle.padding === 5 ? padding : padding + (axis.titleStyle.padding as number)) + : padding; + + const scrollPadding: number = 0; + padding = isOpposed ? padding + scrollPadding : -padding - scrollPadding; + if ((labelRotation !== -90 && !isOpposed) || (labelRotation !== 90 && isOpposed)) { + const axislabelPadding: number = axis.labelStyle.padding as number; + const labelPad: number = axis.labelStyle.position === 'Inside' ? axislabelPadding !== 5 ? 0 : + axislabelPadding : axislabelPadding; + padding += isOpposed + ? axis.titleSize.width / 2 + labelPad + : -axis.titleSize.width / 2 - labelPad; + + isRotated = true; + } + const x: number = rect.x + padding; + let y: number; + let anchor: string; + switch (axis.titleStyle?.align) { + case 'Left': + anchor = axis.opposedPosition ? 'end' : 'start'; + y = rect.height + rect.y; + break; + case 'Right': + anchor = axis.opposedPosition ? 'start' : 'end'; + y = rect.y; + break; + default: + anchor = 'middle'; + y = rect.y + rect.height * 0.5; + break; + } + + const titleOffset: number = axis.titleSize.height * (axis.titleCollection.length - 1); + const labelPad: number = + axis.labelStyle.position === 'Inside' + ? axis.labelStyle.padding !== 5 + ? 0 + : (axis.labelStyle.padding as number) + : (axis.labelStyle.padding as number); + + const finalY: number = y + (isRotated ? -titleOffset : -labelPad - titleOffset); + const transform: string = `rotate(${labelRotation},${x},${y})`; + const options: TextOption = { + id: `${chart.element.id}_AxisTitle_${index}`, + x: x, + y: finalY, + anchor: anchor, + text: axis.titleCollection, + transform: transform, + labelRotation: labelRotation, + fontFamily: axis.titleStyle?.fontFamily || chart.themeStyle.axisTitleFont.fontFamily, + fontWeight: axis.titleStyle?.fontWeight || chart.themeStyle.axisTitleFont.fontWeight, + fontSize: axis.titleStyle?.fontSize || chart.themeStyle.axisTitleFont.fontSize, + fontStyle: axis.titleStyle?.fontStyle || chart.themeStyle.axisTitleFont.fontStyle, + opacity: axis.titleStyle?.opacity as number, + fill: axis.titleStyle?.color || chart.themeStyle.axisTitleFont.color, + baseLine: '' + }; + axis.axisTitleOptions.push(options); +} + +/** + * Draws the title for the X-axis of the chart. + * + * @param {AxisModel} axis - The axis model containing details for the axis. + * @param {number} index - The index position of the axis in its collection. + * @param {Rect} rect - The rectangle area where the axis title will be drawn. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @returns {void} + * @private + */ +function calculateXAxisTitleOptions(axis: AxisModel, index: number, rect: Rect, chart: Chart): void { + if (!axis.titleStyle.text) { return; } + + const elementSize: ChartSizeProps = measureText( + axis.titleStyle.text, + axis.titleStyle as TextStyleModel, + chart.themeStyle.axisTitleFont + ); + const labelRotation: number = (axis.titleStyle.rotationAngle ? axis.titleStyle.rotationAngle : 0) % 360; + const scrollBarHeight: number = 0; + let padding: number = + (axis.tickPosition === 'Inside' ? 0 : (axis.majorTickLines.height as number) + (axis.titleStyle.padding as number)) + + (axis.labelStyle.position === 'Inside' + ? 0 + : axis.maxLabelSize.height + (axis.labelStyle.padding as number)); + + padding = + axis.tickPosition !== 'Outside' && + (axis.labelStyle.position === 'Inside' || axis.tickPosition === 'Inside') + ? axis.titleStyle.padding === 5 + ? padding + : padding + (axis.titleStyle.padding as number) + : padding; + padding += 5 / 2; + + const titleSize: number = axis.titleSize.height * (axis.titleCollection.length - 1); + padding = axis.isAxisOpposedPosition + ? -(padding + elementSize.height / 4 + scrollBarHeight + titleSize) + : padding + (3 * elementSize.height) / 4 + scrollBarHeight; + + let y: number = rect.y + padding; + let x: number; + let anchor: 'start' | 'middle' | 'end'; + + switch (axis.titleStyle?.align) { + case 'Left': + anchor = 'start'; + x = rect.x; + break; + case 'Right': + anchor = 'end'; + x = rect.x + rect.width; + break; + default: + anchor = 'middle'; + x = rect.x + rect.width * 0.5; + break; + } + + if (labelRotation !== 0) { + y += axis.opposedPosition + ? -(axis.titleSize.height / 2 + elementSize.height / 4) + : axis.titleSize.height / 2 - elementSize.height / 4; + } + + const options: TextOption = { + id: `${chart.element.id}_AxisTitle_${index}`, + x: x, + y: y, + anchor: anchor, + text: axis.titleCollection, + transform: `rotate(${labelRotation},${x},${y})`, + labelRotation: axis.titleStyle.rotationAngle as number, + fontFamily: axis.titleStyle?.fontFamily || chart.themeStyle.axisTitleFont.fontFamily, + fontWeight: axis.titleStyle?.fontWeight || chart.themeStyle.axisTitleFont.fontWeight, + fontSize: axis.titleStyle?.fontSize || chart.themeStyle.axisTitleFont.fontSize, + fontStyle: axis.titleStyle?.fontStyle || chart.themeStyle.axisTitleFont.fontStyle, + opacity: axis.titleStyle?.opacity as number, + fill: axis.titleStyle?.color || chart.themeStyle.axisTitleFont.color, + baseLine: '' + }; + axis.axisTitleOptions.push(options); +} + +/** + * Determines if the given axis border should be rendered. + * + * @param {AxisModel} axis - The axis model to check. + * @param {number} index - The index of the axis. + * @param {number} value - The current value on the axis. + * @param {Rect} seriesClipRect - The rectangle defining the series clipping area. + * @param {Chart} chart - The chart instance. + * @returns {boolean} True if the border should be drawn, false otherwise. + * @private + */ +export function isBorder(axis: AxisModel, index: number, value: number, seriesClipRect: Rect, chart: Chart): boolean { + const border: Required = chart.chartArea.border as Required; + const rect: Rect = seriesClipRect; + const orientation: Required = axis.orientation as Required; + const start: number = orientation === 'Horizontal' ? rect.x : rect.y; + const size: number = orientation === 'Horizontal' ? rect.width : rect.height; + const startIndex: number = orientation === 'Horizontal' ? 0 : axis.visibleLabels.length - 1; + const endIndex: number = orientation === 'Horizontal' ? axis.visibleLabels.length - 1 : 0; + + if ((value === start || value === start + size) && + (border.width <= 0 || border.color === 'transparent')) { + return true; + } else if ((value !== start && index === startIndex) || + (value !== (start + size) && index === endIndex)) { + return true; + } + return false; +} + +/** + * Calculates the appropriate tick size for an axis in relation to its crossing axis. + * + * @param {AxisModel} axis - The main axis model for which the tick size is calculated. + * @returns {number} The computed tick size for the main axis. + * @private + */ +export function findTickSize( + axis: AxisModel +): number { + if (axis.tickPosition === 'Inside') { + return 0; + } + return (axis.majorTickLines.height as number); +} + +/** + * Calculates and sets the fixed chart area dimensions based on the provided chart and width. + * + * @param {Chart} chart - The chart instance for which the fixed area is calculated. + * @param {number} chartAreaWidth - The width to set for the chart's series clip rectangle. + * @returns {void} + * @private + */ +function calculateFixedChartArea(chart: Chart, chartAreaWidth: number): void { + chart.chartAxislayout.seriesClipRect.width = chartAreaWidth; + chart.chartAxislayout.seriesClipRect.x = chart.availableSize.width - chart.margin.right - chartAreaWidth; + // - (chart.legendSettings.position === 'Right' ? chart.legendModule.legendBounds.width : 0); + for (const item of chart.rows) { + chart.chartAxislayout.seriesClipRect.x -= calculateAxisSum((item).farSizes); + } +} + +/** + * Calculates the intersection point of two lines represented by two pairs of coordinates. + * + * @param {ChartLocationProps} p1 - The first point of the first line. + * @param {ChartLocationProps} p2 - The second point of the first line. + * @param {ChartLocationProps} p3 - The first point of the second line. + * @param {ChartLocationProps} p4 - The second point of the second line. + * @returns {ChartLocationProps} The intersection point of the two lines. + * @private + */ +function calculateIntersection( + p1: ChartLocationProps, p2: ChartLocationProps, + p3: ChartLocationProps, p4: ChartLocationProps): ChartLocationProps { + const c2x: number = p3.x - p4.x; + const c3x: number = p1.x - p2.x; + const c2y: number = p3.y - p4.y; + const c3y: number = p1.y - p2.y; + const d: number = c3x * c2y - c3y * c2x; + const u1: number = p1.x * p2.y - p1.y * p2.x; + const u4: number = p3.x * p4.y - p3.y * p4.x; + const px: number = (u1 * c2x - c3x * u4) / d; + const py: number = (u1 * c2y - c3y * u4) / d; + const p: ChartLocationProps = { x: px, y: py }; + return p; +} + +/** + * Draws major grid lines on a chart based on the given axis and scale. + * + * @param {number} index - The index of the grid line. + * @param {AxisModel} axis - The axis model for which the grid lines are to be drawn. + * @param {Chart} chart - The chart instance on which the grid lines will appear. + * @param {AxisModel} currentAxis - The current axis model being used for drawing. + * @param {Function} scale - The scale function used to determine the placement of the grid lines. + * @returns {JSX.Element | null} A JSX element representing the grid line, or null if not applicable. + * @private + */ +export function drawMajorGridLines( + index: number, + axis: AxisModel, + chart: Chart, + currentAxis: AxisModel, + scale: Function +): JSX.Element | null { + const isVertical: boolean = axis.orientation === 'Vertical'; + const seriesClipRect: Rect = chart.chartAxislayout.seriesClipRect; + const gridElements: JSX.Element[] = []; + let length: number = axis.visibleLabels.length; + if (axis.valueType === 'Category' && length > 0 && axis.labelStyle.placement === 'BetweenTicks') { + length += 1; + } + const ticksbwtLabel: number = (axis.valueType === 'Category' && currentAxis.labelStyle.placement === 'BetweenTicks') ? 0.5 : 0; + for (let i: number = 0; i < length; i++) { + const tempInterval: number = axis.visibleLabels[i as number] ? axis.visibleLabels[i as number].value - ticksbwtLabel + : (axis.visibleLabels[i - 1].value + axis.visibleRange.interval) - ticksbwtLabel; + const position: number = scale(tempInterval, axis.updatedRect, axis); + const isInBounds: boolean = isVertical + ? position >= axis.updatedRect.y && axis.updatedRect.y + axis.updatedRect.height >= position + : position >= axis.updatedRect.x && axis.updatedRect.x + axis.updatedRect.width >= position; + if (isInBounds && ((inside(tempInterval, axis.visibleRange)) || isBorder(axis, i, position, seriesClipRect, chart))) { + const element: JSX.Element = ; + gridElements.push(element); + } + } + return ( + + {gridElements} + + ); +} + +/** + * Draws an axis line on a chart. + * + * @param {AxisModel} axis - The axis model representing the parameters for the main axis line. + * @param {AxisModel} currentAxis - Represents the current axis settings being rendered. + * @param {Chart} chart - The chart object on which the axis line will be drawn. + * @returns {JSX.Element} A JSX element representing the axis line. + * @private + */ +export function drawAxisLine( + axis: AxisModel, + currentAxis: AxisModel, + chart: Chart +): JSX.Element { + return ( + + ); +} + +/** + * Draws the Y-axis labels for a chart based on the given axis and scaling function. + * + * @param {Chart} chart - The chart instance. + * @returns {JSX.Element[]} An array of JSX Elements for the labels. + * @private + */ +export function drawBottomLines(chart: Chart): JSX.Element { + return ( + + {chart.paneLineOptions.map((line: PathOptions, index: number) => ( + + ))} + + ); +} + + +/** + * Renders the title for a given axis in the chart. + * + * @param {AxisModel} axis - The axis model containing the title configuration and styling. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @param {number} index - The index of the axis in the axis collection. + * @param {AxisModel} currentAxis - The currently processed axis for which the title is being rendered. + * @returns {JSX.Element} A JSX element representing the axis title. + * @private + */ +export function drawAxisTitle(axis: AxisModel, chart: Chart, index: number, currentAxis: AxisModel): JSX.Element { + let titleElement: JSX.Element = <>; + axis.axisTitleOptions.map((title: TextOption, i: number) => { + titleElement = + + {axis.titleCollection.length > 1 + ? (title.text as string[]).map((line: string, index: number) => ( + + {line} + + )) + : title.text[0] + } + ; + }); + return ( + titleElement + ); +} + +/** + * Renders the major tick lines for the X-axis of the chart. + * + * @param {AxisModel} axis - The axis model containing configuration for the X-axis. + * @param {number} index - The index of the axis in the axis collection. + * @param {Function} scale - A scaling function used to map axis values to pixel positions. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @param {AxisModel} currentAxis - The currently processed axis for which tick lines are being rendered. + * @returns {JSX.Element} A JSX element representing the X-axis major tick lines. + * @private + */ +export function drawYAxisTickLines(axis: AxisModel, index: number, scale: Function, chart: Chart, currentAxis: AxisModel): JSX.Element { + const ticksbwtLabel: number = (axis.valueType === 'Category' && currentAxis.labelStyle.placement === 'BetweenTicks') ? 0.5 : 0; + let length: number = axis.visibleLabels.length; + if (axis.valueType === 'Category' && length > 0 && axis.labelStyle.placement === 'BetweenTicks') { + length += 1; + } + const tickElements: JSX.Element[] = []; + for (let i: number = 0; i < length; i++) { + const tempInterval: number = !axis.visibleLabels[i as number] ? + (axis.visibleLabels[i - 1].value + axis.visibleRange.interval) - ticksbwtLabel + : axis.visibleLabels[i as number].value - ticksbwtLabel; + const y: number = scale(tempInterval, axis.updatedRect, axis); + const isTickInside: boolean = axis.tickPosition === 'Inside'; + const isOpposed: boolean = axis.isAxisOpposedPosition; + const tickSize: number = isOpposed ? (currentAxis.majorTickLines.height as number) : + -(currentAxis.majorTickLines.height as number); + const axisLineSize: number = isOpposed ? (currentAxis.lineStyle?.width as number) * 0.5 : + -(currentAxis.lineStyle?.width as number) * 0.5; + const ticks: number = isTickInside + ? axis.updatedRect.x - tickSize - axisLineSize + : axis.updatedRect.x + tickSize + axisLineSize; + const x1: number = axis.updatedRect.x + axisLineSize; + const x2: number = ticks; + if (y >= axis.updatedRect.y && axis.updatedRect.y + axis.updatedRect.height >= y) { + const element: JSX.Element = ; + tickElements.push(element); + } + } + const tickLineElement: JSX.Element = <> + + {tickElements} + + ; + if (axis.tickPosition === 'Outside') { + return tickLineElement; + } else { + axis.majorTickLineElement = tickLineElement; + return <>; + } +} + +/** + * Renders the major tick lines for the X-axis of the chart. + * + * @param {AxisModel} axis - The axis model containing configuration for the X-axis. + * @param {number} index - The index of the axis in the axis collection. + * @param {Function} scale - A scaling function used to map axis values to pixel positions. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @param {AxisModel} currentAxis - The currently processed axis for which tick lines are being rendered. + * @returns {JSX.Element} A JSX element representing the X-axis major tick lines. + * @private + */ +export function drawXAxisTickLines(axis: AxisModel, index: number, scale: Function, chart: Chart, currentAxis: AxisModel): JSX.Element { + const ticksbwtLabel: number = (axis.valueType === 'Category' && currentAxis.labelStyle.placement === 'BetweenTicks') ? 0.5 : 0; + let length: number = axis.visibleLabels.length; + if (axis.valueType === 'Category' && length > 0 && axis.labelStyle.placement === 'BetweenTicks') { + length += 1; + } + const gridElements: JSX.Element[] = []; + for (let i: number = 0; i < length; i++) { + const tempInterval: number = axis.visibleLabels[i as number] ? axis.visibleLabels[i as number].value - ticksbwtLabel + : (axis.visibleLabels[i - 1].value + axis.visibleRange.interval) - ticksbwtLabel; + const x: number = scale(tempInterval, axis.updatedRect, axis); + const isOpposed: boolean = axis.isAxisOpposedPosition; + const tickSize: number = isOpposed ? -(currentAxis.majorTickLines.height as number) : + (currentAxis.majorTickLines.height as number); + const axisLineSize: number = isOpposed ? -(currentAxis.lineStyle?.width as number) * 0.5 : + (currentAxis.lineStyle?.width as number) * 0.5; + const isTickInside: boolean = axis.tickPosition === 'Inside'; + const ticks: number = isTickInside ? axis.updatedRect.y - tickSize - axisLineSize + : axis.updatedRect.y + tickSize + axisLineSize; + if (x >= axis.updatedRect.x && axis.updatedRect.x + axis.updatedRect.width >= x) { + const element: JSX.Element = ; + gridElements.push(element); + } + } + + const tickLineElement: JSX.Element = <> + + {gridElements} + + ; + if (axis.tickPosition === 'Outside') { + return tickLineElement; + } else { + axis.majorTickLineElement = tickLineElement; + return <>; + } +} + +/** + * Renders the minor tick marks for the X-axis of the chart. + * + * @param {AxisModel} axis - The axis model containing configuration for the X-axis. + * @param {number} index - The index of the axis in the axis collection. + * @param {Function} scale - A scaling function used to map axis values to pixel positions. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @param {AxisModel} currentAxis - The currently processed axis for which minor ticks are being rendered. + * @returns {JSX.Element} A JSX element representing the X-axis minor tick marks. + * @private + */ +export function drawXAxisMinorTicks(axis: AxisModel, index: number, scale: Function, chart: Chart, currentAxis: AxisModel): JSX.Element { + const ticksbwtLabel: number = (axis.valueType === 'Category' && currentAxis.labelStyle.placement === 'BetweenTicks') ? 0.5 : 0; + const tickLineElement: JSX.Element = <> + + {(currentAxis.minorTicksPerInterval as number) > 0 && + axis.visibleLabels.map((label: VisibleLabel, i: number, arr: VisibleLabel[]) => { + const current: number = label.value; + const next: number = arr[i + 1]?.value ?? current + axis.visibleRange.interval; + const interval: number = (next - current) / ((currentAxis.minorTicksPerInterval as number) + 1); + return Array.from({ + length: currentAxis.minorTicksPerInterval as number + }, (_: unknown, j: number) => { + const val: number = current + interval * (j + 1); + if (!inside(val - ticksbwtLabel, axis.visibleRange)) { return null; } + const x: number = scale(val - ticksbwtLabel, axis.updatedRect, axis); + const isTickInside: boolean = axis.tickPosition === 'Inside'; + const tickSize: number = axis.isAxisOpposedPosition ? + -(currentAxis.minorTickLines.height as number) : + (currentAxis.minorTickLines.height as number); + const ticksX: number = isTickInside ? (axis.updatedRect.y - tickSize) : + (axis.updatedRect.y + tickSize); + return ( + x >= axis.updatedRect.x && axis.updatedRect.x + axis.updatedRect.width >= x && + ); + }); + }) + } + + ; + if (axis.tickPosition === 'Outside') { + return tickLineElement; + } else { + axis.minorTickLineElement = tickLineElement; + return <>; + } +} + +/** + * Renders the minor tick marks for the Y-axis of the chart. + * + * @param {AxisModel} axis - The axis model containing configuration for the Y-axis. + * @param {number} index - The index of the axis in the axis collection. + * @param {Function} scale - A scaling function used to map axis values to pixel positions. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @param {AxisModel} currentAxis - The currently processed axis for which minor ticks are being rendered. + * @returns {JSX.Element} A JSX element representing the Y-axis minor tick marks. + * @private + */ +export function drawYAxisMinorTicks(axis: AxisModel, index: number, scale: Function, chart: Chart, currentAxis: AxisModel): JSX.Element { + const ticksbwtLabel: number = (axis.valueType === 'Category' && currentAxis.labelStyle.placement === 'BetweenTicks') ? 0.5 : 0; + const tickLineElement: JSX.Element = <> + + {(currentAxis.minorTicksPerInterval as number) > 0 && + axis.visibleLabels.map((label: VisibleLabel, i: number, arr: VisibleLabel[]) => { + const current: number = label.value; + const next: number = arr[i + 1]?.value ?? current + axis.visibleRange.interval; + const interval: number = (next - current) / ((axis.minorTicksPerInterval as number) + 1); + const isTickInside: boolean = axis.tickPosition === 'Inside'; + const tickSize: number = axis.isAxisOpposedPosition ? + -(currentAxis.minorTickLines.height as number) : + currentAxis.minorTickLines.height as number; + const ticksY: number = isTickInside ? (axis.updatedRect.x + tickSize) : + (axis.updatedRect.x - tickSize); + return Array.from({ + length: currentAxis.minorTicksPerInterval as number + }, (_: unknown, j: number) => { + const val: number = current + interval * (j + 1); + if (!inside(val - ticksbwtLabel, axis.visibleRange)) { + return null; + } + const y: number = scale(val - ticksbwtLabel, axis.updatedRect, axis); + const x1: number = axis.rect.x + axis.rect.width; + return ( + () + ); + }); + }) + } + + ; + if (axis.tickPosition === 'Outside') { + return tickLineElement; + } else { + axis.minorTickLineElement = tickLineElement; + return <>; + } +} + +/** + * Renders the minor grid lines for the X-axis of the chart. + * + * @param {AxisModel} axis - The axis model containing configuration for the X-axis. + * @param {number} index - The index of the axis in the axis collection. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @param {Function} scale - A scaling function used to map axis values to pixel positions. + * @param {AxisModel} currentAxis - The currently processed axis for which minor grid lines are being rendered. + * @returns {JSX.Element} A JSX element representing the X-axis minor grid lines. + * @private + */ +export function drawXAxisMinorGridLines( + axis: AxisModel, index: number, chart: Chart, scale: Function, currentAxis: AxisModel): JSX.Element { + const ticksbwtLabel: number = (axis.valueType === 'Category' && currentAxis.labelStyle.placement === 'BetweenTicks') ? 0.5 : 0; + return ( + + {(currentAxis.minorTicksPerInterval as number) > 0 && + axis.visibleLabels.map((label: VisibleLabel, i: number, arr: VisibleLabel[]) => { + const current: number = label.value; + const next: number = arr[i + 1]?.value ?? current + axis.visibleRange.interval; + const interval: number = (next - current) / ((currentAxis.minorTicksPerInterval as number) + 1); + + return Array.from({ + length: currentAxis.minorTicksPerInterval as number + }, (_: unknown, j: number) => { + const val: number = current + interval * (j + 1); + if (!inside(val - ticksbwtLabel, axis.visibleRange)) { return null; } + const x: number = scale(val - ticksbwtLabel, axis.updatedRect, axis); + const seriesClipRect: Rect = chart.chartAxislayout.seriesClipRect; + return ( + x >= axis.updatedRect.x && axis.updatedRect.x + axis.updatedRect.width >= x && + ); + }); + }) + } + + + ); +} + +/** + * Renders the minor grid lines for the Y-axis of the chart. + * + * @param {AxisModel} axis - The axis model containing configuration for the Y-axis. + * @param {number} index - The index of the axis in the axis collection. + * @param {Chart} chart - The chart instance to which the axis belongs. + * @param {Function} scale - A scaling function used to map axis values to pixel positions. + * @param {AxisModel} currentAxis - The currently processed axis for which minor grid lines are being rendered. + * @returns {JSX.Element} A JSX element representing the Y-axis minor grid lines. + * @private + */ +export function drawYAxisMinorGridLines(axis: AxisModel, index: number, chart: Chart, + scale: Function, currentAxis: AxisModel): JSX.Element { + const ticksbwtLabel: number = (axis.valueType === 'Category' && currentAxis.labelStyle.placement === 'BetweenTicks') ? 0.5 : 0; + return ( + + {(axis.minorTicksPerInterval as number) > 0 && + axis.visibleLabels.map((label: VisibleLabel, i: number, arr: VisibleLabel[]) => { + const current: number = label.value; + const next: number = arr[i + 1]?.value ?? current + axis.visibleRange.interval; + const interval: number = (next - current) / ((currentAxis.minorTicksPerInterval as number) + 1); + return Array.from({ + length: (currentAxis.minorTicksPerInterval as number) + }, (_: unknown, j: number) => { + const val: number = current + interval * (j + 1); + if (!inside(val - ticksbwtLabel, axis.visibleRange)) { + return null; + } + const y: number = scale(val - ticksbwtLabel, axis.updatedRect, axis); + const seriesClipRect: Rect = chart.chartAxislayout.seriesClipRect; + return ( + y >= axis.updatedRect.y && axis.updatedRect.y + + axis.updatedRect.height >= y && ( + ) + ); + }); + }) + } + + ); +} diff --git a/components/charts/src/chart/renderer/AxesRenderer/ChartColumnsRender.tsx b/components/charts/src/chart/renderer/AxesRenderer/ChartColumnsRender.tsx new file mode 100644 index 0000000..e4f19b3 --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/ChartColumnsRender.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { useLayout } from '../../layout/LayoutContext'; +import { Column } from '../../base/interfaces'; +import { useEffect, useMemo } from 'react'; +import { Chart, ColumnProps, Rect } from '../../chart-area/chart-interfaces'; + +/** + * Props interface for the ChartColumnsRender component. + * Defines the required properties for column layout management. + */ +interface ChartColumnsRenderProps { + /** + * Array of column configurations with width specification. + */ + columns: Column[]; +} + +/** + * A React component that manages column layout calculations for chart rendering. + */ + +export const ChartColumnsRender: React.FC = ({ columns }: { columns: Column[] }) => { + const { phase, triggerRemeasure } = useLayout(); + const columnWidths: string = useMemo( + () => columns.map((column: Column) => column.width).join(','), + [columns] + ); + useEffect(() => { + if (phase !== 'measuring') { + triggerRemeasure(); + } + }, [columnWidths]); + return <>; /* This component manages row layout calculations without rendering visible content */ +}; + +/** + * Calculates and adjusts the size of columns within the given chart based on the provided rectangular area. + * + * @param {Rect} rect - The rectangular area used for column size calculations. + * @param {Chart} chart - The chart for which column sizes are being adjusted. + * @returns {void} This function does not return any value. + * @private + */ +export function calculateColumnSize(rect: Rect, chart: Chart): void { + let columnLeft: number = rect.x; + let width: number = 0; + let remainingWidth: number = Math.max(0, rect.width); + + for (let i: number = 0; i < chart.columns.length; i++) { + const column: ColumnProps = chart.columns[i as number]; + if (column.width.includes('%')) { + width = Math.min(remainingWidth, (rect.width * parseInt(column.width, 10) / 100)); + } else { + width = Math.min(remainingWidth, parseInt(column.width, 10)); + } + width = (i !== (chart.columns.length - 1)) ? width : remainingWidth; + column.computedWidth = width; + column.computedLeft = columnLeft; + columnLeft += width; + remainingWidth -= width; + } +} diff --git a/components/charts/src/chart/renderer/AxesRenderer/ChartRowsRender.tsx b/components/charts/src/chart/renderer/AxesRenderer/ChartRowsRender.tsx new file mode 100644 index 0000000..9b17ba5 --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/ChartRowsRender.tsx @@ -0,0 +1,62 @@ +import { useLayout } from '../../layout/LayoutContext'; +import { Row } from '../../base/interfaces'; +import { useEffect, useMemo } from 'react'; +import { Chart, Rect, RowProps } from '../../chart-area/chart-interfaces'; + +/** + * Props for the ChartRowsRender component. + */ +interface ChartRowsRenderProps { + /** Array of row configurations defining chart layout rows. */ + rows: Row[]; +} + +/** + * Manages chart row layout calculations and triggers re-measurement when needed. + * This component doesn't render visible content but handles layout calculations. + */ + +export const ChartRowsRender: React.FC = ({ rows }: ChartRowsRenderProps) => { + const { phase, triggerRemeasure } = useLayout(); + + const rowHeights: string = useMemo( + () => rows.map((row: Row) => row.height).join(','), + [rows] + ); + useEffect(() => { + if (phase !== 'measuring') { + triggerRemeasure(); + } + }, [rowHeights]); + return <>; /* This component manages row layout calculations without rendering visible content */ +}; + +/** + * Calculates and adjusts the size of rows within the given chart based on the provided rectangular area. + * + * @param {Chart} chart - The chart for which the row sizes are calculated. + * @param {Rect} rect - The rectangular area that influences the row size calculations. + * @returns {void} This function does not return a value. + * @private + */ +export function calculateRowSize(chart: Chart, rect: Rect): void { + let rowTop: number = rect.y + rect.height; + let remainingHeight: number = Math.max(0, rect.height); + + for (let i: number = 0; i < chart.rows.length; i++) { + const row: RowProps = chart.rows[i as number]; + let height: number; + + if (row.height.includes('%')) { + height = Math.min(remainingHeight, (rect.height * parseInt(row.height, 10)) / 100); + } else { + height = Math.min(remainingHeight, parseInt(row.height, 10)); + } + + height = (i !== (chart.rows.length - 1)) ? height : remainingHeight; + row.computedHeight = height; + rowTop -= height; + row.computedTop = rowTop; + remainingHeight -= height; + } +} diff --git a/components/charts/src/chart/renderer/AxesRenderer/ChartStripLinesRender.tsx b/components/charts/src/chart/renderer/AxesRenderer/ChartStripLinesRender.tsx new file mode 100644 index 0000000..8339bc8 --- /dev/null +++ b/components/charts/src/chart/renderer/AxesRenderer/ChartStripLinesRender.tsx @@ -0,0 +1,885 @@ +import * as React from 'react'; +import { useLayoutEffect, useEffect, useState } from 'react'; +import { DataUtil } from '@syncfusion/react-data/src/util'; +import { logBase, measureText, withIn } from '../../utils/helper'; +import { isNullOrUndefined, getDateParser, getDateFormat, VerticalAlignment, HorizontalAlignment } from '@syncfusion/react-base'; +import { DateFormatOptions } from '../../chart-axis/base'; +import { useLayout } from '../../layout/LayoutContext'; +import { StripLineSizeUnit, ZIndex } from '../../base/enum'; +import { ChartStripLineProps } from '../../base/interfaces'; +import { Rect, AxisModel, Chart, ChartSizeProps, TextStyleModel, StriplineOptions, VisibleRangeProps, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { useAxisRenderVersion } from '../../hooks/useClipRect'; + +/** + * Props interface for strip line renderer components. + */ +interface StripLineRendererProps { + axes: AxisModel[]; +} + +/** + * Renders striplines behind the chart series. + * + * @param {object} props - Component properties. + * @param {AxisModel[]} props.axes - The axis models containing stripline configurations. + * @returns {React.ReactNode | null} The rendered stripline elements or null when not in rendering phase. + */ +export const StripLineBeforeRenderer: React.FC = ({ axes }: { axes: AxisModel[] }) => { + const { layoutRef, reportMeasured, phase, animationProgress } = useLayout(); + const [, setStriplineBehindElements] = useState(0); + const axisBehindInfo: { version: number; id: string } = useAxisRenderVersion(); + useLayoutEffect(() => { + if (phase === 'measuring') { + const chart: Chart = layoutRef.current.chart as Chart; + const stripLineBehindValue: StriplineOptions[] = renderStripLineElements(chart, chart.axisCollection, 'Behind'); + chart.striplineBehind = stripLineBehindValue; + reportMeasured('ChartStripLinesBehind'); + } + }, [phase, layoutRef, reportMeasured]); + + useEffect(() => { + if (phase !== 'measuring') { + const chart: Chart = layoutRef.current?.chart as Chart; + if (chart && axes && axes.some((axis: AxisModel) => axis.stripLines?.length as number > 0)) { + const updatedBehindValue: StriplineOptions[] = renderStripLineElements(chart, axes, 'Behind'); + chart.striplineBehind = updatedBehindValue; + } + setStriplineBehindElements((prev: number) => prev + 1); + } + }, + [ + ...axes?.flatMap((axis: AxisModel) => (axis.stripLines?.flatMap((stripLine: ChartStripLineProps) => [ + stripLine.range?.start, + stripLine.range?.end, + stripLine.style?.zIndex, + stripLine.range?.size, + stripLine.range?.sizeType, + stripLine.text?.content, + stripLine.text?.hAlign, + stripLine.text?.vAlign, + stripLine.text?.rotation, + stripLine.repeat?.every, + stripLine.repeat?.until, + stripLine.segment?.start, + stripLine.segment?.end, + stripLine.segment?.axisName, + stripLine.text?.font?.fontSize + ])) + ) + ] + ); + + useEffect(() => { + const chart: Chart = layoutRef.current?.chart as Chart; + if (phase !== 'measuring' && axisBehindInfo.id === chart.element.id) { + if (chart && axes && axes.some((axis: AxisModel) => axis.stripLines?.length as number > 0)) { + const updatedBehindValue: StriplineOptions[] = renderStripLineElements(chart, axes, 'Behind'); + chart.striplineBehind = updatedBehindValue; + } + setStriplineBehindElements((prev: number) => prev + 1); + } + }, [ + axisBehindInfo.version + ]); + + return (phase === 'rendering') && (() => { + const chart: Chart = layoutRef.current.chart as Chart; + const seriesClipRect: Rect = chart.chartAxislayout.seriesClipRect; + const id: string = `${chart.element.id}_stripline_Behind`; + const renderedBehindElements: React.ReactNode[] = chart.striplineBehind ? + createStripeLineElements(chart.striplineBehind, animationProgress) : []; + + const hasAnimatedSeries: boolean = chart.visibleSeries?.some((series: SeriesProperties) => series.animation?.enable) || false; + const striplineVisibility: 'visible' | 'hidden' = hasAnimatedSeries && !chart.isLegendClicked && chart.delayRedraw ? (animationProgress === 1 ? 'visible' : 'hidden') : 'visible'; + + return ( + + + + + + + {renderedBehindElements} + + ); + })(); +}; + +/** + * Renders striplines over the chart series. + * + * @param {object} props - Component properties. + * @param {AxisModel[]} props.axes - The axis models containing stripline configurations. + * @returns {React.ReactNode | null} The rendered stripline elements or null when not in rendering phase. + */ +export const StripLineAfterRenderer: React.FC = ({ axes }: { axes: AxisModel[] }) => { + const { layoutRef, reportMeasured, phase, animationProgress } = useLayout(); + const [, setStriplineOverElements] = useState(0); + const axisOverInfo: { version: number; id: string } = useAxisRenderVersion(); + useLayoutEffect(() => { + if (phase === 'measuring') { + const chart: Chart = layoutRef.current.chart as Chart; + const stripLineOverValue: StriplineOptions[] = renderStripLineElements(chart, chart.axisCollection, 'Over'); + chart.striplineOver = stripLineOverValue; + reportMeasured('ChartStripLinesOver'); + } + }, [phase, layoutRef, reportMeasured]); + + useEffect(() => { + if (phase !== 'measuring') { + const chart: Chart = layoutRef.current?.chart as Chart; + if (chart && axes && axes.some((axis: AxisModel) => axis.stripLines?.length as number > 0)) { + const updatedOverValue: StriplineOptions[] = renderStripLineElements(chart, axes, 'Over'); + chart.striplineOver = updatedOverValue; + } + setStriplineOverElements((prev: number) => prev + 1); + } + }, + [ + ...axes?.flatMap((axis: AxisModel) => (axis.stripLines?.flatMap((stripLine: ChartStripLineProps) => [ + stripLine.range?.start, + stripLine.range?.end, + stripLine.style?.zIndex, + stripLine.range?.size, + stripLine.range?.sizeType, + stripLine.text?.content, + stripLine.text?.hAlign, + stripLine.text?.vAlign, + stripLine.text?.rotation, + stripLine.repeat?.every, + stripLine.repeat?.until, + stripLine.segment?.start, + stripLine.segment?.end, + stripLine.segment?.axisName, + stripLine.text?.font?.fontSize + ])) + ) + ] + ); + + useEffect(() => { + const chart: Chart = layoutRef.current?.chart as Chart; + if (phase !== 'measuring' && axisOverInfo.id === chart.element.id) { + if (chart && axes && axes.some((axis: AxisModel) => axis.stripLines?.length as number > 0)) { + const updatedOverValue: StriplineOptions[] = renderStripLineElements(chart, axes, 'Over'); + chart.striplineOver = updatedOverValue; + } + setStriplineOverElements((prev: number) => prev + 1); + } + }, [ + axisOverInfo.version + ]); + + return (phase === 'rendering') && (() => { + const chart: Chart = layoutRef.current.chart as Chart; + const seriesClipRect: Rect = chart.chartAxislayout.seriesClipRect; + const id: string = `${chart.element.id}_stripline_Over`; + const renderedOverElements: React.ReactNode[] = chart.striplineOver ? + createStripeLineElements(chart.striplineOver, animationProgress) : []; + const hasAnimatedSeries: boolean = chart.visibleSeries?.some((series: SeriesProperties) => series.animation?.enable) || false; + const striplineVisibility: 'visible' | 'hidden' = hasAnimatedSeries && !chart.isLegendClicked && chart.delayRedraw ? (animationProgress === 1 ? 'visible' : 'hidden') : 'visible'; + + return ( + + + + + + + {renderedOverElements} + + ); + })(); +}; + +/** + * Renders stripline elements for the chart. + * + * @param {Chart} chart - The chart instance. + * @param {AxisModel[]} axes - Array of axis models. + * @param {ZIndex} position - Z-index position of the striplines. + * @returns {StriplineOptions[]} Array of stripline option objects. + * @private + */ +function renderStripLineElements(chart: Chart, axes: AxisModel[], position: ZIndex): StriplineOptions[] { + const stripLineOptions: StriplineOptions[] = []; + const seriesClipRect: Rect = chart.chartAxislayout.seriesClipRect; + const id: string = `${chart.element.id}_stripline_${position}`; + const xAxisIndices: Record = {}; + const yAxisIndices: Record = {}; + let end: number = 0; + let limit: number = 0; + let startValue: number = 0; + let segmentAxis: AxisModel | null = null; + let range: boolean; + for (let a: number = 0; a < axes.length; a++) { + const axis: AxisModel = axes[a as number]; + if (!axis.stripLines || axis.stripLines.length === 0) { + continue; + } + const axisName: string = axis.name as string; + let validAxis: AxisModel = axis; + if (chart.axisCollection?.length) { + const foundAxis: AxisModel = chart.axisCollection.find((currentAxis: AxisModel) => currentAxis.name === axis.name) as AxisModel; + if (foundAxis) { + validAxis = foundAxis; + } + } + if (validAxis.orientation === 'Horizontal') { + if (!xAxisIndices[axisName as string]) { + xAxisIndices[axisName as string] = 0; + } + } else { + if (!yAxisIndices[axisName as string]) { + yAxisIndices[axisName as string] = 0; + } + } + for (let i: number = 0; i < axis.stripLines.length; i++) { + const stripLine: ChartStripLineProps = axis.stripLines[i as number]; + if (!stripLine.visible || stripLine.style?.zIndex !== position) { + continue; + } + if (stripLine.segment?.enable && stripLine.segment?.start !== undefined && + stripLine.segment?.end !== undefined && stripLine.range?.sizeType !== 'Pixel') { + segmentAxis = getSegmentAxis(axes, axis, stripLine); + } + if (stripLine.repeat?.enable && stripLine.repeat?.every !== undefined && + stripLine.range?.size !== undefined && stripLine.range?.sizeType !== 'Pixel') { + limit = stripLine.repeat?.until !== undefined ? + (axis.valueType === 'DateTime' ? + dateToMilliSeconds(stripLine.repeat?.until) : + +stripLine.repeat?.until) : + validAxis.actualRange.maximum; + startValue = axis.valueType === 'DateTime' && isCoreDate(stripLine.range.start as number) ? + dateToMilliSeconds(stripLine.range.start as number) : + stripLine.range.start as number; + if ((stripLine.range.shouldStartFromAxis && axis.valueType === 'DateTime' && stripLine.range.sizeType === 'Auto') || + (stripLine.range.start === undefined) || (stripLine.range.start as number < validAxis.visibleRange.minimum)) { + startValue = validAxis.visibleLabels[0] && + validAxis.visibleLabels[0].value === validAxis.visibleRange.minimum ? + validAxis.visibleRange.minimum : + validAxis.visibleLabels[0] && validAxis.visibleLabels[0].value - + (axis.valueType === 'DateTime' ? axis.dateTimeInterval : validAxis.visibleRange.interval); + } + startValue = stripLine.range.shouldStartFromAxis && axis.valueType !== 'DateTime' ? + validAxis.visibleRange.minimum : startValue; + while (startValue < limit) { + end = startValue + (axis.valueType === 'DateTime' ? + axis.dateTimeInterval * (stripLine.range.size !== undefined ? +stripLine.range.size : 0) : + (stripLine.range.size ?? 0)); + range = withIn(end, validAxis.visibleRange); + if ((startValue >= validAxis.visibleRange.minimum && startValue < validAxis.visibleRange.maximum) || range) { + const axisIndex: number = validAxis.orientation === 'Horizontal' ? + xAxisIndices[axisName as string]++ : yAxisIndices[axisName as string]++; + const rect: Rect = measureStripLine(axis, stripLine, seriesClipRect, startValue, segmentAxis, chart); + stripLineOptions.push({ + id: `${id}`, + rect: rect, + stripLine: stripLine, + axis: axis, + axisIndex: axisIndex, + chart: chart, + position: position + }); + } + startValue = getStartValue(axis, stripLine, startValue, chart); + } + } else { + if ((stripLine.range?.start === undefined || stripLine.range?.end === undefined) && + !(stripLine.range?.shouldStartFromAxis && stripLine.range?.size !== undefined)) { + continue; + } + const axisIndex: number = validAxis.orientation === 'Horizontal' ? + xAxisIndices[axisName as string]++ : yAxisIndices[axisName as string]++; + const rect: Rect = measureStripLine(axis, stripLine, seriesClipRect, 0, segmentAxis, chart); + stripLineOptions.push({ + id: `${id}`, + rect: rect, + stripLine: stripLine, + axis: validAxis, + axisIndex: axisIndex, + chart: chart, + position: position + }); + } + } + } + return stripLineOptions; +} + +/** + * Adds stripline elements to the elements array. + * + * @param {StriplineOptions[]} stripLineOptions - The stripline options array. + * @param {number} animationProgress - The animation progress (0-1). + * @returns {React.ReactNode[]} Array of React nodes representing the stripline elements. + * @private + */ +function createStripeLineElements(stripLineOptions: StriplineOptions[], animationProgress: number = 1): React.ReactNode[] { + const resultElements: React.ReactNode[] = []; + const chart: Chart = stripLineOptions[0]?.chart as Chart; + const hasAnimatedSeries: boolean = chart?.visibleSeries?.some((series: SeriesProperties) => series.animation?.enable) || false; + const elementVisibility: 'visible' | 'hidden' = hasAnimatedSeries && !chart.isLegendClicked && chart.delayRedraw ? (animationProgress === 1 ? 'visible' : 'hidden') : 'visible'; + + for (let i: number = 0; i < stripLineOptions.length; i++) { + const option: StriplineOptions = stripLineOptions[i as number]; + if (option.stripLine?.style?.imageUrl) { + resultElements.push( + + ); + } else { + let strokeWidth: number = option.stripLine?.range?.size as number; + let direction: string = (option.axis?.orientation === 'Vertical') ? + ('M ' + option.rect?.x + ' ' + option.rect?.y + ' ' + 'L ' + ((option.rect?.x || 0) + (option.rect?.width || 0)) + ' ' + option.rect?.y) : + ('M ' + option.rect?.x + ' ' + option.rect?.y + ' ' + 'L ' + option.rect?.x + ' ' + ((option.rect?.y || 0) + (option.rect?.height || 0))); + + if (option.stripLine?.range?.sizeType !== 'Pixel') { + direction = (option.axis?.orientation === 'Vertical') ? + ('M ' + option.rect?.x + ' ' + ((option.rect?.y || 0) + ((option.rect?.height || 0) / 2)) + ' ' + 'L ' + ((option.rect?.x || 0) + (option.rect?.width || 0)) + ' ' + ((option.rect?.y || 0) + ((option.rect?.height || 0) / 2))) : + ('M ' + ((option.rect?.x || 0) + ((option.rect?.width || 0) / 2)) + ' ' + option.rect?.y + ' ' + 'L ' + ((option.rect?.x || 0) + ((option.rect?.width || 0) / 2)) + ' ' + ((option.rect?.y || 0) + (option.rect?.height || 0))); + + strokeWidth = (option.axis?.orientation === 'Vertical' ? option.rect?.height : option.rect?.width) as number; + } + resultElements.push( + + ); + const pixelRect: Rect = option.stripLine?.range?.sizeType === 'Pixel' ? { + x: option.axis?.orientation === 'Horizontal' ? ((option.rect?.x as number) - (option.stripLine.range.size as number) / 2) : (option.rect?.x as number), + y: option.axis?.orientation === 'Vertical' ? ((option.rect?.y as number) - (option.stripLine.range.size as number) / 2) : (option.rect?.y as number), + width: (option.rect?.width as number) ? (option.rect?.width as number) : (option.stripLine.range.size as number), + height: (option.rect?.height as number) ? (option.rect?.height as number) : (option.stripLine.range.size as number) + } : (option.rect as Rect || { x: 0, y: 0, width: 0, height: 0 }); + resultElements.push( + + ); + } + if (option.stripLine?.text) { + const textElement: React.ReactNode = renderText(option.stripLine, option.rect as Rect, `${option.id}_text_${option.axis?.name}_${option.axisIndex}`, option.chart as Chart, option.axis as AxisModel, elementVisibility); + resultElements.push(textElement); + } + } + return resultElements; +} + +/** + * Gets the axis for a segment based on the provided axes, current axis, and stripline settings. + * + * @param {AxisModel[]} axes - Array of available axis models. + * @param {AxisModel} axis - The current axis model. + * @param {ChartStripLineProps} stripLine - The stripline settings that may specify a segment axis. + * @returns {AxisModel | null} The segment axis if found, otherwise null. + * @private + */ +function getSegmentAxis(axes: AxisModel[], axis: AxisModel, stripLine: ChartStripLineProps): AxisModel | null { + let segment: AxisModel | null = null; + if (stripLine.segment?.axisName == null) { + return (axis.orientation === 'Horizontal') ? axes[1] : axes[0]; + + } else { + for (let i: number = 0; i < axes.length; i++) { + if (stripLine.segment.axisName === axes[i as number].name) { + segment = axes[i as number]; + } + } + return segment; + } +} + +/** + * Calculates the rectangular dimensions for a stripline based on axis and stripline settings. + * + * @param {AxisModel} axis - The axis model on which the stripline will be rendered. + * @param {ChartStripLineProps} stripline - The settings model defining the stripline properties. + * @param {Rect} seriesClipRect - The rectangle defining the series clip area. + * @param {number} startValue - The starting value for the stripline, if defined. + * @param {AxisModel | null} segmentAxis - Optional secondary axis for segmented striplines. + * @param {Chart} chart - The chart instance. + * @returns {Rect} The calculated rectangle for the stripline. + * @private + */ +function measureStripLine(axis: AxisModel, stripline: ChartStripLineProps, seriesClipRect: Rect, + startValue: number, segmentAxis: AxisModel | null, chart: Chart): Rect { + let actualStart: number; + let actualEnd: number; + let validAxis: AxisModel = axis; + let validSegmentAxis: AxisModel | null = segmentAxis; + if (chart.axisCollection?.length) { + const foundAxis: AxisModel = chart.axisCollection.find((currentAxis: AxisModel) => currentAxis.name === axis.name) as AxisModel; + if (foundAxis) { + validAxis = foundAxis; + } + if (segmentAxis) { + const foundSegmentAxis: AxisModel = chart.axisCollection.find((currentSegmentAxis: AxisModel) => + currentSegmentAxis.name === segmentAxis.name) as AxisModel; + if (foundSegmentAxis) { + validSegmentAxis = foundSegmentAxis; + } + } + } + const orientation: string = validAxis.orientation as string; + const isDateTimeAxis: boolean = axis.valueType === 'DateTime'; + if (stripline.repeat?.enable && stripline.range?.size !== null) { + actualStart = startValue as number; + actualEnd = 0; + } else { + actualStart = stripline.range?.start === 0 ? 0 : isDateTimeAxis && + isCoreDate(stripline.range?.start as Date | number | Object | string) ? + dateToMilliSeconds(stripline.range?.start as Date | number | Object) : +(axis.valueType === 'Logarithmic' ? logBase(stripline.range?.start as number, axis.logBase as number) : stripline.range?.start as Date | number | Object); + actualEnd = stripline.range?.end === 0 ? 0 : isDateTimeAxis && + isCoreDate(stripline.range?.start as Date | number | Object | string) ? + dateToMilliSeconds(stripline.range?.end as Date | number | Object) : +(axis.valueType === 'Logarithmic' ? logBase(stripline.range?.end as number, axis.logBase as number) : stripline.range?.end as Date | number | Object); + } + const rect: { from: number, to: number } = getFromToValue( + actualStart, actualEnd, stripline.range?.size as number, stripline.range?.shouldStartFromAxis || false, + axis, stripline, chart.axisCollection, chart + ); + let height: number = (orientation === 'Vertical') ? (rect.to - rect.from) * validAxis.rect.height : seriesClipRect.height; + let width: number = (orientation === 'Horizontal') ? (rect.to - rect.from) * validAxis.rect.width : seriesClipRect.width; + let x: number = (orientation === 'Vertical') ? seriesClipRect.x : ((rect.from * validAxis.rect.width) + validAxis.rect.x); + let y: number = (orientation === 'Horizontal') ? seriesClipRect.y : (validAxis.rect.y + validAxis.rect.height - + ((stripline.range?.sizeType === 'Pixel' ? rect.from : rect.to) * validAxis.rect.height)); + if (stripline.segment?.enable && stripline.segment.start != null && stripline.segment.end != null && + (stripline.range?.sizeType as string) !== 'Pixel' && validSegmentAxis) { + const start: number = isDateTimeAxis && isCoreDate(stripline.segment.start) ? + dateToMilliSeconds(stripline.segment.start) : +stripline.segment.start; + const end: number = isDateTimeAxis && isCoreDate(stripline.segment.end) ? + dateToMilliSeconds(stripline.segment.end) : +stripline.segment.end; + const segRect: { from: number, to: number } = getFromToValue(start, end, 0, false, validSegmentAxis, + stripline, chart.axisCollection, chart); + if (validSegmentAxis.orientation === 'Vertical') { + y = (validSegmentAxis.rect.y + validSegmentAxis.rect.height - (segRect.to * validSegmentAxis.rect.height)); + height = (segRect.to - segRect.from) * validSegmentAxis.rect.height; + } else { + x = ((segRect.from * validSegmentAxis.rect.width) + validSegmentAxis.rect.x); + width = (segRect.to - segRect.from) * validSegmentAxis.rect.width; + } + } + if ((height !== 0 && width !== 0) || ((stripline.range?.sizeType as string) === 'Pixel' && + (stripline.range?.start !== undefined || stripline.range?.shouldStartFromAxis))) { + return { x: x, y: y, width: width, height: height }; + } + return { x: 0, y: 0, width: 0, height: 0 }; +} + +/** + * Converts a date value to milliseconds. + * + * @param {Date | number | Object} value - The date value to convert. + * @returns {number} The date value converted to milliseconds. + * @private + */ +function dateToMilliSeconds(value: Date | number | Object): number { + return dateParse(value).getTime(); +} + +/** + * Parses a date value using the chart's locale settings. + * + * @param {Date | Object} value - The date value to parse. + * @returns {Date} The parsed date object. + * @private + */ +function dateParse(value: Date | Object): Date { + const option: DateFormatOptions = { + skeleton: 'full', + type: 'dateTime' + }; + const dateParser: Function = getDateParser(option); + const dateFormatter: Function = getDateFormat(option); + return new Date(Date.parse(dateParser(dateFormatter(new Date( + (DataUtil.parse as Required).parseJson({ val: value }).val))))); +} + +/** + * Determines if a value is a core date (string representation). + * + * @param {string | number | Object | Date} value - The value to check. + * @returns {boolean} True if the value is a string (core date), false otherwise. + * @private + */ +function isCoreDate(value: string | number | Object | Date): boolean { + return typeof value === 'string' ? true : false; +} + +/** + * Calculates the 'from' and 'to' values for a stripline. + * + * @param {number} start - The starting value of the stripline. + * @param {number} end - The ending value of the stripline. + * @param {number} size - The size of the stripline. + * @param {boolean} startFromAxis - Whether the stripline should start from the axis. + * @param {AxisModel} axis - The axis model associated with the stripline. + * @param {ChartStripLineProps} stripline - The stripline settings model. + * @param {AxisModel[]} axisCollection - Collection of all axes in the chart. + * @param {Chart} chart - The chart instance. + * @returns {{ from: number, to: number }} An object containing the calculated 'from' and 'to' values. + * @private + */ +function getFromToValue(start: number, end: number, size: number, + startFromAxis: boolean, axis: AxisModel, stripline: ChartStripLineProps, + axisCollection: AxisModel[], chart: Chart): { from: number, to: number } { + let visibleRange: VisibleRangeProps = axis.visibleRange; + if (axisCollection?.length) { + const validAxis: AxisModel = axisCollection.find((currentAxis: AxisModel) => currentAxis.name === axis.name) as AxisModel; + if (validAxis?.visibleRange) { + visibleRange = validAxis.visibleRange; + } + } + let from: number = (!stripline.repeat?.enable && startFromAxis) ? visibleRange.minimum : (start as number); + if (axis.valueType === 'Double' && size !== undefined && !startFromAxis && stripline.range?.start === undefined) { + from += (size || 0); + } + let to: number = getToValue( + Math.max(start || 0, isNullOrUndefined(end) ? (start || 0) : (end || 0)), + from, size || 0, axis, end || null, stripline, chart + ); + from = findValue(from, axis, axisCollection); + to = findValue(to, axis, axisCollection); + return { + from: striplineValueToCoefficient(axis.isAxisInverse ? to : from, axis, axisCollection), + to: striplineValueToCoefficient(axis.isAxisInverse ? from : to, axis, axisCollection) + }; +} + +/** + * Calculates the 'to' value for a stripline based on provided parameters. + * + * @param {number} to - The initial 'to' value. + * @param {number} from - The 'from' value. + * @param {number} size - The size of the stripline. + * @param {AxisModel} axis - The axis model associated with the stripline. + * @param {number | null} end - The end value, if specified. + * @param {ChartStripLineProps} stripline - The stripline settings model. + * @param {Chart} chart - The chart instance. + * @returns {number} The calculated 'to' value for the stripline. + * @private + */ +function getToValue(to: number, from: number, size: number, axis: AxisModel, end: number | null, + stripline: ChartStripLineProps, chart: Chart): number { + let sizeType: StripLineSizeUnit = stripline.range?.sizeType as StripLineSizeUnit; + const isEnd: boolean = (end === null); + let validAxis: AxisModel = axis; + if (chart.axisCollection?.length) { + const foundAxis: AxisModel = chart.axisCollection.find((currentAxis: AxisModel) => currentAxis.name === axis.name) as AxisModel; + if (foundAxis) { + validAxis = foundAxis; + } + } + if (axis.valueType === 'DateTime') { + const fromValue: Date = new Date(from); + if (sizeType === 'Auto') { + sizeType = validAxis.actualIntervalType; + size *= validAxis.visibleRange.interval; + } + switch (sizeType) { + case 'Years': + return isEnd ? +new Date(fromValue.setFullYear(fromValue.getFullYear() + size)) : to; + case 'Months': + return isEnd ? +new Date(fromValue.setMonth(fromValue.getMonth() + size)) : to; + case 'Days': + return isEnd ? +new Date(fromValue.setDate(fromValue.getDate() + size)) : to; + case 'Hours': + return isEnd ? +new Date(fromValue.setHours(fromValue.getHours() + size)) : to; + case 'Minutes': + return isEnd ? +new Date(fromValue.setMinutes(fromValue.getMinutes() + size)) : to; + case 'Seconds': + return isEnd ? +new Date(fromValue.setSeconds(fromValue.getSeconds() + size)) : to; + default: + return from; + } + } else { + return stripline.range?.sizeType === 'Pixel' ? from : (isEnd ? (from + size) : to); + } +} + +/** + * Constrains a value to be within the visible range of an axis. + * + * @param {number} value - The value to constrain. + * @param {AxisModel} axis - The axis model containing the visible range. + * @param {AxisModel[]} axisCollection - Collection of all axes in the chart. + * @returns {number} The value constrained to be within the axis visible range. + * @private + */ +function findValue(value: number, axis: AxisModel, axisCollection: AxisModel[]): number { + let targetAxis: AxisModel = axis; + if (axisCollection?.length) { + const foundAxis: AxisModel = axisCollection.find((currentAxis: AxisModel) => currentAxis.name === axis.name) as AxisModel; + if (foundAxis && foundAxis.visibleRange && + (foundAxis.visibleRange.minimum !== 0 || foundAxis.visibleRange.maximum !== 0)) { + targetAxis = foundAxis; + } + } + if (value < targetAxis.visibleRange.minimum) { + value = targetAxis.visibleRange.minimum; + } else if (value > targetAxis.visibleRange.maximum) { + value = targetAxis.visibleRange.maximum; + } + return value; +} + +/** + * Renders the text element for a stripline. + * + * @param {ChartStripLineProps} stripline - The settings model for the stripline. + * @param {Rect} rect - The rectangular area for the stripline. + * @param {string} id - The unique identifier for the stripline. + * @param {Chart} chart - The chart instance. + * @param {AxisModel} axis - The axis model associated with the stripline. + * @param {'visible' | 'hidden'} visibility - The visibility state of the text element. + * @returns {React.ReactNode} The rendered text element for the stripline. + * @private + */ +function renderText(stripline: ChartStripLineProps, rect: Rect, id: string, chart: Chart, axis: AxisModel, visibility: 'visible' | 'hidden' = 'visible'): React.ReactNode { + const textSize: ChartSizeProps = measureText(stripline.text as string, stripline.text?.font as TextStyleModel, + chart.themeStyle.stripLineLabelFont); + let validAxis: AxisModel = axis; + if (chart.axisCollection?.length) { + const foundAxis: AxisModel = chart.axisCollection.find((currentAxis: AxisModel) => currentAxis.name === axis.name) as AxisModel; + if (foundAxis) { + validAxis = foundAxis; + } + } + const isRotationNull: boolean = (stripline.text?.rotation === undefined); + const textMid: number = isRotationNull ? 3 * (textSize.height / 8) : 0; + let ty: number = rect.y + (rect.height / 2) + textMid; + const rotation: number | undefined = isRotationNull ? ((validAxis.orientation === 'Vertical') ? 0 : -90) : stripline.text?.rotation; + let tx: number = rect.x + (rect.width / 2); + let anchor: VerticalAlignment | HorizontalAlignment = 'Center'; + const hAlign: HorizontalAlignment = stripline.text?.hAlign as HorizontalAlignment; + const vAlign: VerticalAlignment = stripline.text?.vAlign as VerticalAlignment; + const padding: number = 5; + if (validAxis.orientation === 'Horizontal') { + tx = getTextStart( + tx + (textMid * getAlignmentFactor(hAlign)), + rect.width, hAlign + ); + ty = getTextStart(ty - textMid, rect.height, vAlign) + + (vAlign === 'Top' && !isRotationNull ? (textSize.height / 4) : 0); + anchor = isRotationNull ? invertAlignment(vAlign) : hAlign; + anchor = tx - textSize.width / 2 < validAxis.rect.x ? 'Left' : tx + textSize.width / 2 > validAxis.rect.width ? 'Right' : anchor; + } else { + tx = getTextStart(tx, rect.width, hAlign); + ty = getTextStart( + ty + (textMid * getAlignmentFactor(vAlign)) - padding, + rect.height, vAlign + ); + anchor = hAlign; + anchor = chart.enableRtl ? (anchor === 'Right' ? 'Left' : anchor === 'Left' ? 'Right' : anchor) : anchor; + } + return ( + + {stripline.text?.content} + + ); +} + +/** + * Calculates the starting coordinate for stripline text based on its alignment. + * + * @param {HorizontalAlignment | VerticalAlignment} alignment - Maps a horizontal or vertical alignment to its corresponding `text-anchor` value. + * @returns {'Start' | 'Middle' | 'End'} The corresponding value for the `text-anchor` attribute. + * @private + */ +function mapToTextAnchor(alignment: HorizontalAlignment | VerticalAlignment): 'Start' | 'Middle' | 'End' { + if (alignment === 'Left' || alignment === 'Top') { + return 'Start'; + } else if (alignment === 'Right' || alignment === 'Bottom') { + return 'End'; + } else { + return 'Middle'; + } +} + +/** + * Calculates the starting coordinate for stripline text based on its alignment. + * + * @param {number} xy - The initial x or y coordinate of the stripline. + * @param {number} size - The width or height of the text, used to calculate the offset. + * @param {HorizontalAlignment | VerticalAlignment} textAlignment - The desired alignment ('Left', 'Top', 'Right', 'Bottom', or 'Center'). + * @returns {number} The adjusted coordinate for the text's starting position. + * @private + */ +function getTextStart(xy: number, size: number, textAlignment: HorizontalAlignment | VerticalAlignment): number { + const padding: number = 5; + switch (textAlignment) { + case 'Left': + case 'Top': + xy = xy - (size / 2) + padding; + break; + case 'Right': + case 'Bottom': + xy = xy + (size / 2) - padding; + break; + } + return xy; +} + +/** + * Converts a text alignment into a numeric factor for positioning calculations. + * + * @param {HorizontalAlignment | VerticalAlignment} anchor - The alignment anchor ('Top', 'Bottom', 'Left', 'Right', or 'Center'). + * @returns {number} Returns `1` for 'Left'/'Top', `-1` for 'Right'/'Bottom', and `0` for 'Center'. + * @private + */ +function getAlignmentFactor(anchor: HorizontalAlignment | VerticalAlignment): number { + let factor: number = 0; + switch (anchor) { + case 'Left': + case 'Top': + factor = 1; + break; + case 'Right': + case 'Bottom': + factor = -1; + break; + } + return factor; +} + +/** + * Translates a VerticalAlignment into its corresponding HorizontalAlignment. + * + * @param {VerticalAlignment} anchor - The vertical alignment to invert ('Top', 'Bottom', or 'Center'). + * @returns {HorizontalAlignment} The inverted horizontal alignment ('Right', 'Left', or 'Center'). + * @private + */ +function invertAlignment(anchor: VerticalAlignment): HorizontalAlignment { + switch (anchor) { + case 'Top': + return 'Right'; + case 'Bottom': + return 'Left'; + default: + return 'Center'; + } +} + +/** + * Calculates the start value for a stripline based on axis type. + * + * @param {AxisModel} axis - The axis model. + * @param {ChartStripLineProps} stripLine - The stripline settings. + * @param {number} startValue - The initial start value. + * @param {Chart} chart - The chart instance. + * @returns {number} The calculated start value, adjusted for datetime or with repeatEvery added. + * @private + */ +function getStartValue(axis: AxisModel, stripLine: ChartStripLineProps, startValue: number, chart: Chart): number { + if (axis.valueType === 'DateTime') { + return (getToValue( + 0, startValue, + stripLine.repeat?.every ? +stripLine.repeat.every : 0, + axis, null, stripLine, chart + )); + } else { + return startValue + (stripLine.repeat?.every ? +stripLine.repeat.every : 0); + } +} + +/** + * Converts a value to a coefficient relative to a specified axis. + * + * @param {number} value - The numerical value to convert to a coefficient. + * @param {AxisModel} axis - The axis model containing the visible range information. + * @param {AxisModel[]} axisCollection - Optional collection of all axes in the chart. + * @returns {number} The coefficient representing the position of the value relative to the axis range. + * @private + */ +function striplineValueToCoefficient(value: number, axis: AxisModel, axisCollection: AxisModel[]): number { + let range: VisibleRangeProps = axis.visibleRange; + if (axisCollection?.length) { + const validAxis: AxisModel = axisCollection.find((currentAxis: AxisModel) => currentAxis.name === axis.name) as AxisModel; + if (validAxis?.visibleRange && + (validAxis.visibleRange.minimum !== 0 || validAxis.visibleRange.maximum !== 0)) { + range = validAxis.visibleRange; + } + } + const result: number = (value - range.minimum) / (range.delta); + const isInverse: boolean = axis.isAxisInverse as boolean; + return isInverse ? (1 - result) : result; +} diff --git a/components/charts/src/chart/renderer/ChartAreaRender.tsx b/components/charts/src/chart/renderer/ChartAreaRender.tsx new file mode 100644 index 0000000..34aeb73 --- /dev/null +++ b/components/charts/src/chart/renderer/ChartAreaRender.tsx @@ -0,0 +1,96 @@ +// ChartAreaRenderer.tsx +import { useLayout } from '../layout/LayoutContext'; +import { ChartAreaProps } from '../base/interfaces'; +import { useEffect, useLayoutEffect, useState } from 'react'; +import { useRegisterClipRectSetter } from '../hooks/useClipRect'; +import { Chart, Rect } from '../chart-area/chart-interfaces'; + +/** + * `ChartAreaRenderer` is a functional component responsible for rendering the main plotting area of the chart. + * It manages the clip rectangle used to restrict drawing within the chart bounds and integrates with + * the layout system for measurement and positioning. + * + * @param {ChartAreaProps} props - The properties required for rendering the chart area, + * including background, border, and layout-related settings. + * @returns {React.JSX.Element | null} The rendered chart area or null if not applicable. + */ +export const ChartAreaRenderer: React.FC = (props: ChartAreaProps) => { + const { layoutRef, phase, setLayoutValue, reportMeasured, triggerRemeasure } = useLayout(); + const [clipRect, setClip] = useState(null); + const registerClipRect: (fn: (clipRect: Rect) => void) => void = useRegisterClipRectSetter(); + + /** + * Sets the clipping rectangle for the chart area. + * This rectangle defines the boundaries where chart content can be drawn. + * + * @param {Rect} rect - The rectangle coordinates and dimensions + * @returns {void} - cha + */ + const setClipRect: (rect: Rect) => void = + (rect: Rect): void => { + setClip(rect); + }; + + useEffect(() => { + registerClipRect(setClipRect); + }, [registerClipRect]); + + useLayoutEffect(() => { + if (phase === 'measuring') { + const chart: Chart = layoutRef.current.chart as Chart; + const borderWidth: number = props.border?.width as number; + chart.clipRect.x = chart.clipRect.x + (props.margin?.left || 0); + chart.clipRect.width = chart.clipRect.width - (props.margin?.left || 0) - (props.margin?.right || 0); + chart.clipRect.y = chart.clipRect.y + borderWidth / 2 + (props.margin?.top || 0); + chart.clipRect.height = chart.clipRect.height + borderWidth / 2 - (props.margin?.top || 0) - (props.margin?.bottom || 0); + chart.chartAreaRect = { + x: chart.clipRect.x, + y: chart.clipRect.y, + width: chart.clipRect.width, + height: chart.clipRect.height + }; + setLayoutValue('chartArea', { + clipRect: { ...chart.clipRect, ...clipRect } + }); + setClip(chart.clipRect); + } + reportMeasured('ChartArea'); + + }, [phase, layoutRef]); + + useEffect(() => { + if (phase !== 'measuring') { + triggerRemeasure(); + } + }, [props.width, props.border?.width]); + + const chart: Chart = layoutRef.current.chart as Chart; + return phase === 'rendering' && ( + <> + {props.backgroundImage && + } + + + ); +}; diff --git a/components/charts/src/chart/renderer/ChartRenderer.tsx b/components/charts/src/chart/renderer/ChartRenderer.tsx new file mode 100644 index 0000000..8bb4876 --- /dev/null +++ b/components/charts/src/chart/renderer/ChartRenderer.tsx @@ -0,0 +1,286 @@ +// ChartRenderer.tsx +import { useLayout } from '../layout/LayoutContext'; +import { ChartBorderProps, ChartAreaProps, ChartComponentProps, ChartTooltipProps, ChartZoomSettingsProps, Column, ChartMarginProps, Row } from '../base/interfaces'; +import { JSX, useContext, useEffect, useLayoutEffect } from 'react'; +import { calculateVisibleAxis } from './AxesRenderer/AxisRender'; +import { extend, useProviderContext } from '@syncfusion/react-base'; +import { getSeriesColor, getThemeColor } from '../utils/theme'; +import { processChartSeries } from './SeriesRenderer/ProcessData'; +import { ChartContext } from '../layout/ChartProvider'; +import { defaultChartConfigs } from '../base/default-properties'; +import { Theme } from '../base/enum'; +import { AxisModel, Chart, ColumnProps, ElementWithSize, RowProps, SeriesProperties, ChartSizeProps } from '../chart-area/chart-interfaces'; +import { markerShapes } from './SeriesRenderer/MarkerRenderer'; + +/** + * ChartRenderer - Core functional component responsible for rendering the complete chart layout and structure. + * + * `ChartRenderer` is the root functional component responsible for rendering the entire chart layout. + * It handles the layout measurements, sizing, and rendering logic for all chart elements. + * + * @param {ChartComponentProps} props - The properties used to configure and render the chart, + * including series, axes, title, legend, and other chart-specific options. + * + * @returns {JSX.Element} The complete chart element with its layout and visual components. + */ +export const ChartRenderer: React.FC = (props: ChartComponentProps) => { + const { layoutRef, availableSize, reportMeasured, phase, setLayoutValue, + triggerRemeasure, disableAnimation, setDisableAnimation } = useLayout(); + const { parentElement, rows, columns, chartArea, chartSeries, axisCollection, chartZoom, chartTooltip } = useContext(ChartContext); + const { locale, dir } = useProviderContext(); + useLayoutEffect(() => { + if (phase === 'measuring') { + const border: ChartBorderProps = { ...defaultChartConfigs.chart.border, ...props.border }; + const margin: Required = { ...defaultChartConfigs.chart.margin, ...props.margin }; + const borderWidth: number = border.width as number; + const rectWidth: number = availableSize.width - borderWidth; + const rectHeight: number = availableSize.height - borderWidth; + const theme: Theme = props.theme || 'Material3'; + let visibleSeries: SeriesProperties[] = calculateVisibleSeries(chartSeries as SeriesProperties[], props, theme); + const requireInvertedAxis: boolean = calculateAreaType(visibleSeries, props); + const visibleAxisCollection: AxisModel[] = calculateVisibleAxis( + requireInvertedAxis, axisCollection, visibleSeries); + + visibleSeries = processChartSeries(visibleSeries); + + const chartRows: RowProps[] = rows.map((row: Row) => { + const newRow: RowProps = extend({}, row) as RowProps; + newRow.axes = []; + return newRow; + }); + + const chartColumns: ColumnProps[] = columns.map((c: Column) => { + const newColumn: ColumnProps = extend({}, c) as ColumnProps; + newColumn.axes = []; + return newColumn; + }); + + const chartConfiguration: Partial = createChartLayoutConfig( + props, parentElement, margin, disableAnimation as boolean, border as Required, availableSize, + dir, borderWidth, rectWidth, rectHeight, locale, theme, requireInvertedAxis, axisCollection, + triggerRemeasure, visibleAxisCollection, chartRows, chartColumns, visibleSeries, chartArea, chartZoom, chartTooltip); + setLayoutValue('chart', chartConfiguration); + setDisableAnimation?.(false); + reportMeasured('Chart'); + } + }, [phase, layoutRef]); + + useEffect(() => { + if (phase !== 'measuring') { + triggerRemeasure(); + } + }, [props.border?.width, props.theme, props.margin?.left, props.margin?.right, props.margin?.top, props.margin?.bottom, locale, dir]); + + return phase === 'rendering' && ( + <> + {renderChartBorder(layoutRef.current.chart as Chart, props)} + {renderBackground(layoutRef.current.chart as Chart, props)} + + ); +}; + +/** + * Renders the border rectangle element for the chart container. + * + * Creates an SVG rect element that serves as the visual border around the entire chart. + * The border styling is determined by the chart's border configuration and theme settings. + * + * @param {Chart} chart - The complete chart configuration object containing layout and styling information. + * @param {ChartComponentProps} chartProps - The properties of the chart component that may affect how the border is rendered. + * @returns {JSX.Element} A JSX rectangle element representing the chart border. + * @private + */ +function renderChartBorder(chart: Chart, chartProps: ChartComponentProps): JSX.Element { + return ( + + ); +} + +/** + * Conditionally renders a background image element for the chart if specified. + * + * Creates an SVG image element to display a background image behind all chart elements. + * The function performs a null check and only renders when a background image is configured. + * + * @param {Chart} chart - The chart instance containing specifications for rendering. + * @param {ChartComponentProps} chartProps - Additional properties for rendering the chart. + * @returns {JSX.Element|null} A JSX element representing the chart's background or null if no background image is set. + * @private + */ +function renderBackground(chart: Chart, chartProps: ChartComponentProps): JSX.Element | null { + if (!chart.backgroundImage) { + return null; + } + return ( + + ); +} + +/** + * Determines if an inverted axis is required based on the series collection and chart properties. + * + * Analyzes the primary series type and chart transposition setting to determine + * if the chart requires inverted axes. This is essential for proper rendering of bar charts + * and transposed chart layouts. + * + * @param {SeriesProperties[]} seriesCollection - An array of series used in the chart to evaluate the axis requirement. + * @param {ChartComponentProps} chart - Properties of the chart used to determine the need for an inverted axis. + * @returns {boolean} A boolean indicating whether an inverted axis is necessary for the chart's area type. + * @private + */ +export function calculateAreaType(seriesCollection: SeriesProperties[], chart: ChartComponentProps): boolean { + const series: SeriesProperties = seriesCollection[0]; + const isBarType: boolean = series.type?.includes('Bar') as boolean; + const isTransposed: boolean = chart.transposed as boolean; + return (isBarType && !isTransposed) || (!isBarType && isTransposed); +} + +/** + * Calculates and returns the visible series based on the provided chart series and theme. + * + * Transforms raw chart series data into a processed collection of visible series + * with applied themes, colors, markers, and sorting. Handles color palette assignment, + * marker shape configuration, and z-order sorting for proper rendering layering. + * + * @param {SeriesProperties} chartSeries - The series of the chart that needs to be evaluated for visibility. + * @param {ChartComponentProps} chart - The chart component props. + * @param {Theme} theme - The theme applied to the chart. + * @returns {SeriesProperties[]} An array of series that are deemed visible according to the given criteria. + * @private + */ +export function calculateVisibleSeries(chartSeries: SeriesProperties[], chart: ChartComponentProps, theme: Theme): SeriesProperties[] { + let series: SeriesProperties; + const visibleSeries: SeriesProperties[] = []; + const palettes: string[] = getSeriesColor(theme); + const colors: string[] = chart.palettes?.length ? chart.palettes : palettes; + const count: number = colors.length; + const seriesCollection: SeriesProperties[] = ([] as SeriesProperties[]). + concat(chartSeries).sort((a: SeriesProperties, b: SeriesProperties) => { + const zOrderA: number = a.zOrder as number; // Default to 0 if a.zOrder is null or undefined + const zOrderB: number = b.zOrder as number; // Default to 0 if b.zOrder is null or undefined + return zOrderA - zOrderB; + }); + for (let i: number = 0; i < seriesCollection.length; i++) { + series = seriesCollection[i as number]; + if (series.marker && series.marker.visible && !series.marker.shape) { + series.marker.shape = markerShapes[i % (markerShapes.length - 1)]; + } + series.category = 'Series'; + series.index = i; + series.interior = series.fill || colors[i % count]; + series.chartProps = chart; + visibleSeries.push(series); + seriesCollection[i as number] = series; + } + return visibleSeries; +} + +/** + * Creates a partial chart configuration object based on layout and rendering parameters. + * This function is used to compute the layout and visual settings for rendering a chart. + * + * Constructs a complete Chart configuration object by combining layout measurements, + * styling properties, behavioral flags, and component references. This configuration serves as the + * single source of truth for chart rendering and interaction handling. + * + * @param {ChartComponentProps} props - Properties of the chart component. + * @param {ElementWithSize} parentElement - The parent DOM element with size information. + * @param {Required} margin - Margin settings around the chart area. + * @param {boolean} disableAnimation - Flag to disable chart animations. + * @param {Required} border - Border settings for the chart container. + * @param {ChartSizeProps} availableSize - The available size for rendering the chart. + * @param {string} dir - Text direction (e.g., 'ltr' or 'rtl'). + * @param {number} borderWidth - Width of the chart border. + * @param {number} rectWidth - Width of the chart's bounding rectangle. + * @param {number} rectHeight - Height of the chart's bounding rectangle. + * @param {string} locale - Locale identifier for formatting and localization. + * @param {Theme} theme - Theme settings for chart appearance. + * @param {boolean} requireInvertedAxis - Flag indicating if axes should be inverted. + * @param {AxisModel[]} axisCollection - Collection of all axis models. + * @param {Function} triggerRemeasure - Callback to trigger layout re-measurement. + * @param {AxisModel[]} visibleAxisCollection - Collection of currently visible axes. + * @param {RowProps[]} chartRows - Configuration for chart rows. + * @param {ColumnProps[]} chartColumns - Configuration for chart columns. + * @param {SeriesProperties[]} visibleSeries - Collection of visible series to be rendered. + * @param {ChartAreaProps} chartArea - Properties defining the chart area. + * @param {ChartZoomSettingsProps} chartZoom - Zoom settings for the chart. + * @param {ChartTooltipProps} chartTooltip - Tooltip configuration for the chart. + * @returns {Partial} A partial chart configuration object used for rendering. + * @private + */ +function createChartLayoutConfig( + props: ChartComponentProps, parentElement: ElementWithSize, + margin: Required, disableAnimation: boolean, + border: Required, availableSize: ChartSizeProps, dir: string, + borderWidth: number, rectWidth: number, rectHeight: number, locale: string, theme: Theme, + requireInvertedAxis: boolean, axisCollection: AxisModel[], triggerRemeasure: () => void, visibleAxisCollection: AxisModel[], + chartRows: RowProps[], chartColumns: ColumnProps[], visibleSeries: SeriesProperties[], chartArea: ChartAreaProps, + chartZoom: ChartZoomSettingsProps, chartTooltip: ChartTooltipProps): Partial { + const chartConfig: Partial = { + element: parentElement.element, + margin: margin, + animateSeries: !disableAnimation, + border: border, + availableSize: availableSize, + enableRtl: dir === 'rtl' ? true : false, + rect: { x: borderWidth / 2, y: borderWidth / 2, width: rectWidth, height: rectHeight }, + background: props.background || 'transparent', + backgroundImage: props.backgroundImage, + locale: locale, + palettes: props.palettes || [], + theme: theme, + themeStyle: getThemeColor(theme), + requireInvertedAxis: requireInvertedAxis, + axes: axisCollection.slice(2), + currentLegendIndex: 0, + currentSeriesIndex: 0, + currentPointIndex: 0, + previousTargetId: '', + chartProps: props, + triggerRemeasure: triggerRemeasure, + clipRect: { + x: margin.left + borderWidth, + y: margin.top + borderWidth, + height: availableSize.height - (margin.top + borderWidth + borderWidth + margin.bottom), + width: availableSize.width - (margin.left + borderWidth + margin.right + borderWidth) + }, + iSTransPosed: props.transposed, + axisCollection: visibleAxisCollection, + rows: chartRows, + columns: chartColumns, + visibleSeries: visibleSeries, + horizontalAxes: [], + verticalAxes: [], + enableAnimation: props.enableAnimation ?? true, + chartArea: chartArea, + paneLineOptions: [], + zoomSettings: chartZoom, + clickCount: 0, + delayRedraw: true, + isGestureZooming: false, + enableSideBySidePlacement: props.enableSideBySidePlacement, + startPanning: false, + tooltipModule: chartTooltip, + dataLabelCollections: [] + }; + return chartConfig; +} + diff --git a/components/charts/src/chart/renderer/ChartStackLabelsRenderer.tsx b/components/charts/src/chart/renderer/ChartStackLabelsRenderer.tsx new file mode 100644 index 0000000..cdb71ad --- /dev/null +++ b/components/charts/src/chart/renderer/ChartStackLabelsRenderer.tsx @@ -0,0 +1,439 @@ +import { useLayoutEffect, JSX, useEffect, useState } from 'react'; +import { ChartStackLabelsProps, ChartBorderProps, ChartLocationProps } from '../base/interfaces'; +import { useLayout } from '../layout/LayoutContext'; +import { measureText } from '../utils/helper'; +import { colorNameToHex, convertHexToColor, DataLabelRendererResults } from './SeriesRenderer/DataLabelRender'; +import { useSeriesRenderVersion } from '../hooks/useClipRect'; +import { Chart, ColorValue, Points, Rect, SeriesProperties, ChartSizeProps, TextOption, TextStyleModel } from '../chart-area/chart-interfaces'; + +/** + * Configuration interface for shape rectangle styling and positioning. + */ +interface ShapeRectConfig { + /** Unique identifier for the shape element */ + id: string; + /** Fill color for the shape background */ + fill: string; + /** Border configuration including color and width */ + border: ChartBorderProps; + /** Opacity level from 0 (transparent) to 1 (opaque) */ + opacity: number; + /** Rectangle dimensions and position */ + rect: Rect; + /** Horizontal corner radius */ + rx: number; + /** Vertical corner radius */ + ry: number; + /** CSS transform string for positioning and rotation */ + transform: string; + /** Stroke color for the border, if applicable */ + stroke: string | undefined; +} + +/** + * ChartStackLabelsRenderer is a functional component that renders stack labels for stacked chart series. + * This component displays cumulative values at the top of stacked data points, making it easier to + * understand the total contribution of all series at each data point. It integrates with the chart + * layout system to provide proper positioning, styling, and animation support. + * + * The component handles: + * - Calculating cumulative values for positive and negative stacked series + * - Positioning labels to avoid overlap with chart elements + * - Applying custom formatting to label text + * - Supporting animations and theme-based styling + * - Managing visibility based on series grouping + * + * @param {ChartStackLabelsProps} props - Configuration properties for stack labels including visibility, styling, formatting, and positioning options + * @returns {JSX.Element | null} The rendered stack labels SVG elements or null if labels are not visible or applicable + */ +export const ChartStackLabelsRenderer: React.FC = (props: ChartStackLabelsProps): JSX.Element | null => { + const { layoutRef, reportMeasured, phase, animationProgress } = useLayout(); + + const [labelOpacity, setLabelOpacity] = useState(0); + + // Measure phase - register the component in the layout system + useLayoutEffect(() => { + if (phase === 'measuring') { + if (layoutRef.current?.chart && props?.visible) { + const calculatedLabels: DataLabelRendererResults[] = renderStackLabels(layoutRef.current.chart as Chart, props); + (layoutRef.current.chart as Chart).stackLabelsOptions = calculatedLabels; + } + reportMeasured('ChartStackLabels'); + } + }, [phase, layoutRef]); + + const legendClickedInfo: { version: number; id: string } = useSeriesRenderVersion(); + // Update stack labels when dependencies change + useEffect(() => { + if (phase !== 'measuring' && ((legendClickedInfo && legendClickedInfo.id === (layoutRef.current.chart as Chart)?.element.id))) { + if (layoutRef.current?.chart && props?.visible) { + requestAnimationFrame(() => { + const calculatedLabels: DataLabelRendererResults[] = renderStackLabels(layoutRef.current.chart as Chart, props); + (layoutRef.current.chart as Chart).stackLabelsOptions = calculatedLabels; + }); + } + reportMeasured('ChartStackLabels'); + } + }, [props?.rotationAngle, props.border?.width, + props.font?.fontSize, props.font?.fontWeight, props.font?.fontFamily, props?.format, props?.margin, legendClickedInfo.version]); + + useEffect(() => { + let frameId: number | undefined; + + if (props?.visible) { + const visibleSeries: SeriesProperties = (layoutRef.current.chart as Chart)?.visibleSeries?.find( + (series: SeriesProperties) => series.visible + ) as SeriesProperties; + + const durations: number = visibleSeries?.animation?.duration ?? 400; + + if (animationProgress === 1) { + let start: number | null = null; + + const animate: (timestamp: number) => void = (timestamp: number) => { + if (!start) { + start = timestamp; + } + const elapsed: number = timestamp - start; + const duration: number = durations; // ms + const eased: number = Math.min(elapsed / duration, 1); + + setLabelOpacity(eased); + + if (eased < 1) { + frameId = requestAnimationFrame(animate); + } + }; + + frameId = requestAnimationFrame(animate); + } else if (!visibleSeries?.animation?.enable) { + setLabelOpacity(1); + } else { + setLabelOpacity(0); + } + } + + return () => { + if (frameId !== undefined) { + cancelAnimationFrame(frameId); + } + }; + }, [animationProgress, props?.visible]); + + + // Render the stack labels + return (phase === 'rendering') && (props.visible && (layoutRef?.current?.chart as Chart)?.stackLabelsOptions) ? ( + + {(layoutRef.current.chart as Chart).stackLabelsOptions.map((labelData: DataLabelRendererResults, labelIdx: number) => ( + + {labelData.shapeRect && ( + + )} + + {labelData.textOption.text} + + + ))} + + ) : null; +}; + +/** + * Renders stack labels for all stacked series in the chart by calculating cumulative values + * and determining appropriate positioning for each label. + * + * This function processes all visible series in the chart, groups them by stackingGroup, + * and calculates the total values for both positive and negative stacks. It then + * generates the necessary rendering information for each stack label. + * + * @param {Chart} chart - The chart instance containing series data, axes, and rendering context + * @param {ChartStackLabelsProps} props - Configuration properties for stack labels including styling and formatting options + * @returns {DataLabelRendererResults[]} Array of stack label rendering information containing position, styling, and text data + * @private + */ +function renderStackLabels(chart: Chart, props: ChartStackLabelsProps): DataLabelRendererResults[] { + const stackLabels: DataLabelRendererResults[] = []; + let positivePoints: Record = {}; + let negativePoints: Record = {}; + const groupingValues: Record = {}; + let keys: string[] = []; + + if (chart.visibleSeries && chart.visibleSeries.length > 0) { + // Group series by stackingGroup + for (let i: number = 0; i < chart.visibleSeries.length; i++) { + const series: SeriesProperties = chart.visibleSeries[i as number]; + const stackGroup: string = series.stackingGroup || ''; + + if (!groupingValues[stackGroup as string]) { + groupingValues[stackGroup as string] = []; + } + groupingValues[stackGroup as string].push(series); + } + + keys = Object.keys(groupingValues); + + if (keys.length > 0) { + for (let groupIndex: number = 0; groupIndex < keys.length; groupIndex++) { + positivePoints = {}; + negativePoints = {}; + const seriesGroup: SeriesProperties[] = groupingValues[keys[groupIndex as number]]; + + if (!seriesGroup || seriesGroup.length === 0) { continue; } + + const lastSeriesIndex: number = seriesGroup[seriesGroup.length - 1].index; + + for (let seriesIndex: number = lastSeriesIndex; seriesIndex >= 0; seriesIndex--) { + const series: SeriesProperties | undefined = chart.visibleSeries[seriesIndex as number]; + + if (!series) { continue; } + + if (series.visible && series.points && series.points.length > 0) { + for (let pointIndex: number = 0; pointIndex < series.points.length; pointIndex++) { + const point: Points = series.points[pointIndex as number]; + const pointXValueAsKey: string = String(point.x); + if (!positivePoints[pointXValueAsKey as string] && + (Number(series.stackedValues?.endValues?.[pointIndex as number])) > 0 && + point.visible) { + positivePoints[pointXValueAsKey as string] = point; + } + if (!negativePoints[pointXValueAsKey as string] && + (Number(series.stackedValues?.endValues?.[pointIndex as number])) < 0 && + point.visible) { + negativePoints[pointXValueAsKey as string] = point; + } + } + } + } + + const groupStackLabels: DataLabelRendererResults[] = calculateStackLabel( + positivePoints, + negativePoints, + chart, + props + ); + stackLabels.push(...groupStackLabels); + } + } + } + + return stackLabels; +} + +/** + * Calculates the position, styling, and content for individual stack labels based on + * positive and negative data points in stacked series. + * + * This function performs the core logic for stack label rendering including: + * - Computing cumulative values for each stack + * - Applying custom formatting to label text + * - Calculating optimal positioning within chart boundaries + * - Determining appropriate text and background colors based on contrast + * - Creating shape rectangles for label backgrounds + * - Handling rotation and alignment settings + * + * @param {Record} positivePoints - Collection of data points with positive values, keyed by x-axis value + * @param {Record} negativePoints - Collection of data points with negative values, keyed by x-axis value + * @param {Chart} chart - The chart instance providing context for positioning and styling calculations + * @param {ChartStackLabelsProps} props - Stack label configuration including formatting, styling, and positioning properties + * @returns {DataLabelRendererResults[]} Array of calculated stack label rendering information with position, styling, and content data + * @private + */ +function calculateStackLabel( + positivePoints: Record, + negativePoints: Record, + chart: Chart, + props: ChartStackLabelsProps +): DataLabelRendererResults[] { + const stackLabels: DataLabelRendererResults[] = []; + let stackLabelIndex: number = 0; + const chartBackground: string | undefined = chart.chartArea?.background === 'transparent' ? + chart.background || chart.themeStyle?.background : chart.chartArea?.background; + + [positivePoints, negativePoints].forEach((points: Record, index: number) => { + if (points && Object.keys(points).length > 0) { + Object.keys(points).forEach((pointXValueAsKey: string) => { + let totalValue: number = 0; + let currentPoint: Points | undefined; + + const currentSeries: SeriesProperties | undefined = + points[pointXValueAsKey as string]?.series as SeriesProperties | undefined; + const pointIndex: number | undefined = points[pointXValueAsKey as string]?.index; + + if (!currentSeries?.stackedValues?.endValues || pointIndex === undefined) { + return; + } + + const positiveValue: number = Number(currentSeries.stackedValues.endValues[pointIndex as number]); + const negativeValue: number = negativePoints[pointXValueAsKey as string] && + negativePoints[pointXValueAsKey as string].series && + (negativePoints[pointXValueAsKey as string].series as SeriesProperties).stackedValues?.endValues ? Number( + (negativePoints[pointXValueAsKey as string].series as SeriesProperties).stackedValues?.endValues?.[ + negativePoints[pointXValueAsKey as string].index + ]) : 0; + + if (index === 0) { + // Handle positive points + totalValue = positiveValue + negativeValue; + currentPoint = points[pointXValueAsKey as string]; + } else if (!positivePoints[pointXValueAsKey as string]) { + // Handle negative points only if no corresponding positive point + totalValue = positiveValue; + currentPoint = points[pointXValueAsKey as string]; + } else { + // Skip if we already processed this point in positive case + return; + } + + if (currentPoint?.symbolLocations?.[0]) { + const series: SeriesProperties = currentPoint.series as SeriesProperties; + const symbolLocation: ChartLocationProps = currentPoint.symbolLocations[0]; + + const labelFormat: string | undefined | null = props?.format; + let formattedRawValue: string = totalValue.toString(); + if (!(labelFormat && labelFormat.indexOf('n') > -1)) { + formattedRawValue = (totalValue % 1 === 0) + ? totalValue.toFixed(0) + : (totalValue.toFixed(2).slice(-1) === '0' + ? totalValue.toFixed(1) + : totalValue.toFixed(2)); + } + const stackLabelText: string = labelFormat && labelFormat.match('{value}') !== null + ? labelFormat.replace('{value}', series.yAxis.format(parseFloat(formattedRawValue))) + : series.yAxis.format(parseFloat(formattedRawValue)); + + // Measure text dimensions + const textSize: ChartSizeProps = measureText( + stackLabelText, + props.font as TextStyleModel, + chart.themeStyle.datalabelFont + ); + + // Define padding for spacing + const padding: number = 10; + + // Determine background color + const backgroundColor: string | undefined = props?.fill === 'transparent' && chartBackground === 'transparent' + ? ((series.chart.theme.indexOf('Dark') > -1 || series.chart.theme.indexOf('HighContrast') > -1) ? 'black' : 'white') + : props?.fill !== 'transparent' + ? props?.fill + : chartBackground; + + // Calculate contrast for text color + const rgbValue: ColorValue = convertHexToColor(colorNameToHex(String(backgroundColor))); + const contrast: number = Math.round((rgbValue.r * 299 + rgbValue.g * 587 + rgbValue.b * 114) / 1000); + + const alignmentValue: number = textSize.width + + (props.border?.width ?? 0) + + (Number(props.margin?.left)) + + (Number(props.margin?.right)) - padding / 2; + + // Calculate position offsets + const yOffset: number = chart.requireInvertedAxis ? padding / 2 : + (chart.axisCollection[0].inverted ? (index === 0 ? (textSize.height + padding / 2) : -padding) + : (index === 0 ? -padding : (textSize.height + padding / 2))); + + let xOffset: number = chart.requireInvertedAxis ? + ((chart.axisCollection[0]?.inverted ? (index === 0 ? -(padding + textSize.width / 2) : + (padding + textSize.width / 2)) : (index === 0 ? (padding + textSize.width / 2) : + -(padding + textSize.width / 2)))) : 0; + + xOffset += props.align === 'Right' ? alignmentValue : + (props.align === 'Left' ? -alignmentValue : 0); + + if (!series.clipRect) { + return; + } + + // Calculate final position constrained within clip rect + const clip: Rect = series.clipRect; + let xPosition: number = Math.max(clip.x + textSize.width, Math.min(xOffset + clip.x + symbolLocation.x + , clip.x + clip.width - textSize.width)); + + let yPosition: number = Math.max( + clip.y + textSize.height, + Math.min( + yOffset + clip.y + symbolLocation.y - + ((Number(props.rotationAngle) > 0 && !chart.requireInvertedAxis) ? textSize.width / 2 : 0), + clip.y + clip.height - textSize.height + ) + ); + const isBorder: boolean = props.border?.color !== '' && props.border?.color !== 'Transparent' && Number(props.border?.width) > 0; + xPosition = chart.requireInvertedAxis && isBorder ? xPosition + (Number(props.border?.width) * 2) : xPosition; + yPosition = !chart.requireInvertedAxis && isBorder ? yPosition - (Number(props.border?.width) * 2) : yPosition; + // Create rectangle for label background + const rect: Rect = { + x: xPosition - textSize.width / 2 - (Number(props.margin?.left)), + y: yPosition - textSize.height - (Number(props.margin?.top)), + width: textSize.width + ((Number(props.margin?.left)) + (Number(props.margin?.right))), + height: textSize.height + padding / 2 + ((Number(props.margin?.top)) + + (Number(props.margin?.bottom))) + }; + + // Create shape rect config + const shapeRect: ShapeRectConfig = { + id: `${chart.element.id}_StackLabel_TextShape_${stackLabelIndex}`, + fill: String(props.fill), + border: props.border!, + opacity: 1, + rect: rect, + rx: Number(props.borderRadius?.x), + ry: Number(props.borderRadius?.y), + transform: `rotate(${props.rotationAngle}, ${rect.width / 2 + rect.x}, ${rect.height / 2 + rect.y})`, + stroke: undefined + }; + + // Determine text color based on contrast + const textColor: string = props?.font?.color || (contrast >= 128 ? 'black' : 'white'); + + // Create text option + const textOption: TextOption = { + id: `${chart.element.id}_StackLabel_${stackLabelIndex}`, + x: xPosition, + y: yPosition, + anchor: 'middle', + text: stackLabelText, + transform: `rotate(${props.rotationAngle}, ${rect.width / 2 + rect.x}, ${rect.height / 2 + rect.y})`, + labelRotation: props.rotationAngle, + fontFamily: props.font?.fontFamily, + fontSize: props.font?.fontSize, + fontStyle: props.font?.fontStyle, + fontWeight: props.font?.fontWeight, + fill: textColor + }; + + // Add to stack labels collection + stackLabels.push({ shapeRect, textOption }); + stackLabelIndex++; + } + }); + } + }); + + return stackLabels; +} + +export default ChartStackLabelsRenderer; diff --git a/components/charts/src/chart/renderer/ChartSubtitleRender.tsx b/components/charts/src/chart/renderer/ChartSubtitleRender.tsx new file mode 100644 index 0000000..96482b5 --- /dev/null +++ b/components/charts/src/chart/renderer/ChartSubtitleRender.tsx @@ -0,0 +1,269 @@ +// ChartSubTitleRenderer.tsx +import { ForwardedRef, forwardRef, JSX, useEffect, useLayoutEffect } from 'react'; +import { useLayout } from '../layout/LayoutContext'; +import { ChartTitleProps } from '../base/interfaces'; +import { getMaxRotatedTextSize, getTextAnchor, getTitle, measureText, titlePositionX } from '../utils/helper'; +import { TextOverflow, TitlePosition } from '../base/enum'; +import { Chart, MarginModel, Rect, ChartSizeProps, TextStyleModel, TitleOptions } from '../chart-area/chart-interfaces'; +import { HorizontalAlignment } from '@syncfusion/react-base'; + +/** + * `ChartSubTitleRenderer` is a functional component used to render the subtitle of the chart. + * It participates in the layout system and reports its measurements during the measuring phase. + * + * @param {ChartTitleProps} props - Contains properties required for rendering the chart subtitle, + * including the title content and styling options. + * + * @returns {JSX.Element | null} The subtitle element or null if no subtitle is defined. + */ +export const ChartSubTitleRenderer: React.ForwardRefExoticComponent> = + forwardRef((props: ChartTitleProps, ref: React.ForwardedRef) => { + const { layoutRef, phase, reportMeasured, triggerRemeasure } = useLayout(); + useLayoutEffect(() => { + if (phase === 'measuring') { + if (props.text) { + const chart: Chart = layoutRef.current.chart as Chart; + const margin: MarginModel = chart.margin; + const rect: Rect = { + x: margin.left, y: margin.top, width: chart.availableSize.width - margin.left - margin.right, + height: chart.availableSize.height - margin.top - margin.bottom + }; + const titleStyle: TextStyleModel = { + color: props.color as string, + fontSize: props.fontSize as string, + fontStyle: props.fontStyle as string, + fontFamily: props.fontFamily as string, + fontWeight: props.fontWeight as string + }; + const titleCollection: string[] = getTitle( + props.text, titleStyle, ((props.position === 'Top' || props.position === 'Bottom') ? rect.width : rect.height), + chart.enableRtl, chart.themeStyle.chartSubTitleFont, (props.textOverflow as TextOverflow)); + const titleOptions: TitleOptions = computeSubtitlePosition(chart, props, titleStyle, titleCollection); + chart.subTitleSettings = titleOptions; + } + reportMeasured('ChartSubsTitle'); + } + }, [phase, layoutRef]); + useEffect(() => { + if (phase !== 'measuring') { + triggerRemeasure(); + } + }, [props?.fontSize, props?.position, props.x, + props?.y, props?.text, props?.align, props?.textOverflow]); + + return (phase === 'rendering' && props.text) && ( + <> + {subTitleBorder((layoutRef.current.chart as Chart), props)} + {renderSubTitleElement((layoutRef.current.chart as Chart), props, ref)} + + ); + }); + +/** + * Computes the position for the chart subtitle based on the chart dimensions and text style. + * + * @param {Chart} chart - The chart object containing dimensions and margin details. + * @param {ChartTitleProps} subtitleProps - The style settings for the subtitle text. + * @param {TextStyleModel} textStyle - The text style model for the subtitle. + * @param {string[]} subTitle - An array of strings representing the subtitle text lines. + * @returns {TitleOptions} The calculated position and options for rendering the subtitle. + * @private + */ +function computeSubtitlePosition(chart: Chart, subtitleProps: ChartTitleProps, textStyle: TextStyleModel, + subTitle: string[]): TitleOptions { + const margin: MarginModel = chart.margin; + let subTitleSize: ChartSizeProps; + let maxWidth: number = 0; + let maxHeight: number = 0; + subTitle.forEach((text: string) => { + const size: ChartSizeProps = measureText(text, textStyle, chart.themeStyle.chartSubTitleFont); + maxWidth = Math.max(maxWidth, size.width); + maxHeight = Math.max(maxHeight, size.height); + }); + subTitleSize = { height: maxHeight, width: maxWidth }; + const rect: Rect = { + x: margin.left, y: margin.top, width: chart.availableSize.width - margin.left - margin.right, + height: chart.availableSize.height - margin.top - margin.bottom + }; + let positionX: number = titlePositionX(rect, subtitleProps.align as HorizontalAlignment) + (subtitleProps.border?.width as number); + let positionY: number = 0; + let rotation: string = ''; + const subtitleBorderWidth: number = subtitleProps.border?.width || 0; + const textAnchor: string = getTextAnchor( + subtitleProps.align as HorizontalAlignment, chart.enableRtl, subtitleProps.position as TitlePosition); + const padding: number = 5; + const chartAreaPadding: number = 10; + const titleoptions: TitleOptions = chart.titleSettings; + const ascentOffset: number = subTitleSize.height * 3 / 4; + let titleBorderX: number = 0; + let titleBorderY: number = 0; + let titleBorderWidth: number = 0; + let titleBorderHeight: number = 0; + let totalSubtitleHeight: number = 0; + const elementSpacing: number = 5; + switch (subtitleProps.position) { + case 'Top': + if (titleoptions && titleoptions.position === 'Top') { + positionY += (titleoptions.y * titleoptions.title.length) + titleoptions.borderWidth + + (subTitleSize.height) + (subtitleBorderWidth * 0.5) + padding; + chart.clipRect.y += (subTitleSize.height * subTitle.length) + subtitleBorderWidth + chartAreaPadding; + chart.clipRect.height -= (subTitleSize.height * subTitle.length) + subtitleBorderWidth + chartAreaPadding; + } else { + positionY += (subTitleSize.height * 3 / 4) + subtitleBorderWidth + margin.top; + chart.clipRect.y += (subTitleSize.height * subTitle.length) + subtitleBorderWidth + chartAreaPadding; + chart.clipRect.height -= (subTitleSize.height * subTitle.length) + subtitleBorderWidth + chartAreaPadding; + } + titleBorderX = positionX - (textAnchor === 'middle' ? (subTitleSize.width / 2) + elementSpacing : textAnchor === 'end' ? subTitleSize.width + elementSpacing : elementSpacing); + titleBorderY = positionY - subTitleSize.height + (subTitleSize.height / 4); + titleBorderWidth = subTitleSize.width + elementSpacing * 2; + titleBorderHeight = subTitle.length * subTitleSize.height; + break; + case 'Bottom': + totalSubtitleHeight = subTitleSize.height * subTitle.length; + positionY = chart.availableSize.height - totalSubtitleHeight + ascentOffset - margin.bottom; + chart.clipRect.height -= totalSubtitleHeight + subtitleBorderWidth + chartAreaPadding; + titleBorderX = positionX - (textAnchor === 'middle' ? (subTitleSize.width / 2) + elementSpacing : textAnchor === 'end' ? subTitleSize.width + elementSpacing : elementSpacing); + titleBorderY = positionY - subTitleSize.height + (subTitleSize.height / 4); + titleBorderWidth = subTitleSize.width + elementSpacing * 2; + titleBorderHeight = subTitle.length * subTitleSize.height; + break; + case 'Left': + subTitleSize = getMaxRotatedTextSize(subTitle, -90, textStyle, chart.themeStyle.chartSubTitleFont); + if (titleoptions && titleoptions.position === 'Left') { + positionX = margin.left + (titleoptions.titleSize.width * titleoptions.title.length) + subTitleSize.width + + (titleoptions.borderWidth) + padding; + chart.clipRect.x += (subTitleSize.width * subTitle.length) + subtitleBorderWidth + chartAreaPadding; + chart.clipRect.width -= (subTitleSize.width * subTitle.length) + subtitleBorderWidth + chartAreaPadding; + } else { + positionX = margin.left + (subTitleSize.width * 3 / 4) + (subtitleBorderWidth * 0.5); + chart.clipRect.x += (subTitleSize.width * subTitle.length) + subtitleBorderWidth + chartAreaPadding; + chart.clipRect.width -= (subTitleSize.width * subTitle.length) + subtitleBorderWidth + chartAreaPadding; + } + positionY = subtitleProps.align === 'Left' ? margin.bottom + (subtitleBorderWidth * 0.5) + chart.border.width : + subtitleProps.align === 'Right' ? chart.availableSize.height - margin.bottom - (subtitleBorderWidth * 0.5) - chart.border.width : chart.availableSize.height / 2; + rotation = `rotate(-90, ${positionX}, ${positionY})`; + titleBorderX = positionX - (textAnchor === 'middle' ? (subTitleSize.height / 2) + elementSpacing : textAnchor === 'end' ? subTitleSize.height + elementSpacing : elementSpacing); + titleBorderY = positionY - subTitleSize.width + (subTitleSize.width / 4); + titleBorderWidth = subTitleSize.height + elementSpacing * 2; + titleBorderHeight = subTitle.length * subTitleSize.width; + break; + case 'Right': + subTitleSize = getMaxRotatedTextSize(subTitle, 90, textStyle, chart.themeStyle.chartSubTitleFont); + if (titleoptions && titleoptions.position === 'Right') { + positionY = subtitleProps.align === 'Left' ? margin.bottom + (subtitleBorderWidth * 0.5) + chart.border.width : + subtitleProps.align === 'Right' ? chart.availableSize.height - margin.bottom - (subtitleBorderWidth * 0.5) - chart.border.width : chart.availableSize.height / 2; + positionX = chart.availableSize.width - (margin.right + (titleoptions.titleSize.width * titleoptions.title.length) + + subTitleSize.width + (titleoptions.borderWidth) + padding); + chart.clipRect.x -= subtitleBorderWidth - margin.left; + chart.clipRect.width -= (subTitleSize.width * subTitle.length) + margin.left + subtitleBorderWidth + chartAreaPadding; + } else { + positionX = chart.availableSize.width - (margin.right + (subTitleSize.width * 3 / 4) + (subtitleBorderWidth) * 0.5); + chart.clipRect.x -= (subtitleBorderWidth) + chartAreaPadding - margin.left; + chart.clipRect.width -= (subTitleSize.width * subTitle.length) + margin.left + subtitleBorderWidth + chartAreaPadding; + positionY += chart.availableSize.height / 2; + } + positionY = subtitleProps.align === 'Left' ? margin.bottom + (subtitleBorderWidth * 0.5) + chart.border.width : + subtitleProps.align === 'Right' ? chart.availableSize.height - margin.bottom - (subtitleBorderWidth * 0.5) - chart.border.width : chart.availableSize.height / 2; + rotation = `rotate(90, ${positionX}, ${positionY})`; + titleBorderX = positionX - (textAnchor === 'middle' ? (subTitleSize.height / 2) + elementSpacing : textAnchor === 'end' ? subTitleSize.height + elementSpacing : elementSpacing); + titleBorderY = positionY - subTitleSize.width + (subTitleSize.width / 4); + titleBorderWidth = subTitleSize.height + elementSpacing * 2; + titleBorderHeight = subTitle.length * subTitleSize.width; + break; + case 'Custom': + positionX = subtitleProps.x as number; + positionY = subtitleProps.y as number; + titleBorderX = positionX - (textAnchor === 'middle' ? (subTitleSize.width / 2) + elementSpacing : textAnchor === 'end' ? subTitleSize.width + elementSpacing : elementSpacing); + titleBorderY = positionY - subTitleSize.height + (subTitleSize.height / 4); + titleBorderWidth = subTitleSize.width + elementSpacing * 2; + titleBorderHeight = subTitle.length * subTitleSize.height; + break; + } + return { + x: positionX, y: positionY, + rotation: rotation, + textAnchor: textAnchor, + textStyle: textStyle, + borderWidth: subtitleBorderWidth, + title: subTitle, + titleSize: subTitleSize, + position: subtitleProps.position as TitlePosition, + titleBorder: { + x: titleBorderX, + y: titleBorderY, + width: titleBorderWidth, + height: titleBorderHeight + } + }; +} + +/** + * Renders the SVG rectangle element for the subtitle border using the specified options and properties. + * + * @param {Chart} chart - The chart instance to which the subtitle belongs. + * @param {ChartTitleProps} titleProps - Properties that provide details about the chart subtitle. + * @returns {JSX.Element} A rectangle SVG element representing the border around the subtitle. + * @private + */ +function subTitleBorder(chart: Chart, titleProps: ChartTitleProps): JSX.Element { + const titleOptions: TitleOptions = chart.subTitleSettings; + return (titleOptions) && ( + + ); +} + +/** + * Renders the SVG text element for the chart subtitle based on provided options and properties. + * + * @param {Chart} chart - The chart instance to which the subtitle belongs. + * @param {ChartTitleProps} titleProps - Properties that define the subtitle characteristics in the chart. + * @param {ForwardedRef} ref - A reference to the SVG text element. + * @returns {JSX.Element} A text SVG element used to display the chart subtitle. + * @private + */ +function renderSubTitleElement(chart: Chart, titleProps: ChartTitleProps, ref: ForwardedRef): JSX.Element { + const subTitleOptions: TitleOptions = chart.subTitleSettings; + return (subTitleOptions) && ( + + {subTitleOptions.title.length > 1 + ? subTitleOptions.title.map((line: string, index: number) => ( + + {line} + + )) + : subTitleOptions.title + } + + ); +} diff --git a/components/charts/src/chart/renderer/ChartTitleRenderer.tsx b/components/charts/src/chart/renderer/ChartTitleRenderer.tsx new file mode 100644 index 0000000..c722513 --- /dev/null +++ b/components/charts/src/chart/renderer/ChartTitleRenderer.tsx @@ -0,0 +1,261 @@ +import { ForwardedRef, forwardRef, JSX, useContext, useEffect, useLayoutEffect } from 'react'; +import { useLayout } from '../layout/LayoutContext'; +import { ChartTitleProps, ChartMarginProps } from '../base/interfaces'; +import { getMaxRotatedTextSize, getMaxTextSize, getTextAnchor, getTitle, measureText, titlePositionX } from '../utils/helper'; +import { ChartContext } from '../layout/ChartProvider'; +import { TextOverflow, TitlePosition } from '../base/enum'; +import { Chart, MarginModel, Rect, ChartSizeProps, TextStyleModel, TitleOptions } from '../chart-area/chart-interfaces'; +import { HorizontalAlignment } from '@syncfusion/react-base'; + +/** + * `ChartTitleRenderer` is a functional component that renders the main title of the chart. + * It integrates with the chart layout system to measure and position the title appropriately. + * + * @param {ChartTitleProps} props - Properties required to render the chart title, + * including the title content, style, and alignment settings. + * + * @returns {JSX.Element | null} The rendered chart title element or null if not applicable. + */ +export const ChartTitleRenderer: React.ForwardRefExoticComponent> = + forwardRef((props: ChartTitleProps, ref: React.ForwardedRef) => { + const { layoutRef, reportMeasured, phase, triggerRemeasure } = useLayout(); + const { chartSubTitle, chartProps } = useContext(ChartContext); + useLayoutEffect(() => { + if (phase === 'measuring') { + if (props.text) { + const chart: Chart = layoutRef.current.chart as Chart; + const margin: Required = chart.margin; + const rect: Rect = { + x: margin.left, y: margin.top, width: chart.availableSize.width - margin.left - margin.right, + height: chart.availableSize.height - margin.top - margin.bottom + }; + if (!chartProps.accessibility?.ariaLabel) { + chart.element.setAttribute('aria-label', props.text + '. Syncfusion interactive chart.'); + } + const titleStyle: TextStyleModel = { + color: props.color as string, + fontSize: props.fontSize as string, + fontStyle: props.fontStyle as string, + fontFamily: props.fontFamily as string, + fontWeight: props.fontWeight as string + }; + const titleCollection: string[] = getTitle(props.text, titleStyle, ((props.position === 'Top' || + props.position === 'Bottom') ? rect.width : rect.height), chart.enableRtl, chart.themeStyle.chartTitleFont, props.textOverflow as TextOverflow); + const alignment: HorizontalAlignment = props.align as HorizontalAlignment; + const aligmentAnchor: string = getTextAnchor(alignment, chart.enableRtl, props.position as TitlePosition); + const titleOptions: TitleOptions = calculateTitlePosition( + props, titleStyle, chart, titleCollection, aligmentAnchor, chartSubTitle); + chart.titleSettings = titleOptions; + } + reportMeasured('ChartTitle'); + } + }, [phase, layoutRef]); + + useEffect(() => { + if (phase !== 'measuring') { + triggerRemeasure(); + } + }, [props?.fontSize, props?.position, props.x, + props?.y, props?.text, props?.align, props?.textOverflow]); + + return (phase === 'rendering' && props.text) && ( + <> + {titleBorder((layoutRef.current.chart as Chart), props)} + {titleElement((layoutRef.current.chart as Chart), props, ref)} + + ); + }); + +/** + * Calculates the position of the chart title based on various settings such as text style and alignment. + * + * @param {ChartTitleProps} titleProps - The style settings for the title text. + * @param {TextStyleModel} textStyle - The style settings for the title text. + * @param {Chart} chart - The chart object containing layout and data details. + * @param {string[]} title - An array representing the title text lines. + * @param {string} alignment - The alignment setting determining how the title is positioned. + * @param {ChartTitleProps} subTitle - Properties related to the subtitle of the chart. + * @returns {TitleOptions} The calculated position and options for rendering the title. + * @private + */ +function calculateTitlePosition( + titleProps: ChartTitleProps, textStyle: TextStyleModel, chart: Chart, title: string[], alignment: string, + subTitle: ChartTitleProps): TitleOptions { + const margin: MarginModel = chart.margin; + let titleSize: ChartSizeProps = getMaxTextSize(title, textStyle, chart.themeStyle.chartTitleFont); + let y: number = margin.top + ((titleSize.height) * 3 / 4); // ascent - to align with vertical center + const rect: Rect = { + x: margin.left, y: margin.top, width: chart.availableSize.width - margin.left - margin.right, + height: chart.availableSize.height - margin.top - margin.bottom + }; + const textAlignment: HorizontalAlignment = titleProps.align as HorizontalAlignment; + let x: number = titlePositionX(rect, textAlignment) + (titleProps.border?.width as number); + let rotation: string = ''; + const borderWidth: number = (titleProps?.border?.width as number); + const padding: number = subTitle && subTitle.text ? 10 : 15; + const ascentOffset: number = titleSize.height * 3 / 4; + let titleBorderX: number = 0; + let titleBorderY: number = 0; + let titleBorderWidth: number = 0; + let titleBorderHeight: number = 0; + const elementSpacing: number = 5; + let totalTitleHeight: number = 0; + switch (titleProps.position) { + case 'Top': + titleSize = getMaxTextSize(title, textStyle, chart.themeStyle.chartTitleFont); + y = margin.top + ((titleSize.height) * 3 / 4) + (borderWidth * 0.5); + chart.clipRect.y += (titleSize.height * title.length) + borderWidth + padding; + chart.clipRect.height -= (titleSize.height * title.length) + borderWidth + padding; + titleBorderX = x - (alignment === 'middle' ? (titleSize.width / 2) + elementSpacing : alignment === 'end' ? titleSize.width + elementSpacing : elementSpacing); + titleBorderY = y - titleSize.height + (titleSize.height / 4); + titleBorderWidth = titleSize.width + elementSpacing * 2; + titleBorderHeight = title.length * titleSize.height; + break; + case 'Bottom': + totalTitleHeight = titleSize.height * title.length; + y = chart.availableSize.height - margin.bottom - totalTitleHeight + ascentOffset - (borderWidth * 0.5); + if (subTitle && subTitle.text && subTitle.position === 'Bottom') { + const rect: Rect = { + x: margin.left, y: margin.top, width: chart.availableSize.width - margin.left - margin.right, + height: chart.availableSize.height - margin.top - margin.bottom + }; + const subtitleStyle: TextStyleModel = { + color: subTitle.color as string, + fontSize: subTitle.fontSize as string, + fontStyle: subTitle.fontStyle as string, + fontFamily: subTitle.fontFamily as string, + fontWeight: subTitle.fontWeight as string + }; + const subtitleCollection: string[] = getTitle(subTitle.text, subtitleStyle, (rect.width), + false, chart.themeStyle.chartSubTitleFont, subTitle.textOverflow as TextOverflow); + const subTitleSize: ChartSizeProps = measureText(subtitleCollection[0], textStyle, chart.themeStyle.chartSubTitleFont); + y -= (subTitleSize.height * subtitleCollection.length) + (subTitle.border?.width as number) + padding / 2; + } + chart.clipRect.height -= (titleSize.height * title.length) + borderWidth + padding; + titleBorderX = x - (alignment === 'middle' ? (titleSize.width / 2) + elementSpacing : alignment === 'end' ? titleSize.width + elementSpacing : elementSpacing); + titleBorderY = y - titleSize.height + (titleSize.height / 4); + titleBorderWidth = titleSize.width + elementSpacing * 2; + titleBorderHeight = title.length * titleSize.height; + break; + case 'Left': + titleSize = getMaxRotatedTextSize(title, -90, textStyle, chart.themeStyle.chartTitleFont); + x = margin.left + (titleSize.width * 3 / 4) + borderWidth; + y = textAlignment === 'Left' ? margin.bottom + (borderWidth * 0.5) + chart.border.width : + textAlignment === 'Right' ? chart.availableSize.height - margin.bottom - (borderWidth * 0.5) - chart.border.width : chart.availableSize.height / 2; + rotation = `rotate(-90, ${x}, ${y})`; + chart.clipRect.x += (titleSize.width * title.length) + borderWidth + padding; + chart.clipRect.width -= (titleSize.width * title.length) + borderWidth + padding; + titleBorderX = x - (alignment === 'middle' ? (titleSize.height / 2) + elementSpacing : alignment === 'end' ? titleSize.height + elementSpacing : elementSpacing); + titleBorderY = y - titleSize.width + (titleSize.width / 4); + titleBorderWidth = titleSize.height + elementSpacing * 2; + titleBorderHeight = title.length * titleSize.width; + break; + case 'Right': + titleSize = getMaxRotatedTextSize(title, 90, textStyle, chart.themeStyle.chartTitleFont); + x = chart.availableSize.width - margin.right - (titleSize.width * 3 / 4) - borderWidth * 0.5; + y = textAlignment === 'Left' ? margin.bottom + (borderWidth * 0.5) + chart.border.width : + textAlignment === 'Right' ? chart.availableSize.height - margin.bottom - (borderWidth * 0.5) - chart.border.width : chart.availableSize.height / 2; + rotation = `rotate(90, ${x}, ${y})`; + chart.clipRect.x -= borderWidth + padding; + chart.clipRect.width -= (titleSize.width * title.length) + borderWidth + padding; + titleBorderX = x - (alignment === 'middle' ? (titleSize.height / 2) + elementSpacing : alignment === 'end' ? titleSize.height + elementSpacing : elementSpacing); + titleBorderY = y - titleSize.width + (titleSize.width / 4); + titleBorderWidth = titleSize.height + elementSpacing * 2; + titleBorderHeight = title.length * titleSize.width; + break; + case 'Custom': + x = titleProps.x as number; + y = titleProps.y as number; + titleBorderX = x - (alignment === 'middle' ? (titleSize.width / 2) + elementSpacing : alignment === 'end' ? titleSize.width + elementSpacing : elementSpacing); + titleBorderY = y - titleSize.height + (titleSize.height / 4); + titleBorderWidth = titleSize.width + elementSpacing * 2; + titleBorderHeight = title.length * titleSize.height; + break; + } + return { + x: x, y: y, + rotation: rotation, + textStyle: textStyle, + title: title, + titleSize: titleSize, + textAnchor: alignment, + borderWidth: borderWidth, + position: titleProps.position as TitlePosition, + titleBorder: { + x: titleBorderX, + y: titleBorderY, + width: titleBorderWidth, + height: titleBorderHeight + } + }; +} + +/** + * Renders the border for the chart title based on the provided title options and properties. + * + * @param {Chart} chart - The chart instance to which the title belongs. + * @param {ChartTitleProps} titleProps - The properties containing details of the chart title. + * @returns {JSX.Element} A rectangle SVG element representing the border of the chart title. + * @private + */ +function titleBorder(chart: Chart, titleProps: ChartTitleProps): JSX.Element { + const titleOptions: TitleOptions = chart.titleSettings; + return (titleOptions) && ( + ); +} + +/** + * Renders the text element for the chart title using the given options and properties. + * + * @param {Chart} chart - The chart instance to which the title belongs. + * @param {ChartTitleProps} titleProps - The properties that define the title's attributes in the chart. + * @param {ForwardedRef} ref - The reference to the SVG text element. + * @returns {JSX.Element} A text SVG element used to display the chart title. + * @private + */ +function titleElement(chart: Chart, titleProps: ChartTitleProps, ref: ForwardedRef): JSX.Element { + const titleOptions: TitleOptions = chart.titleSettings; + return (titleOptions) && ( + + {titleOptions.title.length > 1 + ? titleOptions.title.map((line: string, index: number) => ( + + {line} + + )) + : titleOptions.title + } + + ); +} diff --git a/components/charts/src/chart/renderer/LegendRenderer/ChartLegendRenderer.tsx b/components/charts/src/chart/renderer/LegendRenderer/ChartLegendRenderer.tsx new file mode 100644 index 0000000..b502bc0 --- /dev/null +++ b/components/charts/src/chart/renderer/LegendRenderer/ChartLegendRenderer.tsx @@ -0,0 +1,795 @@ +import { forwardRef, useEffect, useLayoutEffect, useRef, useMemo, useReducer } from 'react'; +import { useLayout } from '../../layout/LayoutContext'; +import { ChartLegendProps, ChartFontProps, ChartLocationProps, ChartAccessibilityProps } from '../../base/interfaces'; +import { BaseLegend, LegendOptions, RectOption } from '../../base/Legend-base'; +import { + getLegendOptions, + calculateLegendBound, + renderLegend, + renderSymbol, + calculateLegendTitle, + changePage, + LegendClick +} from './CommonLegend'; +import { IThemeStyle } from '../../utils/theme'; +import { getTextAnchor } from '../../utils/helper'; +import { registerChartEventHandler, useLegendShapeRenderVersion } from '../../hooks/useClipRect'; +import { Chart, PathOptions, Rect, SeriesProperties, TextOption } from '../../chart-area/chart-interfaces'; +import { LegendShape } from '../../base/enum'; +import { HorizontalAlignment } from '@syncfusion/react-base'; + +// Define reducer state and action types +type LegendState = { + pageText: string; + transformValue: string; + triggerUpdate: number; +}; + +const LEGEND_PAGE_UP: string = '_pageup'; +const LEGEND_PAGE_DOWN: string = '_pagedown'; + +type LegendAction = + | { type: 'SET_PAGE_TEXT'; payload: string } + | { type: 'SET_TRANSFORM'; payload: string } + | { type: 'TRIGGER_UPDATE' }; + +// Reducer for legend state management +const legendReducer: (state: LegendState, action: LegendAction) => LegendState = +(state: LegendState, action: LegendAction): LegendState => { + switch (action.type) { + case 'SET_PAGE_TEXT': + return { ...state, pageText: action.payload }; + case 'SET_TRANSFORM': + return { ...state, transformValue: action.payload }; + case 'TRIGGER_UPDATE': + return { ...state, triggerUpdate: state.triggerUpdate + 1 }; + default: + return state; + } +}; + +/** + * Component that renders the legend for a chart. + * This component is responsible for measuring and preparing legend data during the layout phase. + * + * @param {ChartLegendProps} props - The properties for configuring the chart legend. + * @returns {Element} The rendered legend component or null if not visible. + */ +export const ChartLegendRenderer: React.FC = (props: ChartLegendProps): React.ReactElement | null => { + const { layoutRef, reportMeasured, phase, setLayoutValue, triggerRemeasure } = useLayout(); + const [, dispatch] = useReducer(legendReducer, { + pageText: '', + transformValue: 'translate(0, 0)', + triggerUpdate: 0 + }); + + // Memoize dependencies to prevent unnecessary re-renders + const propsToWatch: { + visible: boolean; + width: string | number; + height: string | number; + location: object; + position: string; + padding: number; + isInversed: boolean; + reverse: boolean; + fixedWidth: boolean; + maxLabelWidth: number; + enablePages: boolean; + itemPadding: number; + align: string; + shapePadding: number; + margin: object; + containerPadding: object; + maxTitleWidth: number; + textStyle: ChartFontProps; + } = useMemo(() => ({ + visible: props.visible || false, + width: props.width || '', + height: props.height || '', + location: props.location || {}, + position: props.position || '', + padding: props.padding || 0, + isInversed: props.inversed || false, + reverse: props.reverse || false, + fixedWidth: props.fixedWidth || false, + maxLabelWidth: props.maxLabelWidth || 0, + enablePages: props.enablePages || false, + itemPadding: props.itemPadding || 0, + align: props.align || '', + shapePadding: props.shapePadding || 0, + margin: props.margin || {}, + containerPadding: props.containerPadding || {}, + maxTitleWidth: props.maxTitleWidth || 0, + textStyle: props.textStyle || {} + }), [ + props.visible, + props.width, + props.height, + props.location, + props.position, + props.padding, + props.inversed, + props.reverse, + props.fixedWidth, + props.maxLabelWidth, + props.enablePages, + props.itemPadding, + props.align, + props.shapePadding, + props.margin, + props.containerPadding, + props.maxTitleWidth, + props.textStyle + ]); + + // Handle shape property changes + const shapeProps: { + shapeHeight: number | undefined; + shapeWidth: number | undefined; + } = useMemo(() => ({ + shapeHeight: props.shapeHeight, + shapeWidth: props.shapeWidth + }), [props.shapeHeight, props.shapeWidth]); + + // Handle title property changes + const titleProps: { + title: string | undefined; + titleStyle: ChartFontProps | undefined; + } = useMemo(() => ({ + title: props.title, + titleStyle: props.titleStyle + }), [props.title, props.titleStyle]); + + useLayoutEffect(() => { + if (phase === 'measuring') { + const chart: Chart = layoutRef.current.chart as Chart; + const legend: BaseLegend = getLegendOptions( + props, + chart.visibleSeries, + chart + ); + + if (layoutRef.current?.chart && legend) { + legend.chart = layoutRef.current.chart as Chart; + } + + if (layoutRef.current?.chart && props.visible) { + setLayoutValue('chartLegend', legend); + calculateLegendBound( + props, + chart.clipRect, + chart.availableSize, + chart, + layoutRef.current.chartLegend as BaseLegend + ); + renderLegend( + props, + (layoutRef.current.chartLegend as BaseLegend).legendBounds as Rect, + chart, + layoutRef.current.chartLegend as BaseLegend + ); + } + reportMeasured('ChartLegend'); + } + }, [phase]); + + useEffect(() => { + if (phase !== 'measuring') { + triggerRemeasure(); + } + }, [propsToWatch, shapeProps]); + + useEffect(() => { + if (phase !== 'measuring' && layoutRef.current.chart && layoutRef.current.chartLegend) { + const chart: Chart = layoutRef.current.chart as Chart; + const legend: BaseLegend = layoutRef.current.chartLegend as BaseLegend; + const legendBounds: Rect = legend.legendBounds as Required; + calculateLegendBound( + props, + chart.clipRect, + chart.availableSize, + chart, + legend + ); + if (legend.legendBounds === legendBounds) { + calculateLegendTitle(props, legend.legendBounds, chart, legend); + dispatch({ type: 'TRIGGER_UPDATE' }); + } else { + triggerRemeasure(); + } + } + }, [titleProps]); + + if (!props.visible || phase === 'measuring' || !layoutRef.current.chartLegend || !layoutRef.current.chart) { + return null; + } + + return <>; +}; + +interface LegendTitleProps { + legend: BaseLegend; + chart: Chart; + props: ChartLegendProps; + chartTheme: IThemeStyle; +} + +/** + * Renders the title for the chart legend + * + * This component displays the legend title with proper styling and positioning. + * It handles multiple lines of text and proper text alignment based on the chart's + * RTL settings and legend position. + * + * @component + * @param {LegendTitleProps} props - Properties for the legend title + * @param {BaseLegend} props.legend - The legend configuration object + * @param {Chart} props.chart - The parent chart instance + * @param {ChartLegendProps} props.props - The original legend props from the chart + * @param {IThemeStyle} props.chartTheme - The theme styling for the chart + * @returns {React.ReactElement} The rendered legend title element + */ +const LegendTitle: React.FC = ({ + legend, + chart, + props, + chartTheme +}: LegendTitleProps): React.ReactElement => { + const textAnchor: string = useMemo(() => { + return legend.isVertical || legend.isTop ? + getTextAnchor( + legend.titleAlign as HorizontalAlignment, + chart.enableRtl, + 'Top' + ) : + (chart.enableRtl) ? 'end' : ''; + }, [legend, chart]); + + return ( + ).x} + y={(legend.legendTitleLoction as Required).y} + fill={props.titleStyle?.color || chartTheme.legendTitleFont.color} + fontSize={props.titleStyle?.fontSize || chartTheme.legendTitleFont.fontSize} + fontStyle={props.titleStyle?.fontStyle || chartTheme.legendTitleFont.fontStyle} + fontFamily={props.titleStyle?.fontFamily || chartTheme.legendTitleFont.fontFamily} + fontWeight={props.titleStyle?.fontWeight || chartTheme.legendTitleFont.fontWeight} + opacity={props.titleStyle?.opacity || props.opacity} + textAnchor={textAnchor} + > + {(legend.legendTitleCollections as Required).map((line: string, index: number) => ( + index === 0 ? ( + line + ) : ( + ).x} + dy="1.2em" + > + {line} + + ) + ))} + + ); +}; + +interface LegendPagingProps { + legend: BaseLegend; + pageText: string; + pageUpOption: PathOptions; + pageDownOption: PathOptions; + pageTextOption: TextOption; + chartTheme: IThemeStyle; +} + +/** + * Renders the pagination controls for the legend + * + * This component displays navigation arrows and page indicator when the legend + * has multiple pages of items. It handles the visual representation of + * the pagination controls and their active/inactive states. + * + * @component + * @param {LegendPagingProps} props - Properties for the legend pagination + * @param {BaseLegend} props.legend - The legend configuration object + * @param {string} props.pageText - The current page indicator text (e.g., "1/3") + * @param {PathOptions} props.pageUpOption - Configuration for the previous page arrow + * @param {PathOptions} props.pageDownOption - Configuration for the next page arrow + * @param {TextOption} props.pageTextOption - Configuration for the page indicator text + * @param {IThemeStyle} props.chartTheme - The theme styling for the chart + * @returns {React.ReactElement} The rendered pagination controls + */ +const LegendPaging: React.FC = ({ + legend, + pageText, + pageUpOption, + pageDownOption, + pageTextOption, + chartTheme +}: LegendPagingProps): React.ReactElement => { + const upRegion: Rect = legend.pagingRegions?.[0] as Rect; + const downRegion: Rect = legend.pagingRegions?.[1] as Rect; + + const parseTranslate: (t?: string) => { + x: number; + y: number; + } = (t?: string) => { + if (!t) { return { x: 0, y: 0 }; } + const start: number = t.indexOf('('); + const end: number = t.indexOf(')'); + if (start === -1 || end === -1) { return { x: 0, y: 0 }; } + const [xStr, yStr] = t.slice(start + 1, end).split(','); + return { + x: parseFloat(xStr) || 0, + y: parseFloat(yStr) || 0 + }; + }; + const { x: tx, y: ty } = parseTranslate(legend.transform as string); + + return ( + + {upRegion && ( + + )} + + + {pageText} + + {downRegion && ( + + )} + + + ); +}; + +interface LegendMarkerProps { + markerShape: PathOptions; + markerType: string; + opacity: number; +} + +/** + * Renders a marker for line-type legend items + * + * This component displays the marker symbol (e.g., circle, square) used + * for line series in the legend. It handles different marker shapes + * with appropriate SVG elements. + * + * @component + * @param {LegendMarkerProps} props - Properties for the legend marker + * @param {PathOptions} props.markerShape - The shape configuration for the marker + * @param {string} props.markerType - The type of marker to render (e.g., "Circle") + * @param {number} props.opacity - The opacity to apply to the marker + * @returns {React.ReactElement} The rendered marker element + */ +const LegendMarker: React.FC = ({ + markerShape, + markerType, + opacity +}: LegendMarkerProps): React.ReactElement => { + return markerType === 'Circle' ? ( + + ) : ( + + ); +}; + +interface LegendItemProps { + index: number; + legend: BaseLegend; + legendItem: LegendOptions; + props: ChartLegendProps; + chartTheme: IThemeStyle; +} + +/** + * Renders an individual legend item with shape and text + * + * This component displays a single legend entry consisting of a shape/symbol + * and corresponding text label. It handles accessibility attributes, + * styling, and optional markers for specific series types. + * + * @component + * @param {LegendItemProps} props - Properties for the legend item + * @param {number} props.index - The index of this item in the legend collection + * @param {BaseLegend} props.legend - The legend configuration object + * @param {LegendOptions} props.legendItem - The specific item's configuration + * @param {ChartLegendProps} props.props - The original legend props from the chart + * @param {IThemeStyle} props.chartTheme - The theme styling for the chart + * @returns {React.ReactElement} The rendered legend item + */ +const LegendItem: React.FC = ({ + index, + legend, + legendItem, + props, + chartTheme +}: LegendItemProps): React.ReactElement => { + const legendShape: Required = legendItem.symbolOption as Required; + const textOption: Required = legendItem.textOption as Required; + + return ( + ).role || 'button'} + aria-pressed={legendItem.visible ? 'true' : 'false'} + aria-label={(legend.accessibility as Required).ariaLabel || + `${legendItem.text} series is ${legendItem.visible ? 'showing, press enter to hide the ' : 'hidden, press enter to show the '} ${legendItem.text} series`} + style={{ + outline: 'none', + cursor: !legend.toggleVisibility ? 'auto' : 'pointer' + }} + tabIndex={(index === 0 && legend.accessibility && legend.accessibility.focusable) ? + legend.accessibility.tabIndex : -1} + > + {legendItem.shape === 'Circle' || legendItem.type === 'Bubble' || (legendItem.type === 'Scatter' && legendItem.markerShape === 'Circle') ? ( + + ) : ( + + )} + + {legendItem.type === 'Line' && + legendItem.markerVisibility && + legendItem.markerShape !== 'Image' && + legendItem.markerOption && ( + + )} + + } + textAnchor={textOption.anchor} + > + {(Array.isArray(textOption.text) ? textOption.text : [textOption.text]).map((line: string, index: number) => + index === 0 ? ( + line + ) : ( + + {line} + + ) + )} + + + ); +}; + +interface LegendItemsProps { + legend: BaseLegend; + props: ChartLegendProps; + chartTheme: IThemeStyle; +} + +/** + * Renders all legend items as a collection + * + * This component serves as a container for all the individual legend items, + * mapping through the legend collection and rendering each item component. + * + * @component + * @param {LegendItemsProps} props - Properties for the legend items collection + * @param {BaseLegend} props.legend - The legend configuration object + * @param {ChartLegendProps} props.props - The original legend props from the chart + * @param {IThemeStyle} props.chartTheme - The theme styling for the chart + * @returns {React.ReactElement | null} The rendered collection of legend items or null if no items exist + */ +const LegendItems: React.FC = ({ + legend, + props, + chartTheme +}: LegendItemsProps): React.ReactElement | null => { + if (!legend.legendCollections) { + return null; + } + + return ( + <> + {legend.legendCollections.map((chartLegend: LegendOptions, i: number) => ( + + ))} + + ); +}; + +/** + * Component that renders the custom legend for a chart with interactive elements. + * Handles pagination, visibility toggling, and styling of legend items. + * + * @param props - The properties for configuring the chart legend. + * @param ref - Reference to the SVG group element. + * @returns The rendered custom legend component or null if not visible. + */ +export const CustomLegendRenderer: React.ForwardRefExoticComponent> = +forwardRef((props: ChartLegendProps, ref: React.Ref): React.ReactElement | null => { + const { layoutRef, phase } = useLayout(); + const legendRef: React.RefObject = useRef(null); + const legendShapeVersion: { version: number; id: string } = useLegendShapeRenderVersion(); + + const [state, dispatch] = useReducer(legendReducer, { + pageText: '', + transformValue: 'translate(0, 0)', + triggerUpdate: 0 + }); + + // Update the transform value when it changes + const updateLegendTransform: (transform: string) => void = (transform: string): void => { + dispatch({ type: 'SET_TRANSFORM', payload: transform }); + }; + + // Update page text when it changes + const updatePageText: (newPageText: string) => void = (newPageText: string): void => { + dispatch({ type: 'SET_PAGE_TEXT', payload: newPageText }); + }; + + useEffect(() => { + if (legendRef.current) { + legendRef.current.setAttribute('transform', state.transformValue); + } + }, [state.transformValue]); + + useEffect(() => { + if (phase !== 'measuring' && layoutRef.current.chart && layoutRef.current.chartLegend && + legendShapeVersion.id === (layoutRef.current.chart as Chart).element.id) { + const chart: Chart = layoutRef.current.chart as Chart; + const legend: BaseLegend = layoutRef.current.chartLegend as BaseLegend; + + for (const series of chart.visibleSeries as SeriesProperties[]) { + if (series.name !== '') { + (legend.legendCollections as LegendOptions[])[series.index as number].shape = + series.legendShape as LegendShape; + } + } + + for (let i: number = 0; legend.legendCollections && i < legend.legendCollections.length; i++) { + renderSymbol(props, legend.legendCollections[i as number], i, chart, legend); + } + + dispatch({ type: 'TRIGGER_UPDATE' }); + } + }, [legendShapeVersion.version]); + + useLayoutEffect(() => { + if (phase === 'measuring') { + if (layoutRef.current?.chart && props.visible) { + const legend: BaseLegend = layoutRef.current.chartLegend as BaseLegend; + if (legend.isPaging) { + const initialPageText: string = legend.pageTextOption?.text as string; + dispatch({ type: 'SET_PAGE_TEXT', payload: initialPageText }); + } + } + } + }, [phase]); + + useEffect(() => { + const handleLegendClick: (e: Event) => void = (e: Event): void => { + if (!props.visible || !props.toggleVisibility || !layoutRef.current.chartLegend) { + return; + } + + const targetId: string = (e.target as HTMLElement).id; + if (!targetId || !targetId.includes('_chart_legend')) { + return; + } + + const chart: Chart = layoutRef.current.chart as Chart; + const currentLegend: BaseLegend = layoutRef.current.chartLegend as BaseLegend; + + const legendItemsId: string[] = [ + `${currentLegend.legendID}_text_`, + `${currentLegend.legendID}_shape_marker_`, + `${currentLegend.legendID}_shape_` + ]; + + for (const id of legendItemsId) { + if (targetId.indexOf(id) > -1) { + const seriesIndex: number = parseInt(targetId.split(id)[1], 10); + LegendClick(props, seriesIndex, chart, currentLegend); + dispatch({ type: 'TRIGGER_UPDATE' }); + break; + } + } + + if (targetId.indexOf(currentLegend.legendID + LEGEND_PAGE_UP) > -1) { + changePage(props, true, updateLegendTransform, updatePageText, currentLegend); + } else if (targetId.indexOf(currentLegend.legendID + LEGEND_PAGE_DOWN) > -1) { + changePage(props, false, updateLegendTransform, updatePageText, currentLegend); + } + }; + + const unregister: () => void = registerChartEventHandler( + 'click', + handleLegendClick, + (layoutRef.current?.chart as Chart)?.element.id + ); + + return unregister; + }, [props.toggleVisibility, props]); + + if (!props.visible || phase === 'measuring' || !layoutRef.current.chartLegend || !layoutRef.current.chart) { + return null; + } + + const legend: BaseLegend = layoutRef.current.chartLegend as BaseLegend; + const chart: Chart = layoutRef.current.chart as Chart; + const chartTheme: IThemeStyle = chart.themeStyle; + const { x, y, width, height } = legend.legendBounds as Required; + const clipRect: RectOption = legend.clipRect as Required; + const pageUpOption: PathOptions = legend.pageUpOption as Required; + const pageDownOption: PathOptions = legend.pageDownOption as Required; + const pageTextOption: TextOption = legend.pageTextOption as Required; + + return ( + <> + + + + + + + {(legend.legendTitleCollections?.length as Required) > 0 && ( + + )} + + + + + + + + {legend?.isPaging && (legend?.totalPages as Required) > 1 && ( + + )} + + + ); +}); + diff --git a/components/charts/src/chart/renderer/LegendRenderer/CommonLegend.tsx b/components/charts/src/chart/renderer/LegendRenderer/CommonLegend.tsx new file mode 100644 index 0000000..5d74a13 --- /dev/null +++ b/components/charts/src/chart/renderer/LegendRenderer/CommonLegend.tsx @@ -0,0 +1,1157 @@ +import { extend, HorizontalAlignment, VerticalAlignment } from '@syncfusion/react-base'; +import { ChartBorderProps, ChartLegendProps, ChartFontProps, LegendClickEvent, ChartLocationProps } from '../../base/interfaces'; +import { ChartSeriesType, LegendPosition, LegendShape, TextOverflow } from '../../base/enum'; +import { BaseLegend, createLegendOption, createPathOption, createRectOption, RectOption } from '../../base/Legend-base'; +import { LegendOptions } from '../../base/Legend-base'; +import { calculateLegendShapes, calculateShapes, getTitle, measureText, stringToNumber, titlePositionX, useTextTrim, useTextWrap } from '../../utils/helper'; +import { subtractThickness } from '../AxesRenderer/CartesianLayoutRender'; +import { AxisTextStyle } from '../../chart-axis/base'; +import { useRegisterAxisRender, useRegisterSeriesRender } from '../../hooks/useClipRect'; +import { AxisModel, Chart, PathOptions, Rect, SeriesProperties, ChartSizeProps, TextOption, TextStyleModel } from '../../chart-area/chart-interfaces'; + +// === LEGEND INITIALIZATION & CONFIGURATION === + +/** + * Retrieves the legend options based on the visible series collection and chart. + * + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {SeriesProperties[]} visibleSeriesCollection - The collection of visible series. + * @param {Chart} chart - The chart object. + * @returns {BaseLegend} An object containing the configuration for the legend, tailored to the provided series and chart settings. + * @private + */ +export function getLegendOptions(chartLegend: ChartLegendProps, visibleSeriesCollection: SeriesProperties[], chart: Chart): BaseLegend { + const legend: BaseLegend = extend({}, chartLegend) as BaseLegend; + + legend.maxItemHeight = 0; + legend.rowHeights = []; + legend.pageHeights = []; + legend.columnHeights = []; + legend.legendCollections = []; + legend.legendTitleCollections = []; + legend.itemPadding = 0; + legend.isRtlEnable = false; + legend.isReverse = false; + legend.isVertical = false; + legend.isPaging = false; + legend.clipPathHeight = 0; + legend.totalPages = 0; + legend.fivePixel = 5; + legend.rowCount = 0; + legend.pageButtonSize = 8; + legend.pageXCollections = []; + legend.maxColumns = 0; + legend.maxWidth = 0; + legend.legendID = chart.element.id + '_chart_legend'; + legend.currentPage = 1; + legend.backwardArrowOpacity = 0; + legend.forwardArrowOpacity = 1; + legend.accessbilityText = ''; + legend.arrowWidth = 26; // 2 * (5 + 8 + 5) + legend.arrowHeight = 26; + legend.chartRowCount = 1; + legend.legendTitleSize = { height: 0, width: 0 }; + legend.isTop = false; + legend.isTitle = false; + legend.clearTooltip = 0; + legend.currentPageNumber = 1; + legend.legendRegions = []; + legend.pagingRegions = []; + legend.totalNoOfPages = 0; + legend.legendTranslate = ''; + + let seriesType: ChartSeriesType; + let fill: string; + let dashArray: string; + legend.isRtlEnable = chart.enableRtl; + legend.isReverse = !legend.isRtlEnable && chartLegend.reverse; + for (const series of visibleSeriesCollection) { + if (series.name !== '') { + seriesType = series.type as Required; + dashArray = series.dashArray as Required; + fill = series.interior as Required; + legend.legendCollections.push(createLegendOption( + series.name as Required, fill, series.legendShape as Required, + series.visible as Required, seriesType, series.legendImageUrl ? series.legendImageUrl : '', + series.marker?.shape, + series.marker?.visible, undefined, undefined, dashArray + )); + } + } + if (legend.reverse) { + legend.legendCollections.reverse(); + } + return legend; +} + +/** + * Calculate the bounds for the legends. + * + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {Rect} rect - The rectangle defining the legend area. + * @param {ChartSizeProps} availableSize - The available size for rendering. + * @param {Chart} chart - The chart object. + * @param {BaseLegend} legend - The legend object. + * @param {Rect} [previousLegendBounds] - The previous legend bounds, if available. + * @param {boolean} [pointAnimation] - Indicates if point animation is enabled. + * @returns {void} This function does not return a value. + * @private + */ +export function calculateLegendBound( + chartLegend: ChartLegendProps, + rect: Rect, + availableSize: ChartSizeProps, + chart: Chart, + legend: BaseLegend, + previousLegendBounds?: Rect, + pointAnimation?: boolean +): void { + const defaultValue: string = '20%'; + legend.legendBounds = { x: rect.x, y: rect.y, width: 0, height: 0 }; + legend.isVertical = (legend.position === 'Left' || legend.position === 'Right'); + legend.itemPadding = chartLegend.itemPadding ? chartLegend.itemPadding : legend.isVertical ? 8 : 20; + if (legend.isVertical) { + legend.legendBounds.height = stringToNumber( + legend.height, availableSize.height - (rect.y - chart.margin.top)) || rect.height; + legend.legendBounds.width = stringToNumber(legend.width || defaultValue, availableSize.width); + } else { + legend.legendBounds.width = stringToNumber(legend.width, availableSize.width) || rect.width; + legend.legendBounds.height = stringToNumber(legend.height || defaultValue, availableSize.height); + } + getLegendBounds(availableSize, legend.legendBounds, chartLegend, chart, legend); + legend.legendBounds.width += (chartLegend.containerPadding?.left as Required + + (chartLegend.containerPadding?.right as Required)); + legend.legendBounds.height += (chartLegend.containerPadding?.top as Required + + (chartLegend.containerPadding?.bottom as Required)); + if (legend.legendBounds.height > 0 && legend.legendBounds.width > 0) { + getLocation(legend.position as Required, legend.align as Required, + legend.legendBounds, rect, availableSize, chart, legend, previousLegendBounds, pointAnimation); + } +} + +// === LAYOUT CALCULATION & POSITIONING === + +/** + * Calculates the rendering point for the legend item based on various parameters. + * + * @param {ChartSizeProps} availableSize - The available size for rendering. + * @param {Rect} legendBounds - The bounds within which the legend is to be positioned. + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {Chart} chart - The chart object. + * @param {BaseLegend} legend - The legend object. + * @returns {void} This function does not return a value. + * @private + */ +export function getLegendBounds( + availableSize: ChartSizeProps, + legendBounds: Rect, + chartLegend: ChartLegendProps, + chart: Chart, + legend: BaseLegend +): void { + calculateLegendTitle(chartLegend, legendBounds, chart, legend); + legend.isTitle = chartLegend.title ? true : false; + legend.chartRowCount = 1; + legend.rowHeights = []; + legend.columnHeights = []; + legend.pageHeights = []; + const padding: number = chartLegend.padding as Required; + let extraHeight: number = 0; + let legendOption: LegendOptions; + let extraWidth: number = 0; + const arrowWidth: number = legend.arrowWidth as Required; + const arrowHeight: number = legend.arrowHeight as Required; + const verticalArrowSpace: number = legend.isVertical && !chartLegend.enablePages ? arrowHeight : 0; + const titleSpace: number = legend.isTitle ? legend.legendTitleSize?.height as Required + + (legend.fivePixel as Required) : 0; + if (!legend.isVertical) { + extraHeight = !chartLegend.height ? ((availableSize.height / 100) * 5) : 0; + } else { + extraWidth = !chartLegend.width ? ((availableSize.width / 100) * 5) : 0; + } + legendBounds.height += extraHeight; + legendBounds.width += extraWidth; + let shapeWidth: number = (legend.shapeWidth || 10) as Required; + let shapePadding: number = legend.shapePadding as Required; + let maximumWidth: number = 0; + let rowWidth: number = 0; + let legendWidth: number = 0; + let columnHeight: number = 0; + let columnCount: number = 0; + let rowCount: number = 0; + let titlePlusArrowSpace: number = 0; + let render: boolean = false; + const textStyle: TextStyleModel = chartLegend.textStyle as Required; + legend.maxItemHeight = Math.max(measureText('MeasureText', textStyle, chart.themeStyle.legendLabelFont).height, (chartLegend.shapeHeight || 10) as Required); + if (chartLegend.fixedWidth) { + for (let i: number = 0; legend.legendCollections && i < legend.legendCollections.length; i++) { + const textWidth: number = shapeWidth + shapePadding + (!(legend.isVertical as Required) ? (i === 0) ? padding : + (legend.itemPadding as Required) : padding) + (chartLegend.maxLabelWidth ? + chartLegend.maxLabelWidth : + measureText(legend.legendCollections[i as number].text, (chartLegend.textStyle as Required), + chart.themeStyle.legendLabelFont).width); + legend.maxWidth = Math.max(legend.maxWidth as Required, textWidth); + } + } + for (let i: number = 0; legend.legendCollections && i < legend.legendCollections.length; i++) { + legendOption = legend.legendCollections[i as number]; + legendOption.textSize = measureText(legendOption.text, legend.textStyle as Required, + chart.themeStyle.legendLabelFont); + shapeWidth = legendOption.text ? (chartLegend.shapeWidth || 10) as Required : 0; + shapePadding = legendOption.text ? chartLegend.shapePadding as Required : 0; + if (legendOption.render && legendOption.text) { + render = true; + const textWidth: number = chartLegend.maxLabelWidth || legendOption.textSize.width; + let paddingValue: number = padding; + const isHorizontalLayout: boolean = !legend.isVertical; + + if (isHorizontalLayout) { + const isFirstItem: boolean = i === 0; + + if (!isFirstItem) { + paddingValue = legend.itemPadding as Required; + } + } + + legendWidth = chartLegend.fixedWidth ? + legend.maxWidth as Required : + shapeWidth + shapePadding + textWidth + paddingValue; + + rowWidth = rowWidth + legendWidth; + if (!chartLegend.enablePages && !legend.isVertical) { + titlePlusArrowSpace = 0; + titlePlusArrowSpace += arrowWidth; + } + getLegendHeight(legendOption, chartLegend, legendBounds, rowWidth, legend.maxItemHeight, padding, chart); + if (legendBounds.width < (rowWidth + titlePlusArrowSpace) || (legend.isVertical)) { + maximumWidth = Math.max(maximumWidth, (rowWidth + padding + titlePlusArrowSpace - (legend.isVertical ? 0 : legendWidth))); + if (rowCount === 0 && (legendWidth !== rowWidth)) { + rowCount = 1; + } + rowWidth = legend.isVertical ? 0 : legendWidth; + rowCount++; + columnCount = 0; + columnHeight = verticalArrowSpace; + } + const len: number = (rowCount > 0 ? (rowCount - 1) : 0); + legend.rowHeights[len as number] = Math.max((legend.rowHeights[len as number] ? legend.rowHeights[len as number] : 0) + , Math.max(legendOption.textSize.height, (chartLegend.shapeHeight || 10) as Required)); + legend.columnHeights[columnCount as number] = (legend.columnHeights[columnCount as number] ? + legend.columnHeights[columnCount as number] : 0) + (((legend.isVertical as Required) || (rowCount > 0 && + chartLegend.itemPadding)) ? (i === 0) ? padding : legend.itemPadding as Required : padding) + + Math.max(legendOption.textSize.height, (chartLegend.shapeHeight || 10) as Required); + columnCount++; + } + } + columnHeight = Math.max.apply(null, legend.columnHeights) + padding + titleSpace; + columnHeight = Math.max(columnHeight, (legend.maxItemHeight + padding) + padding + titleSpace); + legend.isPaging = (legendBounds.height < columnHeight); + if (legend.isPaging && !chartLegend.enablePages) { + if (!legend.isVertical) { + columnHeight = (legend.maxItemHeight + padding) + padding + titleSpace; + } + } + legend.totalPages = rowCount; + if (render) { + setBounds(Math.max((rowWidth + padding), maximumWidth), columnHeight, chartLegend, legendBounds, legend); + } else { + setBounds(0, 0, chartLegend, legendBounds, legend); + } +} +// === TITLE HANDLING === + +/** + * Calculates the legend title text width and height. + * + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {Rect} legendBounds - The bounding rectangle for the legend. + * @param {Chart} chart - The chart object. + * @param {BaseLegend} legend - The legend object. + * @returns {void} This function does not return a value. + * @private + */ +export function calculateLegendTitle( + chartLegend: ChartLegendProps, + legendBounds: Rect, + chart: Chart, + legend: BaseLegend +): void { + if (chartLegend.title) { + legend.isTop = true; + const padding: number = (chartLegend).titleOverflow === 'Trim' ? 2 * (chartLegend.padding as Required) : 0; + if (legend.isTop || legend.isVertical) { + legend.legendTitleCollections = getTitle(chartLegend.title, (chartLegend.titleStyle as Required), ( + legendBounds.width - padding), chart.enableRtl, chart.themeStyle.legendTitleFont, + chartLegend.titleOverflow as Required); + } else { + (legend.legendTitleCollections as Required)[0] = + useTextTrim(chartLegend.maxTitleWidth as Required, chartLegend.title, + chartLegend.titleStyle as Required, chart.enableRtl, chart.themeStyle.legendTitleFont); + } + const text: string = legend.isTop ? chartLegend.title : (legend.legendTitleCollections as Required)[0]; + legend.legendTitleSize = measureText(text, (chartLegend.titleStyle as Required), + chart.themeStyle.legendTitleFont); + ((legend.legendTitleSize as Required).height as Required) *= + (legend.legendTitleCollections as Required).length; + } else { + legend.legendTitleSize = { width: 0, height: 0 }; + } +} + +/** + * Calculates the total height of the legend (including paddings, title, and items). + * + * @param {LegendOptions} legendOption - Options for the legend configuration. + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {Rect} legendBounds - The bounding rectangle for the legend. + * @param {number} rowWidth - The width of a row in the legend. + * @param {number} legendHeight - The current height of the legend. + * @param {number} padding - The padding applied to the legend layout. + * @param {Chart} chart - The chart object. + * @returns {void} This function does not return a value. + * @private + */ +export function getLegendHeight( + legendOption: LegendOptions, + chartLegend: ChartLegendProps, + legendBounds: Rect, + rowWidth: number, + legendHeight: number, + padding: number, + chart: Chart +): void { + const legendWidth: number = legendOption.textSize.width; + const textPadding: number = chartLegend.shapePadding as Required + (padding * 2) + + ((chartLegend.shapeWidth || 10) as Required); + if (legendWidth > (chartLegend.maxLabelWidth as Required) || legendWidth + rowWidth > legendBounds.width) { + legendOption.textCollection = useTextWrap( + legendOption.text, + (chartLegend.maxLabelWidth ? Math.min(chartLegend.maxLabelWidth, (legendBounds.width - textPadding)) : + (legendBounds.width - textPadding)), chartLegend.textStyle as Required, chart.enableRtl, + chart.themeStyle.legendLabelFont + ); + } else { + (legendOption.textCollection as Required).push(legendOption.text); + } + legendOption.textSize.height = (legendHeight * (legendOption.textCollection?.length as Required)); +} + +/** + * To set bounds for chart. + * + * @param {number} computedWidth - The computed width of the legend. + * @param {number} computedHeight - The computed height of the legend. + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {Rect} legendBounds - The bounding rectangle for the legend. + * @param {BaseLegend} legend - The legend object. + * @returns {void} + * @private + */ +export function setBounds( + computedWidth: number, + computedHeight: number, + chartLegend: ChartLegendProps, + legendBounds: Rect, + legend: BaseLegend +): void { + let titleHeight: number = chartLegend.title && legend.legendTitleSize && legend.legendTitleSize.height ? + legend.legendTitleSize.height + (legend.fivePixel as Required) : 0; + if (legend.isVertical && legend.isPaging && !chartLegend.enablePages) { + titleHeight = chartLegend.title ? (legend.legendTitleSize?.height as Required) + (legend.fivePixel as Required) : 0; + titleHeight += ((legend.pageButtonSize as Required) + (legend.fivePixel as Required)); + } + computedWidth = Math.min(computedWidth, legendBounds.width); + computedHeight = Math.min(computedHeight, legendBounds.height); + legendBounds.width = !chartLegend.width ? computedWidth : legendBounds.width; + legendBounds.height = !chartLegend.height ? computedHeight : legendBounds.height; + if (legend.isTop && chartLegend.titleOverflow !== 'None') { + calculateLegendTitle(chartLegend, legendBounds, legend.chart as Chart, legend); + legendBounds.height += chartLegend.titleOverflow === 'Wrap' && (legend.legendTitleCollections as Required).length > 1 ? ((legend.legendTitleSize?.height as Required) - ((legend.legendTitleSize?.height as Required) + / (legend.legendTitleCollections as Required).length)) : 0; + } + legend.rowCount = Math.max(1, Math.ceil((legendBounds.height - (chartLegend.padding as Required) - titleHeight) / + ((legend.maxItemHeight as Required) + (chartLegend.padding as Required)))); +} + +/** + * Determines the location of the legend based on its position and alignment. + * + * @param {LegendPosition} position - Position of the legend. + * @param {HorizontalAlignment} alignment - Alignment of the legend. + * @param {Rect} legendBounds - The bounding rectangle of the legend. + * @param {Rect} rect - The main chart area rectangle. + * @param {ChartSizeProps} availableSize - The available size for the legend. + * @param {Chart} chart - The chart object. + * @param {BaseLegend} legend - The legend object. + * @param {Rect} [previousLegendBounds] - The previous bounding rectangle of the legend. + * @param {boolean} [pointAnimation] - Whether point animation is enabled. + * @returns {void} Does not return a value. + * @private + */ +export function getLocation( + position: LegendPosition, + alignment: HorizontalAlignment | VerticalAlignment, + legendBounds: Rect, + rect: Rect, + availableSize: ChartSizeProps, + chart: Chart, + legend: BaseLegend, + previousLegendBounds?: Rect, + pointAnimation?: boolean +): void { + const padding: number = legend.border?.width as Required; + const marginBottom: number = chart.margin.bottom; + const legendPadding: number = legend.padding as Required + 10; + let legendAlignment: HorizontalAlignment; + let legendHeight: number = legendBounds.height + padding + + (legend.margin?.top as Required) + (legend.margin?.bottom as Required); + const legendWidth: number = legendBounds.width + padding + + (legend.margin?.left as Required) + (legend.margin?.right as Required); + if (position === 'Bottom' || position === 'Auto') { + legendAlignment = alignment === 'Left' || alignment === 'Right' ? alignment : 'Center'; + legendBounds.x = alignLegend(legendBounds.x, availableSize.width, legendBounds.width, legendAlignment); + legendBounds.y = rect.y + (rect.height - legendHeight) + (padding as Required) + + (legend.margin?.top as Required); + if ((!pointAnimation || (legendBounds.height !== previousLegendBounds?.height))) { + subtractThickness(rect, { left: 0, right: 0, top: 0, bottom: legendHeight + legendPadding }); + } + } else if (position === 'Top') { + legendAlignment = alignment === 'Left' || alignment === 'Right' ? alignment : 'Center'; + const axisTextSize: ChartSizeProps = measureText('100', chart.axisCollection[1].labelStyle as Required, chart.themeStyle.legendLabelFont); + legendBounds.x = alignLegend(legendBounds.x, availableSize.width, legendBounds.width, legendAlignment); + legendBounds.y = rect.y + (padding as Required) + (legend.margin?.top as Required); + legendHeight -= -padding * 2 - axisTextSize.height / 2; + if (!pointAnimation || (legendBounds.height !== previousLegendBounds?.height)) { + subtractThickness(rect, { left: 0, right: 0, top: legendHeight + (legend.padding as Required), bottom: 0 }); + } + } else if (position === 'Right') { + legendAlignment = alignment === 'Top' ? 'Left' : alignment === 'Bottom' ? 'Right' : 'Center'; + legendBounds.x = rect.x + (rect.width - legendBounds.width) - (legend.margin?.right as Required); + legendBounds.y = rect.y + alignLegend(0, availableSize.height - (rect.y + marginBottom), + legendBounds.height, legendAlignment); + if (!pointAnimation || (legendBounds.width !== previousLegendBounds?.width)) { + subtractThickness(rect, { left: 0, right: legendWidth + legendPadding, top: 0, bottom: 0 }); + } + + } else if (position === 'Left') { + legendAlignment = alignment === 'Top' ? 'Left' : alignment === 'Bottom' ? 'Right' : 'Center'; + legendBounds.x = legendBounds.x + (legend.margin?.left as Required); + legendBounds.y = rect.y + alignLegend(0, availableSize.height - (rect.y + marginBottom), + legendBounds.height, legendAlignment); + if (!pointAnimation || (legendBounds.width !== previousLegendBounds?.width)) { + subtractThickness(rect, { left: legendWidth + legendPadding, right: 0, top: 0, bottom: 0 }); + } + } else { + legendBounds.x = legend.location?.x as Required; + legendBounds.y = legend.location?.y as Required; + subtractThickness(rect, { left: 0, right: 0, top: 0, bottom: 0 }); + } +} + +/** + * To find legend alignment for chart. + * + * @param {number} start - The starting position for alignment. + * @param {number} size - The size of the available space for the legend. + * @param {number} legendSize - The size of the legend. + * @param {HorizontalAlignment} alignment - The desired alignment (e.g., start, center, end). + * @returns {number} The calculated position for aligning the legend within the chart. + * @private + */ +export function alignLegend(start: number, size: number, legendSize: number, alignment: HorizontalAlignment): number { + switch (alignment) { + case 'Right': + start = (size - legendSize) - start; + break; + case 'Center': + start = ((size - legendSize) / 2); + break; + } + return start; +} + +// === RENDERING FUNCTIONS === + +/** + * Renders the legend. + * + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {Rect} legendBounds - The bounding rectangle for the legend. + * @param {Chart} chart - The chart object. + * @param {BaseLegend} legend - The legend object. + * @returns {void} This function does not return a value. + * @private + */ +export function renderLegend(chartLegend: ChartLegendProps, legendBounds: Rect, chart: Chart, legend: BaseLegend): void { + let titleHeight: number = 0; let titlePlusArrowWidth: number = 0; + let pagingLegendBounds: Rect = { x: 0, y: 0, width: 0, height: 0 }; + let requireLegendBounds: Rect = { x: 0, y: 0, width: 0, height: 0 }; + const firstLegend: number = findFirstLegendPosition(legend.legendCollections as Required); + const padding: number = chartLegend.padding as Required; + const isPaging: boolean = chartLegend.enablePages as Required; + const upArrowHeight: number = legend.isPaging && !chartLegend.enablePages && legend.isVertical ? + legend.pageButtonSize as Required : 0; + createLegendElements(legendBounds, legend, legend.legendID as Required); + legend.legendRegions = []; + legend.chartRowCount = 1; + let maxHeight: number = 0; + titleHeight = !legend.isTitle ? 0 : (legend.isTop || legend.isVertical ? legend.legendTitleSize?.height as Required + : 0); + + let pageCount: number = 1; + let rowHeights: number = (((legend.isVertical) || ((legend.rowHeights?.length as Required) > 1 && + (chartLegend.itemPadding))) ? legend.itemPadding as Required : padding) + + (legend.rowHeights as Required)[0]; + for (let i: number = 1; legend.rowHeights && i < legend.rowHeights.length; i++) { + if ((rowHeights + legend.rowHeights[i as number] + (((legend.isVertical || (legend.rowHeights.length > 1)) && + chartLegend.itemPadding) ? legend.itemPadding as Required : padding)) + > ((legend.legendBounds?.height as Required) - (legend.pageButtonSize as Required) - + (legend.maxItemHeight as Required) / 2) - (chartLegend.containerPadding?.top as Required) - + (chartLegend.containerPadding?.bottom as Required)) { + (legend.pageHeights as Required)[pageCount - 1] = rowHeights + titleHeight; + pageCount++; + rowHeights = 0; + } + rowHeights += (legend.rowHeights[i as number] + ((legend.isVertical || (legend.rowHeights.length > 1 && + chartLegend.itemPadding)) ? legend.itemPadding as Required : padding)); + } + (legend.pageHeights as Required)[pageCount - 1] = rowHeights + titleHeight; + legend.totalPages = pageCount; + + for (let i: number = 0; legend.legendCollections && i < legend.legendCollections.length; i++) { + maxHeight = Math.max(legend.legendCollections[i as number].textSize.height, maxHeight); + break; + } + if (!isPaging && legend.isPaging && !legend.isVertical) { + titlePlusArrowWidth = 0; + titlePlusArrowWidth += ((legend.pageButtonSize as Required) + (2 * (legend.fivePixel as Required))); + } else if (legend.isTitle && !legend.isVertical) { + titlePlusArrowWidth = 0; + } + if (legend.legendCollections && firstLegend !== legend.legendCollections.length) { + let count: number = 0; + let previousLegend: LegendOptions = legend.legendCollections[firstLegend as number]; + const startPadding: number = titlePlusArrowWidth + padding + + (((chartLegend.shapeWidth || 10) as Required) / 2) + (chartLegend.containerPadding?.left as Required); + const xLocation: number = (!legend.isRtlEnable) ? legendBounds.x + startPadding : legendBounds.x + + (legendBounds.width) - startPadding; + const start: ChartLocationProps = { + x: xLocation, y: legendBounds.y + titleHeight + upArrowHeight + padding + + ((legend.maxItemHeight as Required) / 2) + (legend.containerPadding?.top as Required) + }; + const anchor: string = (chart as Chart).isRtlEnabled || (chart as Chart).enableRtl ? 'end' : 'start'; + const textOptions: TextOption = { + id: '', + x: start.x, + y: start.y, + anchor: anchor, + text: '', + labelRotation: 0, + fontFamily: '', + fontWeight: '', + fontSize: '', + fontStyle: '', + opacity: 0, + fill: '', + baseLine: '' + }; + const textPadding: number = chartLegend.shapePadding as Required + (legend.itemPadding as Required) + + ((chartLegend.shapeWidth || 10) as Required); + legend.pageXCollections = []; + legend.legendCollections[firstLegend as number].location = start; + let legendIndex: number; + if (!chartLegend.enablePages && legend.isPaging) { + const x: number = start.x - (legend.fivePixel as Required); + const y: number = start.y - (legend.fivePixel as Required); + const leftSpace: number = 0; + const bottomSapce: number = legend.isVertical ? (legend.pageButtonSize as Required) + + Math.abs(y - legendBounds.y) : 0; + let rightSpace: number = 0; + rightSpace += legend.isVertical ? 0 : ((legend.fivePixel as Required) + + (legend.pageButtonSize as Required) + (legend.fivePixel as Required)); + pagingLegendBounds = { + x: x, y: y, width: legendBounds.width - rightSpace - leftSpace, height: + legendBounds.height - bottomSapce + }; + requireLegendBounds = pagingLegendBounds; + } else { + requireLegendBounds = legendBounds; + } + let legendOption: LegendOptions; + + for (let i: number = 0; i < legend.legendCollections.length; i++) { + const legendTextOption: TextOption = { + id: '', + x: start.x, + y: start.y, + anchor: anchor, + text: '', + labelRotation: 0, + fontFamily: '', + fontWeight: '', + fontSize: '', + fontStyle: '', + opacity: 0, + fill: '', + baseLine: '' + }; + legendOption = legend.legendCollections[i as number]; + legendIndex = !legend.isReverse ? count : (legend.legendCollections.length - 1) - count; + legend.accessbilityText = 'Click to show or hide the ' + legendOption.text + ' series'; + getRenderPoint(chartLegend, legendOption, start, textPadding, previousLegend, requireLegendBounds, count, + firstLegend, legend); + renderSymbol(chartLegend, legendOption, legendIndex, chart, legend); + renderText(chartLegend, legendOption, legendTextOption, legendIndex, chart, legend); + previousLegend = legendOption; + count++; + } + legend.totalPages = (legend.isPaging && !chartLegend.enablePages && !legend.isVertical && + legend.totalPages > legend.chartRowCount) ? legend.chartRowCount : legend.totalPages; + legend.currentPage = (legend.currentPage as Required) > 1 && + (legend.currentPage as Required) > legend.totalPages ? legend.totalPages : legend.currentPage; + if (legend.isPaging && legend.totalPages > 1) { + renderPagingElements(chartLegend, legendBounds, textOptions, chart, legend); + } else { + legend.totalPages = 1; + } + } +} + +/** + * Calculates the rendering point for the legend item based on various parameters. + * + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {LegendOptions} legendOption - The legend option to be rendered. + * @param {ChartLocationProps} start - The starting location for the legend item. + * @param {number} textPadding - The padding between legend text and shapes. + * @param {LegendOptions} previousLegend - The previously rendered legend option. + * @param {Rect} rect - The rectangle defining the rendering bounds. + * @param {number} count - The current legend item count. + * @param {number} firstLegend - The index of the first visible legend item. + * @param {BaseLegend} legend - The legend object. + * @returns {void} This function does not return a value. + * @private + */ +export function getRenderPoint( + chartLegend: ChartLegendProps, + legendOption: LegendOptions, + start: ChartLocationProps, + textPadding: number, + previousLegend: LegendOptions, + rect: Rect, + count: number, + firstLegend: number, + legend: BaseLegend +): void { + const padding: number = chartLegend.padding as Required; + const textWidth: number = chartLegend.fixedWidth ? legend.maxWidth as Required : textPadding + + (chartLegend.maxLabelWidth ? chartLegend.maxLabelWidth as Required : previousLegend.textSize.width); + const rightSpace: number = 0; + const previousBound: number = previousLegend.location.x + ((!legend.isRtlEnable) ? textWidth : -textWidth); + if (isWithinBounds(previousBound + rightSpace, (chartLegend.maxLabelWidth ? + chartLegend.maxLabelWidth : legendOption.textSize.width) + textPadding - (legend.itemPadding as Required), rect, legend) + || (legend.isVertical)) { + legendOption.location.x = start.x; + if (count !== firstLegend) { + (legend.chartRowCount as Required)++; + } + legendOption.location.y = (count === firstLegend) ? previousLegend.location.y : previousLegend.location.y + + (legend.isVertical ? Math.max(previousLegend.textSize.height, (chartLegend.shapeHeight || 10) as Required) : + (legend.rowHeights as Required)[(legend.chartRowCount as Required - 2)]) + + ((legend.isVertical || ((legend.chartRowCount as Required) > 1 + && chartLegend.itemPadding)) ? legend.itemPadding as Required : padding); + } else { + legendOption.location.x = (count === firstLegend) ? previousLegend.location.x : previousBound; + legendOption.location.y = previousLegend.location.y; + } + let availwidth: number = (!legend.isRtlEnable) ? (legend.legendBounds?.x as Required + + (legend.legendBounds?.width as Required)) - + (legendOption.location.x + textPadding - (legend.itemPadding as Required) - + ((chartLegend.shapeWidth || 10) as Required) / 2) : (legendOption.location.x - textPadding + + (legend.itemPadding as Required) + (((chartLegend.shapeWidth || 10) as Required) / 2)) - + (legend.legendBounds?.x as Required); + if (!legend.isVertical && legend.isPaging && !chartLegend.enablePages) { + availwidth = legend.legendBounds?.width as Required - (legend.fivePixel as Required); + } + availwidth = chartLegend.maxLabelWidth ? Math.min(chartLegend.maxLabelWidth, availwidth) : availwidth; +} + +/** + * To create legend rendering elements. + * + * @param {Rect} legendBounds - The bounding rectangle for the legend. + * @param {BaseLegend} legend - The legend object. + * @param {string} id - The unique identifier for the legend elements. + * @returns {void} This function does not return a value. + * @private + */ +export function createLegendElements(legendBounds: Rect, legend: BaseLegend, id: string): void { + const options: RectOption = createRectOption(id + '_element', legend.background as Required, legend.border as Required, legend.opacity as Required, legendBounds, 0, 0, '', legend.border?.dashArray as Required); + if (legend.title) { + renderLegendTitle(legend, legendBounds); + } + options.y += (legend.isTop ? legend.legendTitleSize?.height as Required : 0) + + (legend.containerPadding?.top as Required); + options.height -= (legend.isTop ? legend.legendTitleSize?.height as Required : 0) + + (legend.containerPadding?.top as Required); + options.id += '_clipPath_rect'; + options.width = legendBounds.width; + legend.clipRect = options; + legend.pagingClipRect = options; +} + +/** + * Render the legend title. + * + * @param {BaseLegend} legend - The legend object. + * @param {Rect} legendBounds - The bounding rectangle for the legend. + * @returns {void} This function does not return a value. + * @private + */ +export function renderLegendTitle(legend: BaseLegend, legendBounds: Rect): void { + const padding: number = legend.padding as Required; + const alignment: HorizontalAlignment = legend.titleAlign as HorizontalAlignment; + legend.isTop = true; + let x: number = titlePositionX(legendBounds, legend.titleAlign as Required); + x = alignment === 'Left' ? (x + padding) : alignment === 'Right' ? (x - padding) : x; + x = (legend.isTop || legend.isVertical) ? x : ((legendBounds.x) + ( + (legendBounds.width - (legend.legendTitleSize?.width as Required) - 5))); + const topPadding: number = (legendBounds.height / 2) + ((legend.legendTitleSize?.height as Required) / 4); + const y: number = legendBounds.y + (!legend.isTop && !legend.isVertical ? topPadding : + ((legend.legendTitleSize?.height as Required) / (legend.legendTitleCollections as Required).length)); + legend.legendTitleLoction = { x: x, y: y }; +} + +// === PAGING & NAVIGATION === +/** + * To render legend paging elements for chart. + * + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {Rect} bounds - The bounding rectangle for the legend elements. + * @param {TextOption} textOption - The options for the text elements. + * @param {Chart} chart - The chart object. + * @param {BaseLegend} legend - The legend object. + * @returns {void} This function does not return a value. + * @private + */ +export function renderPagingElements( + chartLegend: ChartLegendProps, + bounds: Rect, + textOption: TextOption, + chart: Chart, + legend: BaseLegend +): void { + const titleHeight: number = legend.legendTitleSize?.height as Required; + const grayColor: string = (chart.theme.indexOf('Dark') > -1) ? '#FFFFFF' : '#545454'; + const padding: number = 8; + const pageUp: string = legend.legendID + (!legend.isRtlEnable ? '_pageup' : '_pagedown'); + const pageDown: string = legend.legendID + (!legend.isRtlEnable ? '_pagedown' : '_pageup'); + legend.pageUpOption = createPathOption(pageUp, 'transparent', 5, grayColor, 1, '', ''); + legend.pageDownOption = createPathOption(pageDown, 'transparent', 5, grayColor, 1, '', ''); + const iconSize: number = chart.availableSize.width < 110 || chart.availableSize.height < 190 ? 4 : + legend.pageButtonSize as Required; + const titleWidth: number = 0; + legend.pagingRegions = []; + legend.backwardArrowOpacity = legend.currentPage !== 1 ? 1 : 0; + legend.forwardArrowOpacity = legend.currentPage === legend.totalPages ? 0 : 1; + if ((chartLegend.enablePages && legend.isPaging)) { + legend.clipPathHeight = (legend.pageHeights as Required)[0] - (legend.isTitle && legend.isTop ? + legend.legendTitleSize?.height as Required : 0) - (legend.containerPadding?.top as Required) - + (legend.containerPadding?.bottom as Required); + } else { + legend.clipPathHeight = ((legend.rowCount as Required) * ((legend.maxItemHeight as Required) + + (legend.padding as Required))); + } + (legend.clipRect as Required).height = legend.clipPathHeight as Required; + let x: number = (bounds.x + iconSize / 2); + let y: number = bounds.y + legend.clipPathHeight + ((titleHeight + bounds.height - legend.clipPathHeight) / 2); + if (legend.isPaging && !chartLegend.enablePages && !legend.isVertical) { + x = (bounds.x + (legend.pageButtonSize as Required) + titleWidth); + y = legend.title && legend.isTop ? (bounds.y + padding + titleHeight + (iconSize / 1) + 0.5) : + (bounds.y + padding + iconSize + 0.5); + } + const size: ChartSizeProps = measureText(legend.totalPages + '/' + legend.totalPages, {} as TextStyleModel, chart.themeStyle.legendLabelFont); + const translateX: number = (legend.isRtlEnable) ? (legend.border?.width as Required) + (iconSize / 2) : + bounds.width - (2 * (iconSize + padding) + padding + size.width); + if (legend.isVertical && !legend.enablePages) { + x = bounds.x + (bounds.width / 2); + y = bounds.y + (iconSize / 2) + (padding / 2) + titleHeight; + (legend.pageUpOption as Required).opacity = legend.backwardArrowOpacity; + legend.pageUpOption = calculateLegendShapes({ x: x, y: y }, { width: iconSize, height: iconSize }, 'UpArrow', (legend.pageUpOption as Required)); + } else { + (legend.pageUpOption as Required).opacity = (legend.enablePages ? 1 : !legend.isRtlEnable ? + legend.backwardArrowOpacity : legend.forwardArrowOpacity); + legend.pageUpOption = calculateLegendShapes({ x: x, y: y }, { width: iconSize, height: iconSize }, 'LeftArrow', (legend.pageUpOption as Required)); + } + legend.pageUpOption = legend.pageUpOption as Required; + legend.pagingRegions.push({ + x: !legend.isRtlEnable ? x + bounds.width - (2 * (iconSize + padding) + padding + size.width) + - iconSize * 0.5 : x, y: y - iconSize * 0.5, width: iconSize, height: iconSize + }); + textOption.x = x + (iconSize / 2) + padding; + textOption.y = y + (size.height / 4); + textOption.id = legend.legendID + '_pagenumber'; + textOption.text = !legend.isRtlEnable ? '1/' + legend.totalPages : legend.totalPages + '/1'; + textOption.fontSize = chart.themeStyle.legendLabelFont.fontSize; + legend.pageTextOption = textOption; + x = textOption.x + padding + (iconSize / 2) + size.width; + if (legend.isPaging && !legend.enablePages && !legend.isVertical) { + x = bounds.x + bounds.width - (legend.pageButtonSize as Required); + } + + (legend.pageDownOption as Required).id = pageDown; + (legend.pageDownOption as Required).opacity = !legend.enablePages ? !legend.isRtlEnable ? legend.forwardArrowOpacity : + legend.backwardArrowOpacity : 1; + if (legend.isVertical && !legend.enablePages) { + x = bounds.x + (bounds.width / 2); + y = bounds.y + bounds.height - (iconSize / 2); + legend.pageDownOption = calculateLegendShapes({ x: x, y: y }, { width: iconSize, height: iconSize }, 'DownArrow', (legend.pageDownOption as Required)); + } else { + legend.pageDownOption = calculateLegendShapes({ x: x, y: y }, { width: iconSize, height: iconSize }, 'RightArrow', (legend.pageDownOption as Required)); + } + legend.pageDownOption = legend.pageDownOption as Required; + legend.pagingRegions.push({ + x: !legend.isRtlEnable ? x + (bounds.width - (2 * (iconSize + padding) + padding + size.width) - iconSize * 0.5) : x, + y: y - iconSize * 0.5, width: iconSize, height: iconSize + }); + if (legend.enablePages) { + legend.transform = 'translate(' + translateX + ', ' + 0 + ')'; + } else { + if (legend.currentPageNumber === 1 && legend.enablePages) { + legend.totalNoOfPages = legend.totalPages; + } + if (!legend.enablePages) { + translatePage((legend.currentPage as Required) - 1, legend.currentPage as Required, chartLegend, legend); + } + } +} + +/** + * To render legend symbols for chart. + * + * @param {ChartLegendProps} chartLegend - The chart legend properties. + * @param {LegendOptions} legendOption - Options for configuring the legend symbols. + * @param {number} legendIndex - The index of the specific legend item. + * @param {Chart} chart - The chart object. + * @param {BaseLegend} legend - The legend object. + * @returns {void} This function does not return a value. + * @private + */ +export function renderSymbol( + chartLegend: ChartLegendProps, + legendOption: LegendOptions, + legendIndex: number, + chart: Chart, + legend: BaseLegend +): void { + const symbolColor: string = legendOption.visible ? legendOption.fill : '#D3D3D3'; + const isStrokeWidth: boolean = (((legendOption.shape === 'SeriesType') && + (legendOption.type.toLowerCase().indexOf('line') > -1) && (legendOption.type.toLowerCase().indexOf('area') === -1)) || + ((legendOption.shape === 'HorizontalLine') || (legendOption.shape === 'VerticalLine') || (legendOption.shape === 'Cross'))); + let shape: string = (legendOption.shape === 'SeriesType') ? legendOption.type : legendOption.shape; + shape = shape === 'Scatter' ? legendOption.markerShape as string : shape; + const strokewidth: number = isStrokeWidth ? chart.visibleSeries[legendIndex as number].width as Required : 1; + let symbolOption: PathOptions = createPathOption( + legend.legendID + '_shape_' + legendIndex, symbolColor, strokewidth, + symbolColor, legend.opacity as Required, legendOption.dashArray as Required, ''); + const textSize: ChartSizeProps = measureText(legendOption.text, chartLegend.textStyle as Required, + chart.themeStyle.legendLabelFont); + const x: number = chartLegend.inversed && !legend.isRtlEnable ? legendOption.location.x + textSize.width + + (chartLegend.shapePadding as Required) : legendOption.location.x; + const y: number = legendOption.location.y; + const shapeWidth: number = shape === 'Rectangle' && !chartLegend.shapeWidth ? 8 : (chartLegend.shapeWidth || 10) as Required; + const shapeHeight: number = shape === 'Rectangle' && !chartLegend.shapeHeight ? 8 : (chartLegend.shapeHeight || 10) as Required; + symbolOption = calculateShapes({ x: x, y: y }, { width: shapeWidth, + height: shapeHeight }, shape, symbolOption, legendOption.url as Required) as PathOptions; + legendOption.symbolOption = symbolOption; + if (shape === 'Line' && legendOption.markerVisibility && legendOption.markerShape !== 'Image') { + let markerOption: PathOptions = createPathOption( + legend.legendID + '_shape_marker_' + legendIndex, symbolColor, strokewidth, + symbolColor, legend.opacity as Required, legendOption.dashArray as Required, ''); + shape = legendOption.markerShape as Required; + markerOption = calculateShapes({ x: x, y: y }, { width: ((chartLegend.shapeWidth || 10) as Required) / 2, + height: ((chartLegend.shapeHeight || 10) as Required) / 2 }, shape, + markerOption, legendOption.url as Required) as PathOptions; + legendOption.markerOption = markerOption; + } +} + +/** + * To render legend text for chart. + * + * @param {ChartLegendProps} chartLegend - Properties for the chart legend. + * @param {LegendOptions} legendOption - Options for the legend. + * @param {TextOption} textOptions - Options for rendering the text. + * @param {number} legendIndex - Index of the legend. + * @param {Chart} chart - The chart object. + * @param {BaseLegend} legend - The legend object. + * @returns {void} + * @private + */ +export function renderText( + chartLegend: ChartLegendProps, + legendOption: LegendOptions, + textOptions: TextOption, + legendIndex: number, + chart: Chart, + legend: BaseLegend +): void { + const hiddenColor: string = '#D3D3D3'; + const fontcolor: string = legendOption.visible ? (legend.textStyle as Required).color || + chart.themeStyle.legendLabelFont.color : hiddenColor; + textOptions.fill = fontcolor; + textOptions.id = legend.legendID + '_text_' + legendIndex; + textOptions.text = (legendOption.textCollection?.length as Required) > 0 ? + legendOption.textCollection as Required : legendOption.text; + if (chartLegend.inversed && !legend.isRtlEnable) { + textOptions.x = legendOption.location.x - (((chartLegend.shapeWidth || 10) as Required) / 2); + } + else if (legend.isRtlEnable) { + const textWidth: number = measureText(legendOption.text, legend.textStyle as Required, + chart.themeStyle.legendLabelFont).width; + textOptions.x = legendOption.location.x - (((legendOption.textCollection as Required).length > 1 ? + textWidth / (legendOption.textCollection as Required).length : textWidth) + + ((legend.shapeWidth || 10) as Required) / 2 + (legend.shapePadding as Required)); + } + else { + textOptions.x = legendOption.location.x + (((chartLegend.shapeWidth || 10) as Required) / 2) + + (chartLegend.shapePadding as Required); + } + textOptions.y = legendOption.location.y + (legend.maxItemHeight as Required) / 4; + legendOption.textOption = textOptions; +} + +/** + * Checks whether the provided coordinates are within the bounds. + * + * @param {number} previousBound - Description for previousBound. + * @param {number} textWidth - Width of the text. + * @param {Rect} rect - Bounding rectangle. + * @param {BaseLegend} legend - The legend object. + * @returns {boolean} `true` if the coordinates are within the bounds, otherwise `false`. + * @private + */ +export function isWithinBounds( + previousBound: number, + textWidth: number, + rect: Rect, + legend: BaseLegend +): boolean { + if (!legend.isRtlEnable) { + return (previousBound + textWidth) > (rect.x + rect.width + (((legend.shapeWidth || 10) as Required) / 2)); + } + return (previousBound - textWidth) < (rect.x - (((legend.shapeWidth || 10) as Required) / 2)); +} + +// === EVENT HANDLING & UTILITIES === +/** + * To find first valid legend text index for chart. + * + * @param {LegendOptions[]} legendCollections - Array of legend options. + * @returns {number} The index of the first valid legend text. + * @private + */ +export function findFirstLegendPosition(legendCollections: LegendOptions[]): number { + let count: number = 0; + for (const legend of legendCollections) { + if (legend.render && legend.text && legend.text !== '') { + break; + } + count++; + } + return count; +} + +/** + * To translate legend pages for chart + * + * @param {number} page - The current page to translate + * @param {number} pageNumber - The specific page number to translate to + * @param {ChartLegendProps} chartLegend - Properties for the chart legend. + * @param {BaseLegend} legend - The legend object + * @param {Function} [updateTransform] - Callback to update the transform of the legend + * @param {Function} [updatePageText] - Callback to update the text of the legend page + * @returns {number} The calculated size after translating the page + * @private + */ +export function translatePage( + page: number, + pageNumber: number, + chartLegend: ChartLegendProps, + legend: BaseLegend, + updateTransform?: (transform: string) => void, + updatePageText?: (text: string) => void +): number { + const size: number = (page ? getPageHeight(legend.pageHeights as Required, page, legend) : 0); + (legend.clipRect as Required).height = (legend.pageHeights as Required)[page as number] - + (legend.isTitle && legend.isTop ? (legend.legendTitleSize as Required).height : 0) - + (chartLegend.containerPadding?.top as Required) - (chartLegend.containerPadding?.bottom as Required); + const translate: string = 'translate(0,-' + size + ')'; + legend.legendTranslate = translate; + if (updateTransform) { + updateTransform(translate); + } + if (chartLegend.enablePages) { + if (updatePageText) { + updatePageText((pageNumber) + '/' + legend.totalPages); + } + } + legend.currentPage = pageNumber; + return size; +} + +/** + * Returns the total height required for all legend pages up to a certain count + * + * @param {number[]} pageHeights - Array of heights for each legend page. + * @param {number} pageCount - The page count up to which the height is calculated. + * @param {BaseLegend} legend - The legend object. + * @returns {number} The total height required for all pages up to the specified count. + * @private + */ +export function getPageHeight(pageHeights: number[], pageCount: number, legend: BaseLegend): number { + let sum: number = 0; + for (let i: number = 0; i < pageCount; i++) { + sum += pageHeights[i as number] - ((legend.isTitle && legend.isTop) + ? (legend.legendTitleSize as Required).height : 0); + } + return sum; +} + +/** + * To change legend pages for chart + * + * @param {ChartLegendProps} chartLegend - Properties for the chart legend. + * @param {boolean} pageUp - Indicates whether to move the page up. + * @param {Function} updateTransform - Callback to update the transform of the legend. + * @param {Function} updatePageText - Callback to update the text of the legend page. + * @param {BaseLegend} legend - The legend object. + * @returns {void} + * @private + */ +export function changePage( + chartLegend: ChartLegendProps, + pageUp: boolean, + updateTransform: (transform: string) => void, + updatePageText: (text: string) => void, + legend: BaseLegend +): void { + const page: number = legend.currentPage as Required; + if (pageUp && page > 1) { + translatePage((page - 2), (page - 1), chartLegend, legend, updateTransform, updatePageText); + } else if (!pageUp && page < (legend.totalPages as Required)) { + translatePage(page, (page + 1), chartLegend, legend, updateTransform, updatePageText); + } + if (legend.isPaging && !legend.enablePages) { + if (legend.currentPage === legend.totalPages) { + (legend.pageDownOption as Required).opacity = 0; + } + else { + (legend.pageDownOption as Required).opacity = 1; + } + if (legend.currentPage === 1) { + (legend.pageUpOption as Required).opacity = 0; + } + else { + (legend.pageUpOption as Required).opacity = 1; + } + } +} + +/** + * Handles the click event for a legend item. + * + * @param {ChartLegendProps} props - The properties of the legend item clicked + * @param {number} index - The index of the legend item + * @param {Chart} chart - The chart object + * @param {BaseLegend} legend - The legend object + * @returns {void} + * @private + */ +export function LegendClick(props: ChartLegendProps, index: number, chart: Chart, legend: BaseLegend): void { + const seriesIndex: number = index; + const legendIndex: number = !legend.isReverse ? index : ((legend.legendCollections as Required).length - 1) + - index; + const series: SeriesProperties = chart.visibleSeries[seriesIndex as number]; + const chartLegend: LegendOptions = (legend.legendCollections as Required)[legendIndex as number]; + if (chart.tooltipRef && chart.tooltipRef.current) { + chart.tooltipRef.current?.fadeOut(); + } + if (chart.trackballRef && chart.trackballRef.current) { + const childElements: HTMLCollection = chart.trackballRef.current.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.display = 'none'; + } + } + } + const legendClickArgs: LegendClickEvent = { + text: chartLegend.text, shape: chartLegend.shape, + seriesName: series.name as string, cancel: false + }; + chart.chartProps?.onLegendClick?.(legendClickArgs); + series.legendShape = legendClickArgs.shape; + if (!legendClickArgs.cancel) { + if (series.fill !== null) { + chart.visibleSeries[index as number].interior = series.fill || ''; + } + if (props.toggleVisibility) { + series.isLegendClicked = true; + changeSeriesVisiblity(series, series.visible, chart); + chartLegend.visible = (series.visible as Required); + if (chartLegend.markerOption) { + chartLegend.markerOption.fill = chartLegend.visible ? series.interior as Required : '#D3D3D3'; + chartLegend.markerOption.stroke = chartLegend.visible ? series.interior as Required : '#D3D3D3'; + } + if (chartLegend.symbolOption) { + if (!((series.type === 'Spline' || series.type === 'StepLine') && chartLegend.shape === 'SeriesType')) { + chartLegend.symbolOption.fill = chartLegend.visible ? series.interior as Required : '#D3D3D3'; + } + chartLegend.symbolOption.stroke = chartLegend.visible ? series.interior as Required : '#D3D3D3'; + } + if (chartLegend.textOption) { + chartLegend.textOption.fill = chartLegend.visible ? (legend.textStyle as Required).color || + chart.themeStyle.legendLabelFont.color : '#D3D3D3'; + } + chart.animateSeries = false; + chart.isLegendClicked = true; + const chartId: string = chart.element.id; + const triggerRender: (chartId?: string) => void = useRegisterAxisRender(); + triggerRender(chartId); + const triggerSeriesRender: (chartId?: string) => void = useRegisterSeriesRender(); + triggerSeriesRender(chartId); + } + } +} + +/** + * Changes the visibility of a series and updates secondary axis visibility if necessary. + * + * @param {SeriesProperties} series - The series to change visibility for. + * @param {boolean} visibility - The new visibility state. + * @param {Chart} chart - The chart object. + * @returns {void} + * @private + */ +export function changeSeriesVisiblity(series: SeriesProperties, visibility: boolean, chart: Chart): void { + series.visible = !visibility; + if (isSecondaryAxis(series.xAxis, chart.axes)) { + series.xAxis.internalVisibility = series.xAxis.series.some((value: SeriesProperties) => (value.visible)); + } + if (isSecondaryAxis(series.yAxis, chart.axes)) { + series.yAxis.internalVisibility = series.yAxis.series.some((value: SeriesProperties) => (value.visible)); + } +} + +/** + * Checks if the axis is a secondary axis within the chart + * + * @param {AxisModel} axis - The axis model to check. + * @param {AxisModel[]} axes - The secondary axis collection. + * @returns {boolean} `true` if the axis is a secondary axis, otherwise `false`. + * @private + */ +export function isSecondaryAxis(axis: AxisModel, axes: AxisModel[]): boolean { + return axes.some((secondaryAxis: AxisModel) => secondaryAxis.name === axis.name); +} diff --git a/components/charts/src/chart/renderer/SeriesRenderer/AreaSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/AreaSeriesRenderer.tsx new file mode 100644 index 0000000..ee8f279 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/AreaSeriesRenderer.tsx @@ -0,0 +1,494 @@ +import { ChartLocationProps, ChartMarkerProps } from '../../base/interfaces'; +import { AreaSeriesAnimateState, AreaSeriesRendererType, PathCommand, Points, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { getPoint } from '../../utils/helper'; +import { LineBase, LineBaseReturnType } from './LineBase'; +import MarkerRenderer from './MarkerRenderer'; +import { AnimationState, interpolatePathD } from './SeriesAnimation'; + +const lineBaseInstance: LineBaseReturnType = LineBase; + +/** + * Specialized path interpolation for area charts - handles both area fill and borders + * + * @param {string} startD - Starting path data + * @param {string} endD - Ending path data + * @param {number} progress - Animation progress (0-1) + * @returns {string} Interpolated path data + * @private + */ +export function interpolateAreaPath(startD: string, endD: string, progress: number): string { + if (!startD || !endD) { + return endD || startD || ''; + } + + // Split paths into commands + const startCommands: PathCommand[] = parsePathCommands(startD); + const endCommands: PathCommand[] = parsePathCommands(endD); + + // If formats don't match, use standard interpolation + if (startCommands.length === 0 || endCommands.length === 0 || + startCommands[0].type !== endCommands[0].type) { + return interpolatePathD(startD, endD, progress); + } + + // Get baseline Y values from the first command + const startBaselineY: number = startCommands[0].coords?.[1] ?? 0; + const endBaselineY: number = endCommands[0].coords?.[1] ?? 0; + + // Identify where data points end and closing path begins + const findLastDataPointIndex: (commands: PathCommand[]) => number = (commands: PathCommand[]): number => { + // Last data point is typically followed by a drop to the baseline + for (let i: number = commands.length - 3; i > 0; i--) { + if (commands[i as number].type === 'L' && commands[i + 1].type === 'L') { + // Check if this point is followed by a drop to baseline + if (Math.abs(commands[i + 1].coords[0] - commands[i as number].coords[0]) < 0.1 && + Math.abs(commands[i + 1].coords[1] - startBaselineY) < 1) { + return i; + } + } + } + + // Fallback: assume all but the last 2 commands are data + return Math.max(0, commands.length - 3); + }; + + // Find data point indices + const startLastDataIndex: number = findLastDataPointIndex(startCommands); + const endLastDataIndex: number = findLastDataPointIndex(endCommands); + + // Generate path with data points + let result: string = ''; + + // First point (M command) + result += 'M '; + result += `${startCommands[0].coords[0] + (endCommands[0].coords[0] - startCommands[0].coords[0]) * progress} `; + result += `${startCommands[0].coords[1] + (endCommands[0].coords[1] - startCommands[0].coords[1]) * progress} `; + + // Data points + for (let i: number = 1; i <= Math.max(startLastDataIndex, endLastDataIndex); i++) { + const startIdx: number = Math.min(i, startLastDataIndex); + const endIdx: number = Math.min(i, endLastDataIndex); + + const startCmd: PathCommand = startCommands[startIdx as number]; + const endCmd: PathCommand = endCommands[endIdx as number]; + + result += 'L '; + + // Important: Ensure proper last point animation by using actual corresponding points + const x: number = startCmd.coords[0] + (endCmd.coords[0] - startCmd.coords[0]) * progress; + const y: number = startCmd.coords[1] + (endCmd.coords[1] - startCmd.coords[1]) * progress; + result += `${x} ${y} `; + } + + // Closing points - get last data point coordinates + const lastStartData: PathCommand = startCommands[startLastDataIndex as number]; + const lastEndData: PathCommand = endCommands[endLastDataIndex as number]; + + // Calculate interpolated values for the last point + const lastX: number = lastStartData.coords[0] + (lastEndData.coords[0] - lastStartData.coords[0]) * progress; + //const lastY = lastStartData.coords[1] + (lastEndData.coords[1] - lastStartData.coords[1]) * progress; + + // Interpolate baseline + const interpolatedBaselineY: number = startBaselineY + (endBaselineY - startBaselineY) * progress; + + // First command coordinates (for closing) + const firstX: number = startCommands[0].coords[0] + (endCommands[0].coords[0] - startCommands[0].coords[0]) * progress; + + // Add vertical line down to baseline + result += `L ${lastX} ${interpolatedBaselineY} `; + + // Add horizontal line to start X + result += `L ${firstX} ${interpolatedBaselineY} `; + + // Close path back to start point + result += `L ${firstX} ${startCommands[0].coords[1] + (endCommands[0].coords[1] - startCommands[0].coords[1]) * progress} `; + + return result; +} + +/** + * Specialized interpolation for the border path of area charts + * Just handles the top line without closing the path + * + * @param {string} startD - Starting border path data + * @param {string} endD - Ending border path data + * @param {number} progress - Animation progress (0-1) + * @returns {string} Interpolated border path + * @private + */ +export function interpolateBorderPath(startD: string, endD: string, progress: number): string { + if (!startD || !endD) { + return endD || startD || ''; + } + + // Parse commands from both paths + const startCommands: PathCommand[] = parsePathCommands(startD); + const endCommands: PathCommand[] = parsePathCommands(endD); + + // If formats don't match or no commands, use standard interpolation + if (startCommands.length === 0 || endCommands.length === 0) { + return interpolatePathD(startD, endD, progress); + } + + // Generate interpolated path + let result: string = ''; + const maxPoints: number = Math.max(startCommands.length, endCommands.length); + + for (let i: number = 0; i < maxPoints; i++) { + // If we've run out of points in either path, use the last known point + const startCmd: PathCommand = i < startCommands.length ? startCommands[i as number] : startCommands[startCommands.length - 1]; + const endCmd: PathCommand = i < endCommands.length ? endCommands[i as number] : endCommands[endCommands.length - 1]; + + // Add command type (M or L) + result += startCmd.type + ' '; + + // Interpolate coordinates + const x: number = startCmd.coords[0] + (endCmd.coords[0] - startCmd.coords[0]) * progress; + const y: number = startCmd.coords[1] + (endCmd.coords[1] - startCmd.coords[1]) * progress; + result += `${x} ${y} `; + } + + return result; +} + +/** + * Renders an Area series for the chart. + * This module provides functions to generate SVG paths for the area fill and its border, + * handle animations for initial rendering and data updates, and integrate with markers. + */ +const AreaSeriesRenderer: AreaSeriesRendererType = { + getAreaPathDirection: ( + xValue: number, yValue: number, series: SeriesProperties, + isInverted: boolean, getPointLocation: Function, startPoint: ChartLocationProps | null, + startPath: string + ): string => { + let direction: string = ''; + let firstPoint: ChartLocationProps; + if (startPoint === null) { + firstPoint = getPointLocation(xValue, yValue, series.xAxis, series.yAxis, isInverted, series); + direction += (startPath + ' ' + (firstPoint.x) + ' ' + (firstPoint.y) + ' '); + } + return direction; + }, + + getAreaEmptyDirection: ( + firstPoint: ChartLocationProps, secondPoint: ChartLocationProps, series: SeriesProperties, + isInverted: boolean, getPointLocation: Function + ): string => { + let direction: string = ''; + direction += AreaSeriesRenderer.getAreaPathDirection( + firstPoint.x, firstPoint.y, series, isInverted, getPointLocation, null, + 'L' + ); + direction += AreaSeriesRenderer.getAreaPathDirection( + secondPoint.x, secondPoint.y, series, isInverted, getPointLocation, null, + 'L' + ); + return direction; + }, + + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: AnimationState | AreaSeriesAnimateState, + enableAnimation: boolean, + currentSeries: SeriesProperties + ): { + strokeDasharray: string | number; + strokeDashoffset: number; + interpolatedD?: string; + animatedDirection?: string; + animatedTransform?: string; + animatedClipPath?: string; + } => { + // Extract animation state + const { isInitialRenderRef, renderedPathDRef, animationProgress } = animationState; + if (!renderedPathDRef.current) { + renderedPathDRef.current = {}; + } + const isInitial: boolean = isInitialRenderRef.current[index as number]; + + // Get path data + const pathD: string = pathOptions.d as string; + const id: string = pathOptions.id ? pathOptions.id.toString() : ''; + const isBorder: boolean = id.includes('_border_'); + const idParts: string[] = id.split('_'); + const seriesIndexStr: string = idParts.length > 0 ? idParts[idParts.length - 1] : '0'; + const seriesIndex: number = parseInt(seriesIndexStr, 10); + + const storedKey: string = `${isBorder ? 'border' : 'area'}_${seriesIndex}`; + + if (enableAnimation) { + // For initial render animation + if (isInitial) { + if (animationProgress === 1) { + isInitialRenderRef.current[index as number] = false; + (renderedPathDRef as React.RefObject>).current[storedKey as string] = pathD; + } + + // Parse the path to find min/max X values + const commands: PathCommand[] = parsePathCommands(pathD); + const xCoords: number[] = commands + .filter((cmd: PathCommand) => cmd.type !== 'Z' && cmd.coords.length >= 2) + .map((cmd: PathCommand) => cmd.coords[0]); + + if (xCoords.length === 0) { + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0 + }; + } + + // Find min/max x values + const minX: number = Math.min(...xCoords); + const maxX: number = Math.max(...xCoords); + const range: number = maxX - minX; + + // Create a clip rect based on progress + const animWidth: number = range * animationProgress; + const isInverted: boolean = currentSeries.chart?.requireInvertedAxis; + const isXAxisInverse: boolean = currentSeries?.xAxis?.isAxisInverse; + const isYAxisInverse: boolean = currentSeries?.yAxis?.isAxisInverse; + let clipPathStr: string = ''; + + if (!isInverted) { + // Normal orientation - clip horizontally based on X values + const xCoordinates: number[] = xCoords.map((coord: number) => coord); + const minX: number = Math.min(...xCoordinates); + const maxX: number = Math.max(...xCoordinates); + const range: number = maxX - minX; + + if (isXAxisInverse) { + // X-axis is inverted - clip from right to left + const animWidth: number = range * animationProgress; + clipPathStr = `inset(0 0 0 ${range - animWidth}px)`; + } else { + clipPathStr = `inset(0 ${range - animWidth}px 0 0)`; + } + } else { + const yCoords: number[] = commands + .filter((cmd: PathCommand) => cmd.type !== 'Z' && cmd.coords.length >= 2) + .map((cmd: PathCommand) => cmd.coords[1]); // Get Y coordinates for transposed mode + + if (yCoords.length === 0) { + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0 + }; + } + + const minY: number = Math.min(...yCoords); + const maxY: number = Math.max(...yCoords); + const range: number = maxY - minY; + const animHeight: number = range * animationProgress; + + if (isYAxisInverse) { + // Y-axis inverted - clip from top to bottom + clipPathStr = `inset(${Math.max(0, range - animHeight)}px 0 0 0)`; + } else { + clipPathStr = `inset(${Math.max(0, range - animHeight)}px 0 0 0)`; + } + } + // Use CSS clip-path for both the area and border + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0, + animatedClipPath: clipPathStr + }; + } + + else if (pathD && (renderedPathDRef as React.RefObject>).current[storedKey as string]) { + const storedD: string = (renderedPathDRef as React.RefObject>).current[storedKey as string]; + + if (pathD !== storedD) { + let endPath: string = pathD; + const startPathCommands: string[] = storedD.match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/g) as string[]; + const endPathCommands: string[] = (pathD).match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/g) as string[]; + const maxLength: number = Math.max(startPathCommands.length, endPathCommands.length); + const minLength: number = Math.min(startPathCommands.length, endPathCommands.length); + if (startPathCommands.length > endPathCommands.length) { + for (let i: number = minLength; i < maxLength; i++) { + if (endPathCommands.length !== startPathCommands.length) { + if (currentSeries.removedPointIndex === currentSeries.points.length) { + if (endPathCommands.length === 1) { + endPathCommands.push(endPathCommands[endPathCommands.length - (isBorder ? 1 : 2)].replace('M', 'L')); + } else { + endPathCommands.splice(endPathCommands.length - 1, 0, + endPathCommands[endPathCommands.length - (isBorder ? 1 : 2)]); + } + } + else { + endPathCommands.splice(1, 0, endPathCommands[1] ? + (isBorder ? endPathCommands[0] : endPathCommands[1]) : endPathCommands[0]); + } + } + } + endPath = endPathCommands.join(''); + } + // Use specialized interpolation based on whether it's border or fill + const interpolatedD: string = isBorder ? + interpolateBorderPath(storedD, endPath, animationProgress) : + interpolateAreaPath(storedD, endPath, animationProgress); + + if (animationProgress === 1) { + (renderedPathDRef as React.RefObject>).current[storedKey as string] = pathD; + } + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0, + interpolatedD + }; + } + } + + // Update stored path when animation completes + if (animationProgress === 1) { + (renderedPathDRef as React.RefObject>).current[storedKey as string] = pathD; + } + } + + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0 + }; + }, + + render: (series: SeriesProperties, isInverted: boolean) => { + let startPoint: ChartLocationProps | null = null; + let direction: string = ''; + const origin: number = Math.max(series.yAxis.visibleRange.minimum, 0); + const getCoordinate: Function = getPoint; + const isDropMode: boolean = (series.emptyPointSettings && series.emptyPointSettings.mode === 'Drop') as boolean; + const borderWidth: number = series.border?.width as number; + const borderColor: string = series.border?.color ? series.border?.color : series.interior; + const visiblePoints: Points[] = lineBaseInstance.enableComplexProperty(series); + let point: Points; + // First point for completing the area path + let firstPointX: number = 0; + let firstPointY: number = 0; + let hasPoints: boolean = false; + + // First pass - collect area fill path directions + for (let i: number = 0; i < visiblePoints.length; i++) { + point = visiblePoints[i as number]; + const currentXValue: number = point.xValue as number; + point.symbolLocations = []; + point.regions = []; + + if (point.visible) { + hasPoints = true; + // Store first point for later closing the path + if (i === 0) { + const baselinePoint: ChartLocationProps = getCoordinate( + currentXValue, origin, series.xAxis, + series.yAxis, isInverted, series); + firstPointX = baselinePoint.x; + firstPointY = baselinePoint.y; + } + + // Area fill path + direction += AreaSeriesRenderer.getAreaPathDirection( + currentXValue, origin, series, isInverted, getCoordinate, startPoint, + startPoint === null ? 'M' : 'L' + ); + + startPoint = startPoint || { x: currentXValue, y: origin }; + direction += AreaSeriesRenderer.getAreaPathDirection( + currentXValue, point.yValue as number, series, isInverted, getCoordinate, null, + 'L' + ); + + // Handle empty points + if (visiblePoints[i + 1] && !visiblePoints[i + 1].visible && !isDropMode) { + direction += AreaSeriesRenderer.getAreaEmptyDirection( + { 'x': currentXValue, 'y': origin }, + startPoint, series, isInverted, getCoordinate + ); + startPoint = null; + } + + lineBaseInstance.storePointLocation(point, series, isInverted, getCoordinate); + } + } + series.visiblePoints = visiblePoints; + // Complete the area path by returning to the baseline and closing it + let finalDirection: string = ''; + + if (hasPoints) { + // Get last point + const lastPoint: Points = visiblePoints[visiblePoints.length - 1]; + const lastPointLoc: ChartLocationProps = getCoordinate( + lastPoint.xValue as number, origin, series.xAxis, series.yAxis, isInverted, series); + + // Create the final direction with proper closing of the path + finalDirection = direction; + + // Return to baseline + finalDirection += `L ${lastPointLoc.x} ${firstPointY} `; + + // Close the path by returning to the first point + finalDirection += `L ${firstPointX} ${firstPointY} `; + } + + // First, create the area fill path + const name: string = series.chart.element.id + '_Series_' + series.index; + const seriesOptions: RenderOptions = { + id: name, + fill: series.interior, + strokeWidth: 0, + stroke: 'transparent', + opacity: series.opacity, + dashArray: series.dashArray, + d: finalDirection + }; + + const options: RenderOptions[] = [seriesOptions]; + + // Then add the border path with the same timing + if (series.border?.width !== 0) { + const borderName: string = series.chart.element.id + '_Series_border_' + series.index; + const borderOptions: RenderOptions = { + id: borderName, + fill: 'transparent', + strokeWidth: borderWidth, + stroke: borderColor, + opacity: 1, + dashArray: series.border?.dashArray, + d: lineBaseInstance.removeEmptyPointsBorder(direction) // Use separate border path for top line only + }; + options.push(borderOptions); + } + const marker: ChartMarkerProps | null = series.marker?.visible ? MarkerRenderer.render(series) as Object : null; + return marker ? { options, marker } : options; + } +}; + +/** + * Parses SVG path commands into structured objects. + * + * @param {string} path - SVG path data string + * @returns {PathCommand[]} Array of structured command objects + * @private + */ +function parsePathCommands(path: string): PathCommand[] { + const result: PathCommand[] = []; + const commandRegex: RegExp = /([MLZ])([^MLZ]*)/g; + let match: RegExpExecArray | null; + + // eslint-disable-next-line no-constant-condition + while (true) { + match = commandRegex.exec(path); + if (match === null) { + break; + } + const [, type, coordsStr] = match; + const coords: number[] = coordsStr.trim().split(/\s+/).map(parseFloat).filter((n: number) => !isNaN(n)); + + result.push({ + type, + coords + }); + } + return result; +} +export default AreaSeriesRenderer; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/BarSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/BarSeriesRenderer.tsx new file mode 100644 index 0000000..485492b --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/BarSeriesRenderer.tsx @@ -0,0 +1,215 @@ +import { ChartMarkerProps } from '../../base/interfaces'; +import { DoubleRangeType, PointRenderingEvent, Points, Rect, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { useVisiblePoints } from '../../utils/helper'; +import { BarSeriesType, ColumnBase, ColumnBaseReturnType } from './ColumnBase'; +import { MarkerRenderer } from './MarkerRenderer'; +import { handleRectAnimation } from './SeriesAnimation'; + +const columnBaseInstance: ColumnBaseReturnType = ColumnBase(); + +/** + * Bar series renderer implementation for chart visualization + * Handles rendering of horizontal bar charts with animation support + */ +const BarSeries: BarSeriesType = { + sideBySideInfo: [] as DoubleRangeType[], + + /** + * Renders the bar series with optional markers. + * + * @param {SeriesProperties} series - Series configuration and data points + * @param {boolean} _isInverted - Chart inversion state (currently unused) + * @returns {RenderOptions[]|Object} Array of render options or object containing options and marker properties + */ + render: (series: SeriesProperties, _isInverted: boolean ): + RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps } => { + // Validate series and required properties + if (!series || !series.points || !Array.isArray(series.points)) { + return []; + } + + // Early exit if no points to process + if (series.points.length === 0) { + return []; + } + + BarSeries.sideBySideInfo[series.index] = columnBaseInstance.getSideBySideInfo(series); + const origin: number = Math.max(series.yAxis?.visibleRange?.minimum as number ?? 0, 0); + const options: RenderOptions[] = []; + + // Filter and process only valid points for better performance + const validPoints: Points[] = series.points.filter((point: Points) => { + return point && + point.xValue !== null && + point.xValue !== undefined && + point.yValue !== null && + point.yValue !== undefined && + point.visible; + }); + + // Early exit if no valid points found + if (validPoints.length === 0) { + return []; + } + + for (const pointBar of validPoints) { + const result: RenderOptions | undefined = BarSeries.renderPoint( + series, + pointBar, + BarSeries.sideBySideInfo[series.index], + origin + ); + + // Only add valid render options + if (result && typeof result === 'object') { + options.push(result); + } + + } + + series.visiblePoints = useVisiblePoints(series); + const marker: ChartMarkerProps | null = series.marker?.visible ? MarkerRenderer.render(series) as Object : null; + return marker ? { options, marker } : options; + }, + + /** + * Renders a single point in the bar series. + * + * @param {SeriesProperties} series - The series properties containing styling and configuration + * @param {Points} pointBar - Individual data point to render + * @param {DoubleRangeType} sideBySideInfo - Information about positioning for multiple series + * @param {number} origin - The origin point for bar positioning + * @returns {RenderOptions} Render options for the point or undefined if point is not visible or rendering is cancelled + */ + renderPoint: (series: SeriesProperties, pointBar: Points, sideBySideInfo: DoubleRangeType + , origin: number): RenderOptions | undefined => { + // Early exit for invalid parameters + if (!series || !pointBar || !sideBySideInfo) { + return undefined; + } + pointBar.symbolLocations = []; + pointBar.regions = []; + + if (pointBar.xValue === null || pointBar.xValue === undefined || + pointBar.yValue === null || pointBar.yValue === undefined) { + return undefined; + } + + // Early exit if point is not visible + if (!pointBar.visible) { + return undefined; + } + + if (!columnBaseInstance || typeof columnBaseInstance.getRectangle !== 'function') { + return undefined; + } + + const rect: Rect = columnBaseInstance.getRectangle( + Number(pointBar.xValue) + sideBySideInfo.start, + Number(pointBar.yValue), + Number(pointBar.xValue) + sideBySideInfo.end, + origin, + series + ); + + // Early exit if rect creation failed + if (!rect || typeof rect !== 'object') { + return undefined; + } + + // Apply column width adjustments when custom column width is specified + if (series.columnWidthInPixel && typeof series.columnWidthInPixel === 'number') { + /** + * Column height calculation for bar series: + * Calculate spacing adjustment based on side-by-side placement settings + */ + let spacingAdjustment: number = 0; + if (series.chart?.enableSideBySidePlacement && series.columnSpacing) { + spacingAdjustment = series.columnWidthInPixel * (series.columnSpacing ?? 0); + } + rect.height = series.columnWidthInPixel - spacingAdjustment; + + /** + * Column Y-position calculation for multiple series alignment: + * Break down the complex positioning logic into clear steps + */ + const rectCount: number = Number(series.rectCount) || 0; + const seriesIndex: number = Number(series.index) || 0; + + // Calculate the center offset for all series combined + const totalSeriesHeight: number = (series.columnWidthInPixel / 2) * rectCount; + + // Calculate the offset for the current series + const currentSeriesOffset: number = series.columnWidthInPixel * seriesIndex; + + // Apply the positioning: move up by total height, then down by current series offset + const yPositionAdjustment: number = totalSeriesHeight - currentSeriesOffset; + rect.y = rect.y - yPositionAdjustment; + } + + // Early exit if event triggering method is not available + if (!columnBaseInstance.triggerEvent || typeof columnBaseInstance.triggerEvent !== 'function') { + return undefined; + } + + const argsData: PointRenderingEvent = columnBaseInstance.triggerEvent( + series, + pointBar, + series.interior, + { + width: series.border?.width ?? 0, + color: series.border?.color ?? 'transparent' + } + ); + + // Early exit if required methods are not available + if (!columnBaseInstance.updateSymbolLocation || typeof columnBaseInstance.updateSymbolLocation !== 'function' || + !columnBaseInstance.drawRectangle || typeof columnBaseInstance.drawRectangle !== 'function') { + return undefined; + } + + columnBaseInstance.updateSymbolLocation(pointBar, { + x: rect.x ?? 0, + y: rect.y ?? 0, + width: rect.width ?? 0, + height: rect.height ?? 0 + }, series); + + // Generate unique name for the rendered element + const chartId: string = series.chart?.element?.id ?? 'chart'; + const seriesIndex: number = series.index ?? 0; + const pointIndex: number = pointBar.index ?? 0; + const name: string = `${chartId}_Series_${seriesIndex}_Point_${pointIndex}`; + + return columnBaseInstance.drawRectangle(series, pointBar, rect, argsData, name); + }, + + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + currentSeries: SeriesProperties, + currentPoint: Points | undefined, + pointIndex: number + ) => { + const animatedvalues: { animatedDirection?: string; animatedTransform?: string; } + = handleRectAnimation(pathOptions, currentSeries, index, currentPoint, pointIndex, animationState, enableAnimation); + return { + strokeDasharray: 'none', + strokeDashoffset: 0, + interpolatedD: undefined, + animatedDirection: animatedvalues.animatedDirection, + animatedTransform: animatedvalues.animatedTransform + }; + } +}; + +export default BarSeries; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/BubbleSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/BubbleSeriesRenderer.tsx new file mode 100644 index 0000000..c767713 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/BubbleSeriesRenderer.tsx @@ -0,0 +1,231 @@ + +import { ChartLocationProps, EmptyPointSettings } from '../../base/interfaces'; +import { drawSymbol, getPoint, withInRange } from '../../utils/helper'; +import { BubbleSeriesType, MarkerElementData, MarkerOptions, MarkerProperties, PointRenderingEvent, Points, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { markerAnimate } from './MarkerRenderer'; + +/** + * The `BubbleSeries` module is used to render the bubble series. + */ +const BubbleSeries: BubbleSeriesType = { + /** + * Renders the Bubble series. + * + * @param {SeriesProperties} series - The series to be rendered. + * @param {boolean} isInverted - Specifies whether the chart is inverted. + * @returns {Object} Returns the final series with assigned data point properties. + */ + render: (series: SeriesProperties, isInverted: boolean): + { options: RenderOptions[]; marker: MarkerProperties } => { + series.isRectSeries = false; + + const visiblePoints: Points[] = series.points; + + const chartAreaLength: number = Math.max( + series.chart.chartAxislayout.initialClipRect.height, + series.chart.chartAxislayout.initialClipRect.width + ); + const percentChange: number = chartAreaLength / 100; + let maxRadius: number = (series.maxRadius as number) * percentChange; + let minRadius: number = (series.minRadius as number) * percentChange; + let maxSize: number | null = series.sizeMax ?? null; + + if (series.maxRadius == null || series.minRadius == null) { + for (const value of series.chart.visibleSeries) { + if (value.type === 'Bubble' && value.visible === true && (value.maxRadius === null || value.minRadius === null)) { + maxSize = value.sizeMax > maxSize ? value.sizeMax : maxSize; + } + } + minRadius = maxRadius = 1; + } + const radius: number = maxRadius - minRadius; + const markerOptionsList: MarkerOptions[] = []; + + const emptyPointFillColor: string = (series.emptyPointSettings as EmptyPointSettings).fill + || series.interior; + const emptyPointBorderColor: string = (series.emptyPointSettings as EmptyPointSettings).border?.color + || series.border?.color || series.interior; + const emptyPointBorderWidth: number = (series.emptyPointSettings as EmptyPointSettings).border?.width + ?? series.border?.width as number; + + const maxValue: number = (chartAreaLength / 5) / 2; + + for (const bubblePoint of visiblePoints) { + bubblePoint.symbolLocations = []; + bubblePoint.regions = []; + + const isEmptyPoint: boolean = bubblePoint.isEmpty || bubblePoint.yValue === null; + + if ( + bubblePoint.visible && + withInRange( + visiblePoints[bubblePoint.index - 1], + bubblePoint, + visiblePoints[bubblePoint.index + 1], + series + ) + ) { + let bubbleRadius: number; + if (series.maxRadius == null || series.minRadius == null) { + minRadius = maxRadius = 1; + const calculatedRadius: number = maxValue; + bubbleRadius = maxSize && bubblePoint.size ? + calculatedRadius * Math.abs(+bubblePoint.size / maxSize) : + minRadius; + } else { + const sizeValue: number = bubblePoint?.size as number; + bubbleRadius = minRadius + (radius) * Math.abs(+sizeValue / (maxSize)); + } + bubbleRadius = bubbleRadius || minRadius; + const yValue: number = bubblePoint?.yValue as number; + const location: ChartLocationProps = getPoint( + bubblePoint?.xValue as number, + yValue, + series?.xAxis, + series?.yAxis, + isInverted + ); + bubblePoint.symbolLocations.push(location); + const pointId: string = `${series.chart.element.id}_Series_${series.index}_Point_${bubblePoint.index}`; + let fillColor: string = isEmptyPoint ? emptyPointFillColor : (bubblePoint.interior || series.interior); + let strokeColor: string = isEmptyPoint ? emptyPointBorderColor : series.border?.color || series.fill as string; + let pointWidth: number = bubbleRadius * 2; + let pointHeight: number = bubbleRadius * 2; + let pointBorderWidth: number = isEmptyPoint ? emptyPointBorderWidth : series.border?.width as number; + + const pointRenderArgs: PointRenderingEvent = { + cancel: false, + seriesName: series.name as string, + point: bubblePoint, + fill: fillColor, + border: { + color: strokeColor, + width: pointBorderWidth + }, + markerWidth: pointWidth, + markerHeight: pointHeight + }; + + // Create region for bubble point tooltip + bubblePoint.regions.push({ + x: bubblePoint.symbolLocations[0].x - bubbleRadius, + y: bubblePoint.symbolLocations[0].y - bubbleRadius, + width: 2 * bubbleRadius, + height: 2 * bubbleRadius + }); + + bubblePoint.color = pointRenderArgs.fill as string; + fillColor = pointRenderArgs.fill as string; + strokeColor = pointRenderArgs.border.color as string; + pointBorderWidth = pointRenderArgs.border.width as number; + pointWidth = pointRenderArgs.markerWidth as number; + pointHeight = pointRenderArgs.markerHeight as number; + const shapeOpts: Element = drawSymbol( + location, + 'Circle', + { width: pointWidth, height: pointHeight }, + series.marker?.imageUrl as string, + { + fill: fillColor, + stroke: strokeColor, + id: pointId, + strokeWidth: pointBorderWidth, + strokeDasharray: series.border?.dashArray as string, + opacity: series.opacity as number, + d: '' + } + ); + + bubblePoint.marker = { + border: { + color: strokeColor, + width: pointBorderWidth, + dashArray: series.border?.dashArray as string + }, + fill: fillColor, + height: pointHeight, + width: pointWidth, + visible: true, + shape: 'Circle' + }; + + const markerOption: MarkerOptions = { + ...shapeOpts, + shape: 'Circle', + cx: location.x, + cy: location.y, + rx: bubbleRadius, + ry: bubbleRadius, + isEmptyPoint: isEmptyPoint, + fill: fillColor, + stroke: strokeColor, + strokeWidth: pointBorderWidth as number, + border: { + color: strokeColor, + width: pointBorderWidth, + dashArray: series.border?.dashArray as string + }, + opacity: series.opacity as number + }; + markerOptionsList.push(markerOption); + } + } + const pathOptions: RenderOptions[] = []; + + return { + options: pathOptions, + marker: { + markerOptionsList, + symbolGroup: { + id: `${series.chart.element.id}_Series_${series.index}_SymbolGroup`, + transform: `translate(${series.clipRect?.x}, ${series.clipRect?.y})` + } + } + }; + }, + + /** + * Animates the Bubble points. + * + * @param {SeriesProperties} series - Series which should be animated. + * @returns {Function} Returns the animated points. + */ + doAnimation: (series: SeriesProperties) => { + const duration: number = series.animation?.duration as number; + const delay: number = series.animation?.delay as number; + const rectElements: NodeList = series.seriesElement.childNodes; + let count: number = 1; + + for (const point of series.points) { + if (!(point.symbolLocations as ChartLocationProps[]).length || !rectElements[count as number]) { + continue; + } + + // Create a MarkerElementData object + const markerData: MarkerElementData = { + // For elliptical markers (most common in scatter charts) + rx: point.marker?.width ? point.marker.width / 2 : 5, + ry: point.marker?.height ? point.marker.height / 2 : 5, + + // For circular markers + r: point.marker?.width ? point.marker.width / 2 : 5, + + // Opacity + opacity: point.marker?.opacity !== undefined ? point.marker.opacity : 1, + + // Optional animation tracking data if needed + _animationData: { + originalX: point.symbolLocations?.[0]?.x, + originalY: point.symbolLocations?.[0]?.y, + targetX: point.symbolLocations?.[0]?.x, + targetY: point.symbolLocations?.[0]?.y + } + }; + + markerAnimate(markerData, delay as number, duration as number); + count++; + } + } +}; + +export default BubbleSeries; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/ColumnBase.tsx b/components/charts/src/chart/renderer/SeriesRenderer/ColumnBase.tsx new file mode 100644 index 0000000..40cd27c --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/ColumnBase.tsx @@ -0,0 +1,533 @@ +import { isNullOrUndefined } from '@syncfusion/react-base'; +import { ChartBorderProps, ChartLocationProps, ChartMarkerProps, ChartSeriesProps } from '../../base/interfaces'; +import { getPoint, getMinPointsDelta, findSeriesCollection, setPointColor, setBorderColor, StackValuesType } from '../../utils/helper'; +import { createDoubleRange } from '../AxesRenderer/AxisTypeRenderer/DoubleAxisRenderer'; +import { DoubleRangeType, PointRenderingEvent, Points, Rect, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; + +// Named constants to replace magic numbers +const DEFAULT_COLUMN_WIDTH: number = 0.7; // default column width ratio +const MAX_COLUMN_WIDTH: number = 1; // maximum column width +const HALF_POSITION_OFFSET: number = 0.5; // offset for centering position calculation + +/** + * Interface for Bar Series renderer functionality + * + * @interface BarSeriesType + * @description Defines the core structure for bar series rendering with support for + * side-by-side positioning, animation, and individual point rendering + * + */ +export interface BarSeriesType { + sideBySideInfo: DoubleRangeType[]; + render: Function, + doAnimation: Function, + renderPoint: Function +} + +/** + * Interface for Stacking Column Series renderer functionality + * + * @interface StackingColumnSeriesRendererType + * @description Defines the complete structure for stacking column series rendering with support for + * animation, side-by-side positioning, marker handling, and custom styling + * + */ +export interface StackingColumnSeriesRendererType { + sideBySideInfo: DoubleRangeType[]; + render: ( + series: SeriesProperties, + _isInverted: boolean + ) => RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }; + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + currentSeries: SeriesProperties, + currentPoint: Points | undefined, + pointIndex: number + ) => { + strokeDasharray: string; + strokeDashoffset: number; + interpolatedD: string | undefined; + animatedDirection: string | undefined; + animatedTransform: string | undefined; + }; + renderPoint: ( + series: SeriesProperties, + point: Points, + sideBySideInfo: DoubleRangeType, + stackedValue: StackValuesType + ) => RenderOptions | undefined; +} + +/** + * Interface for the StackingBarSeriesRenderer object + * @interface StackingBarSeriesRendererType + * @private + */ +export interface StackingBarSeriesRendererType { + /** Array to store side-by-side positioning information for multiple series */ + sideBySideInfo: DoubleRangeType[]; + + /** + * Renders the stacking bar series with all its points. + * + * @param {SeriesProperties} series - Series configuration and data points + * @param {boolean} _isInverted - Chart inversion state (currently unused) + * @param {Object} chartProps - Chart-level properties including event handlers + * @returns {RenderOptions[]|Object} Array of render options or object containing options and marker properties + */ + render: ( + series: SeriesProperties, + _isInverted: boolean + ) => RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }; + + /** + * Handles animation for the stacking bar series. + * + * @param {RenderOptions} pathOptions - Current render options for the path + * @param {number} index - Index of the current series + * @param {AnimationState} animationState - Complete animation state including refs and progress + * @param {boolean} enableAnimation - Flag to enable/disable animations + * @param {SeriesProperties} currentSeries - Series being animated + * @param {Points | undefined} currentPoint - Point being animated (optional) + * @param {number} pointIndex - Index of the current point + * @returns {Object} Animation properties object + */ + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + currentSeries: SeriesProperties, + currentPoint: Points | undefined, + pointIndex: number + ) => { + strokeDasharray: string; + strokeDashoffset: number; + interpolatedD: string | undefined; + animatedDirection: string | undefined; + animatedTransform: string | undefined; + }; + + /** + * Renders a single point in the stacking bar series. + * + * @param series - The series properties + * @param point - The individual point to render + * @param sideBySideInfo - Positioning information for side-by-side series + * @param stackedValue - Stacked values for the current series + * @returns Render options for the point or undefined if not rendered + */ + renderPoint: ( + series: SeriesProperties, + point: Points, + sideBySideInfo: DoubleRangeType, + stackedValue: StackValuesType + ) => RenderOptions | undefined; +} + +/** + * Interface defining the functions available in the ColumnBase utility. + * + * @private + */ +export type ColumnBaseReturnType = { + updateSymbolLocation: (point: Points, rect: Rect, series: SeriesProperties) => void; + getRectangle: (x1: number, y1: number, x2: number, y2: number, series: SeriesProperties) => Rect; + getSideBySideInfo: (series: SeriesProperties) => DoubleRangeType; + calculateRoundedRectPath: ( + rect: Rect, + topLeft: number, + topRight: number, + bottomLeft: number, + bottomRight: number + ) => string; + drawRectangle: (series: SeriesProperties, point: Points, rect: Rect, argsData: PointRenderingEvent, name: string) => RenderOptions; + triggerEvent: Function; +}; + +/** + * Helper function to find rectangle positions for series collection + * @param {SeriesProperties[]} seriesCollection - Collection of series properties + * @returns {void} + */ +const findRectPositionHelper: (seriesCollection: SeriesProperties[]) => void + = (seriesCollection: SeriesProperties[]): void => { + const groupingValues: Record = {}; + const verticalSeries: { rectCount: number; position: number | null } = { rectCount: 0, position: null }; + + for (let i: number = 0; i < seriesCollection.length; i++) { + const value: SeriesProperties = seriesCollection[i as number]; + if (value?.type?.indexOf('Stacking') !== -1 || value.groupName !== '') { + const groupName: string | undefined = value?.type?.indexOf('Stacking') !== -1 ? value.stackingGroup : value.type + value.groupName; + if (groupName) { + if (groupingValues[groupName as string] === undefined) { + value.position = verticalSeries.rectCount; + groupingValues[groupName as string] = verticalSeries.rectCount++; + } else { + value.position = groupingValues[groupName as string]; + } + } else { + if (verticalSeries.position === null) { + value.position = verticalSeries.rectCount; + verticalSeries.position = verticalSeries.rectCount++; + } else { + value.position = verticalSeries.position; + } + } + } else { + value.position = verticalSeries.rectCount++; + } + } + for (let i: number = 0; i < seriesCollection.length; i++) { + const value: SeriesProperties = seriesCollection[i as number]; + value.rectCount = verticalSeries.rectCount; + } + }; + +/** + * Helper function for getting region information for a data point + * @param {Points} point - The data point + * @param {Rect} rect - The rectangle bounds + * @param {SeriesProperties} series - The series properties + * @returns {void} + */ +const getRegionHelper: (point: Points, rect: Rect, series: SeriesProperties) => void + = (point: Points, rect: Rect, series: SeriesProperties): void => { + point.regions = point.regions || []; + point.symbolLocations = point.symbolLocations || []; + + if (point.y === 0) { + const markerWidth: number = (series.marker && series.marker.width) ? series.marker.width : 0; + const markerHeight: number = (series.marker && series.marker.height) ? series.marker.height : 0; + + point.regions.push({ + x: point.symbolLocations[0].x - markerWidth, + y: point.symbolLocations[0].y - markerHeight, + width: 2 * markerWidth, + height: 2 * markerHeight + }); + } else { + point.regions.push(rect); + } + }; + +/** + * Helper function for updating X region symbol location. + * + * @param {Points} point - The data point + * @param {Rect} rect - The rectangle bounds + * @param {SeriesProperties} series - The series properties + * @returns {void} + */ +const updateXRegionHelper: (point: Points, rect: Rect, series: SeriesProperties) => void + = (point: Points, rect: Rect, series: SeriesProperties): void => { + point.symbolLocations?.push({ + x: rect.x + (rect.width) / 2, + y: (series.seriesType === 'BoxPlot' || + (series.seriesType && series.seriesType.indexOf('HighLow') !== -1) || + (point.yValue !== undefined && (point.yValue ?? 0) >= 0 === !series.yAxis.isAxisInverse)) ? rect.y : (rect.y + rect.height) + }); + + getRegionHelper(point, rect, series); + }; + +/** + * Helper function for updating Y region symbol location + * @param {Points} point - The data point + * @param {Rect} rect - The rectangle bounds + * @param {SeriesProperties} series - The series properties + * @returns {void} + */ +const updateYRegionHelper: (point: Points, rect: Rect, series: SeriesProperties) => void + = (point: Points, rect: Rect, series: SeriesProperties): void => { + point.symbolLocations?.push({ + x: (series.seriesType === 'BoxPlot' || + (series.seriesType && series.seriesType.indexOf('HighLow') !== -1) || + (point.yValue !== undefined && (point.yValue ?? 0) >= 0 === !series.yAxis.isAxisInverse)) ? rect.x + rect.width : rect.x, + y: rect.y + rect.height / 2 + }); + + getRegionHelper(point, rect, series); + }; + +/** + * Provides base functionality for column and bar series types + * @returns {ColumnBaseReturnType} Object with column series rendering methods + * @private + */ +export function ColumnBase(): ColumnBaseReturnType { + + /** + * Gets the rectangle bounds based on two points. + * @param {number} x1 - The x-coordinate of the first point + * @param {number} y1 - The y-coordinate of the first point + * @param {number} x2 - The x-coordinate of the second point + * @param {number} y2 - The y-coordinate of the second point + * @param {SeriesProperties} series - The series associated with the rectangle + * @returns {Rect} The rectangle bounds + */ + const getRectangle: (x1: number, y1: number, x2: number, y2: number, series: SeriesProperties) => Rect + = (x1: number, y1: number, x2: number, y2: number, series: SeriesProperties): Rect => { + const point1: ChartLocationProps = getPoint(x1, y1, series.xAxis, series.yAxis, series.chart.requireInvertedAxis); + const point2: ChartLocationProps = getPoint(x2, y2, series.xAxis, series.yAxis, series.chart.requireInvertedAxis); + return { + x: Math.min(point1.x, point2.x), + y: Math.min(point1.y, point2.y), + width: Math.abs(point2.x - point1.x), + height: Math.abs(point2.y - point1.y) + }; + }; + + /** + * Updates symbol location for a data point based on chart orientation + * @param {Points} point - The data point + * @param {Rect} rect - The rectangle bounds + * @param {SeriesProperties} series - The series properties + * @returns {void} + */ + const updateSymbolLocation: (point: Points, rect: Rect, series: SeriesProperties) => void + = (point: Points, rect: Rect, series: SeriesProperties): void => { + if (!series.chart.requireInvertedAxis) { + updateXRegionHelper(point, rect, series); + } else { + updateYRegionHelper(point, rect, series); + } + }; + + /** + * Gets side-by-side positions for all series in the chart. + * + * @param {SeriesProperties} series - The series properties + * @returns {void} + */ + const getSideBySidePositions: (series: SeriesProperties) => void + = (series: SeriesProperties): void => { + const chart: SeriesProperties['chart'] = series.chart; + for (const columnItem of chart.columns) { + for (const item of chart.rows) { + findRectPositionHelper(findSeriesCollection(columnItem, item, false)); + } + } + }; + + /** + * Gets side-by-side information for column positioning. + * + * @param {SeriesProperties} series - The series properties + * @returns {DoubleRangeType} The range information for side-by-side positioning + */ + const getSideBySideInfo: (series: SeriesProperties) => DoubleRangeType + = (series: SeriesProperties): DoubleRangeType => { + series.isRectSeries = true; + + if ((series.chart.enableSideBySidePlacement && !series.position) || !isNullOrUndefined(series.columnWidthInPixel)) { + getSideBySidePositions(series); + } + + if (series.columnWidthInPixel) { + return createDoubleRange(0, 0); + } + + const position: number | undefined = !series.chart.enableSideBySidePlacement ? 0 : series.position; + const rectCount: number | undefined = !series.chart.enableSideBySidePlacement ? 1 : series.rectCount; + const visibleSeries: SeriesProperties[] = series.chart.visibleSeries; + const seriesSpacing: number = series.chart.enableSideBySidePlacement + ? (series.columnSpacing || 0) : 0; + const pointSpacing: number = (series.columnWidth === null || series.columnWidth === undefined || isNaN(+(series.columnWidth))) + ? DEFAULT_COLUMN_WIDTH : Math.min(series.columnWidth, MAX_COLUMN_WIDTH); + + const minimumPointDelta: number = getMinPointsDelta(series.xAxis, visibleSeries); + const width: number = minimumPointDelta * pointSpacing; + + const location: number = (position!) / rectCount! - HALF_POSITION_OFFSET; + let doubleRange: DoubleRangeType = createDoubleRange(location, location + (1 / rectCount!)); + + if (!(isNaN(doubleRange.start) || isNaN(doubleRange.end))) { + if (series.groupName && series?.type?.indexOf('Stacking') === -1) { + let mainColumnWidth: number = DEFAULT_COLUMN_WIDTH; + if (series.chart.visibleSeries) { + series.chart.visibleSeries.filter(function (series: ChartSeriesProps): void { + if ((series.columnWidth ?? 0) > mainColumnWidth) { + mainColumnWidth = series.columnWidth ?? 0; + } + }); + } + const mainWidth: number = minimumPointDelta * mainColumnWidth; + const mainDoubleRange: DoubleRangeType = createDoubleRange(doubleRange.start * mainWidth, doubleRange.end * mainWidth); + const difference: number = (mainDoubleRange.delta - (doubleRange.end * width - doubleRange.start * width)) / 2; + + doubleRange = createDoubleRange(mainDoubleRange.start + difference, mainDoubleRange.end - difference); + } else { + doubleRange = createDoubleRange(doubleRange.start * width, doubleRange.end * width); + } + + const radius: number = seriesSpacing * doubleRange.delta; + doubleRange = createDoubleRange(doubleRange.start + radius / 2, doubleRange.end - radius / 2); + } + + return doubleRange; + }; + + /** + * Calculates the SVG path for a rounded rectangle + * @param {Rect} rect - The rectangle bounds + * @param {number} topLeft - Top-left corner radius + * @param {number} topRight - Top-right corner radius + * @param {number} bottomLeft - Bottom-left corner radius + * @param {number} bottomRight - Bottom-right corner radius + * @param {boolean} [inverted=false] - Whether the chart is inverted + * @returns {string} SVG path string for the rounded rectangle + */ + const calculateRoundedRectPath: ( + rect: Rect, + topLeft: number, + topRight: number, + bottomLeft: number, + bottomRight: number + ) => string = ( + rect: Rect, + topLeft: number, + topRight: number, + bottomLeft: number, + bottomRight: number + ): string => { + const halfWidth: number = rect.width / 2; + const halfHeight: number = rect.height / 2; + topLeft = Math.min(topLeft, halfWidth, halfHeight); + topRight = Math.min(topRight, halfWidth, halfHeight); + bottomLeft = Math.min(bottomLeft, halfWidth, halfHeight); + bottomRight = Math.min(bottomRight, halfWidth, halfHeight); + + return 'M' + ' ' + rect.x + ' ' + (topLeft + rect.y) + + ' Q ' + rect.x + ' ' + rect.y + ' ' + (rect.x + topLeft) + ' ' + + rect.y + ' ' + 'L' + ' ' + (rect.x + rect.width - topRight) + ' ' + rect.y + + ' Q ' + (rect.x + rect.width) + ' ' + rect.y + ' ' + + (rect.x + rect.width) + ' ' + (rect.y + topRight) + ' ' + 'L ' + + (rect.x + rect.width) + ' ' + (rect.y + rect.height - bottomRight) + + ' Q ' + (rect.x + rect.width) + ' ' + (rect.y + rect.height) + ' ' + + (rect.x + rect.width - bottomRight) + ' ' + (rect.y + rect.height) + + ' ' + 'L ' + (rect.x + bottomLeft) + ' ' + (rect.y + rect.height) + + ' Q ' + rect.x + ' ' + (rect.y + rect.height) + ' ' + rect.x + ' ' + + (rect.y + rect.height - bottomLeft) + ' ' + 'L' + ' ' + rect.x + ' ' + + (topLeft + rect.y) + ' ' + 'Z'; + }; + + /** + * Draws a rectangle for a data point in a chart series. + * + * @param {SeriesProperties} series - The series object containing the data point + * @param {Points} point - The data point to be rendered + * @param {Rect} rect - The rectangle dimensions for the data point + * @param {PointRenderingEvent} argsData - Arguments for rendering including styling properties + * @param {string} name - The identifier for the rendered element + * @returns {RenderOptions} Object containing SVG path properties including id, fill, stroke, etc. + */ + const drawRectangle: ( + series: SeriesProperties, + point: Points, + rect: Rect, + argsData: PointRenderingEvent, + name: string + ) => RenderOptions = ( + series: SeriesProperties, + point: Points, + rect: Rect, + argsData: PointRenderingEvent, + name: string + ): RenderOptions => { + const chart: SeriesProperties['chart'] = series.chart; + const check: number = chart.requireInvertedAxis ? rect.height : rect.width; + + if (check <= 0) { + return {} as RenderOptions; + } + + let direction: string; + if (point.y === 0) { + // For 0 values corner radius will not calculate + direction = calculateRoundedRectPath(rect, 0, 0, 0, 0); + } else { + let topLeft: number; + let topRight: number; + let bottomLeft: number; + let bottomRight: number; + const isNegative: boolean = (point.y as number) < 0; + + if (chart.requireInvertedAxis) { + topLeft = isNegative ? argsData.cornerRadius?.topRight ?? 0 : argsData.cornerRadius?.topLeft ?? 0; + topRight = isNegative ? argsData.cornerRadius?.topLeft ?? 0 : argsData.cornerRadius?.topRight ?? 0; + bottomLeft = isNegative ? argsData.cornerRadius?.bottomRight ?? 0 : argsData.cornerRadius?.bottomLeft ?? 0; + bottomRight = isNegative ? argsData.cornerRadius?.bottomLeft ?? 0 : argsData.cornerRadius?.bottomRight ?? 0; + } else { + topLeft = isNegative ? argsData.cornerRadius?.bottomLeft ?? 0 : argsData.cornerRadius?.topLeft ?? 0; + topRight = isNegative ? argsData.cornerRadius?.bottomRight ?? 0 : argsData.cornerRadius?.topRight ?? 0; + bottomLeft = isNegative ? argsData.cornerRadius?.topLeft ?? 0 : argsData.cornerRadius?.bottomLeft ?? 0; + bottomRight = isNegative ? argsData.cornerRadius?.topRight ?? 0 : argsData.cornerRadius?.bottomRight ?? 0; + } + + direction = calculateRoundedRectPath(rect, topLeft, topRight, bottomLeft, bottomRight); + } + + return { + id: name, + fill: argsData.fill, + strokeWidth: argsData.border?.width, + stroke: argsData.border?.color, + opacity: series.opacity, + dashArray: series.border?.dashArray || '', + d: direction + }; + }; + + /** + * Triggers the point render event. + * + * @param {SeriesProperties} series - The series associated with the point + * @param {Points} point - The data point for which the event is triggered + * @param {string} fill - The fill color of the point + * @param {ChartBorderProps} border - The border settings of the point + * @returns {PointRenderingEvent} The event arguments + */ + const triggerEvent: (series: SeriesProperties, point: Points, fill: string, border: ChartBorderProps) + => PointRenderingEvent = (series: SeriesProperties, point: Points, fill: string, border: ChartBorderProps): PointRenderingEvent => { + const argsData: PointRenderingEvent = { + cancel: false, + seriesName: series.name as string, + point: point, + fill: setPointColor(point, fill), + border: setBorderColor(point, border), + cornerRadius: series.cornerRadius + }; + point.color = argsData.fill; + return argsData; + }; + + return { + updateSymbolLocation, + getRectangle, + getSideBySideInfo, + calculateRoundedRectPath, + drawRectangle, + triggerEvent + }; +} + +// Using only named export for consistency +export { ColumnBase as default }; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/ColumnSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/ColumnSeriesRenderer.tsx new file mode 100644 index 0000000..84cb349 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/ColumnSeriesRenderer.tsx @@ -0,0 +1,287 @@ +import { ChartMarkerProps } from '../../base/interfaces'; +import { DoubleRangeType, PointRenderingEvent, Points, Rect, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { useVisiblePoints } from '../../utils/helper'; +import { ColumnBase, ColumnBaseReturnType } from './ColumnBase'; +import MarkerRenderer from './MarkerRenderer'; +import { handleRectAnimation } from './SeriesAnimation'; + +const columnBaseInstance: ColumnBaseReturnType = ColumnBase(); + +/** + * Animation state interface for column series animations + */ +interface AnimationState { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; +} + +/** + * Result interface for animation operations + */ +interface AnimationResult { + strokeDasharray: string; + strokeDashoffset: number; + interpolatedD: string | undefined; + animatedDirection?: string; + animatedTransform?: string; +} + +/** + * Result interface for render operations - ensures consistent return type + */ +interface RenderResult { + options: RenderOptions[]; + marker?: ChartMarkerProps; +} + +/** + * Interface defining the structure and methods for column series rendering + * Used for rendering column charts with proper positioning, animations, and markers + * + * @interface ColumnSeriesType + * + */ +interface ColumnSeriesType { + /** Array storing side-by-side positioning information for multiple series */ + sideBySideInfo: DoubleRangeType[]; + + /** + * Main render function for column series. + * + * @param {SeriesProperties} series - Series configuration and data points + * @param {boolean} _isInverted - Chart inversion state (currently unused) + * @param {Object} chartProps - Chart-level properties including event handlers + * @returns {RenderOptions[]|Object} Array of render options or object containing options and marker properties + */ + render: ( + series: SeriesProperties, + isInverted: boolean + ) => RenderResult | RenderOptions[]; + + /** + * Animation handler for column series. + * + * @param {RenderOptions} pathOptions - Current render options for the path + * @param {number} index - Index of the current series + * @param {AnimationState} animationState - Complete animation state including refs and progress + * @param {boolean} enableAnimation - Flag to enable/disable animations + * @param {SeriesProperties} currentSeries - Series being animated + * @param {Points | undefined} currentPoint - Point being animated (optional) + * @param {number} pointIndex - Index of the current point + * @returns {Object} Animation result with transform and direction properties + */ + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: AnimationState, + enableAnimation: boolean, + currentSeries: SeriesProperties, + currentPoint: Points | undefined, + pointIndex: number + ) => AnimationResult; + + /** + * Renders individual point as a column rectangle. + * + * @param series - Series properties + * @param point - Individual data point to render + * @param sideBySideInfo - Positioning information for side-by-side placement + * @param origin - Origin point for the column base + * @returns Render options for the point or undefined if not visible + */ + renderPoint: ( + series: SeriesProperties, + point: Points, + sideBySideInfo: DoubleRangeType, + origin: number + ) => RenderOptions | undefined; +} + +/** + * Column Series Renderer + * + * Handles rendering of column chart series including: + * - Side-by-side positioning for multiple series + * - Individual point rendering as rectangles + * - Animation support + * - Marker rendering + * + */ +const ColumnSeries: ColumnSeriesType = { + sideBySideInfo: [] as DoubleRangeType[], + + /** + * Renders the complete column series + * + * Processes all points in the series, calculates side-by-side positioning, + * and optionally renders markers. Uses memoized calculations to avoid + * expensive recomputations on each render. + * + * @param {SeriesProperties} series - Series configuration and data points + * @param {boolean} _isInverted - Chart inversion state (currently unused) + * @returns {RenderOptions[]|Object} Array of render options, or object containing both options array and marker properties when markers are visible + */ + render: ( + series: SeriesProperties, + _isInverted: boolean + ): RenderResult => { + // Cache side-by-side info to avoid repeated calculations + ColumnSeries.sideBySideInfo[series.index] = columnBaseInstance.getSideBySideInfo(series); + + const origin: number = Math.max(series.yAxis.visibleRange.minimum as number, 0); + const options: RenderOptions[] = []; + + // Process each point in the series + for (const point of series.points) { + const result: RenderOptions | undefined = ColumnSeries.renderPoint( + series, + point, + ColumnSeries.sideBySideInfo[series.index], + origin + ); + + if (result) { + options.push(result); + } + } + + // Update visible points using optimized helper + series.visiblePoints = useVisiblePoints(series); + + // Render marker if visible + const marker: ChartMarkerProps | undefined = series.marker?.visible + ? MarkerRenderer.render(series) as ChartMarkerProps + : undefined; + + // Return consistent interface + return { options, marker }; + }, + + /** + * Renders an individual data point as a column rectangle + * + * Calculates the rectangle bounds, applies column width settings, + * triggers point render events, and creates the visual representation. + * + * @param {SeriesProperties} series - Series containing styling and configuration + * @param {Points} point - Individual data point with x,y values + * @param {DoubleRangeType} sideBySideInfo - Calculated positioning for multiple series + * @param {number} origin - Base line for column height calculation + * @returns {RenderOptions} Render options for the rectangle or undefined if point is hidden + * + */ + renderPoint: ( + series: SeriesProperties, + point: Points, + sideBySideInfo: DoubleRangeType, + origin: number + ): RenderOptions | undefined => { + // Initialize point properties + point.symbolLocations = []; + point.regions = []; + + if (!point.visible) { + return undefined; + } + + // Calculate base rectangle bounds + const rect: Rect = columnBaseInstance.getRectangle( + (point.xValue || 0) + sideBySideInfo.start, + point.yValue ?? 0, + (point.xValue || 0) + sideBySideInfo.end, + origin, + series + ); + + if (series.columnWidthInPixel) { + const spacingReduction: number = series.chart.enableSideBySidePlacement + ? series.columnWidthInPixel * (series.columnSpacing ?? 0) + : 0; + + rect.width = series.columnWidthInPixel - spacingReduction; + + const offsetCalculation: number = ((series.columnWidthInPixel / 2) * Number(series.rectCount)) + - (series.columnWidthInPixel * series.index); + + rect.x = rect.x - offsetCalculation; + } + + // Trigger point render event for customization + const argsData: PointRenderingEvent = columnBaseInstance.triggerEvent( + series, + point, + series.interior, + { + width: series.border?.width, + color: series.border?.color + } + ); + + // Update symbol location for interactions + columnBaseInstance.updateSymbolLocation(point, { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height + }, series); + + // Generate unique identifier for the rendered element + const name: string = `${series.chart.element.id}_Series_${series.index}_Point_${point.index}`; + + return columnBaseInstance.drawRectangle(series, point, rect, argsData, name); + + }, + + /** + * Handles animation for column series rendering + * + * Processes animation state and applies appropriate transforms for + * smooth column animations including height and position changes. + * + * @param {RenderOptions} pathOptions - Current render options for the path + * @param {number} index - Index of the current series + * @param {AnimationState} animationState - Complete animation state including refs and progress + * @param {boolean} enableAnimation - Flag to enable/disable animations + * @param {SeriesProperties} currentSeries - Series being animated + * @param {Points | undefined} currentPoint - Point being animated (optional) + * @param {number} pointIndex - Index of the current point + * @returns {Object} Animation configuration with transforms and directions + * + */ + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: AnimationState, + enableAnimation: boolean, + currentSeries: SeriesProperties, + currentPoint: Points | undefined, + pointIndex: number + ): AnimationResult => { + // Handle rectangle-specific animations + const animatedValues: { animatedDirection?: string; animatedTransform?: string; } = + handleRectAnimation( + pathOptions, + currentSeries, + index, + currentPoint, + pointIndex, + animationState, + enableAnimation + ); + + // Return standardized animation result + return { + strokeDasharray: 'none', + strokeDashoffset: 0, + interpolatedD: undefined, + animatedDirection: animatedValues.animatedDirection, + animatedTransform: animatedValues.animatedTransform + }; + } +}; + +export default ColumnSeries; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/DataLabelRender.tsx b/components/charts/src/chart/renderer/SeriesRenderer/DataLabelRender.tsx new file mode 100644 index 0000000..9f7bc95 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/DataLabelRender.tsx @@ -0,0 +1,1198 @@ +import { ChartBorderProps, ChartFontProps, ChartLocationProps, DataLabelContentFunction } from '../../base/interfaces'; +import { calculateRect, getDataLabelText, getPosition, getRectanglePoints, getVisiblePoints, isCollide, isDataLabelOverlapWithChartBound, isRotatedRectIntersect, measureText, rotateTextSize, textElement } from '../../utils/helper'; +import { LabelPosition } from '../../base/enum'; +import { LayoutMap } from '../../layout/LayoutContext'; +import { Chart, ColorValue, dataLabelOptions, DataLabelProperties, DataLabelRendererResult, LabelLocation, MarginModel, Points, Rect, SeriesProperties, ChartSizeProps, TextOption, TextStyleModel, DataLabelContentProps } from '../../chart-area/chart-interfaces'; +import { HorizontalAlignment } from '@syncfusion/react-base'; + + +/** + * Interpolates between two values with easing applied + * + * @param {number} start - Starting value + * @param {number} end - Ending value + * @param {number} progress - Raw animation progress (0-1) + * @returns {number} Interpolated value with easing applied + * @private + */ +function interpolateWithEasing(start: number, end: number, progress: number): number { + return start + (end - start) * progress; +} + +/** + * Converts a color name to its hexadecimal or RGB representation. + * + * @param {string} colorName - The name of the color to convert. + * @returns {string} - The hexadecimal or RGB representation of the color. + * @private + */ +export function colorNameToHex(colorName: string): string { + // Return the color as is if it's already in hex format or rgba/rgb format + // Using simpler, safer regex patterns to check color formats + if (/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/.test(colorName)) { + return colorName; + } + // Separate check for rgba/rgb to avoid complex regex + if (/^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/.test(colorName) || + /^rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*(?:0|0\.[0-9]{1,3}|1|1\.0{1,3})\s*\)$/.test(colorName)) { + return colorName; + } + + const element: HTMLDivElement = document.createElement('div'); + element.style.color = colorName; + document.body.appendChild(element); + const computedColor: string = window.getComputedStyle(element).color; + document.body.removeChild(element); + + return computedColor || colorName; +} + +/** + * Converts a hex color string or rgb/rgba string to RGB color components. + * + * @param {string} color - The hexadecimal or rgb/rgba color string. + * @returns {ColorValue} - An object with r, g, b values. + * @private + */ +export function convertHexToColor(color: string): ColorValue { + let r: number = 0; + let g: number = 0; + let b: number = 0; + + if (color.startsWith('rgb')) { + + const cleanColor: string = color.replace(/[rgba()\s]/g, ''); + const parts : string[] = cleanColor.split(','); + + if (parts.length >= 3) { + r = parseInt(parts[0], 10) || 0; + g = parseInt(parts[1], 10) || 0; + b = parseInt(parts[2], 10) || 0; + return { r, g, b }; + } + } + + // Remove the hash (#) if present + color = color.replace('#', ''); + + // Handle shorthand hex format (e.g., #CCC) + if (color.length === 3) { + r = parseInt(color.charAt(0) + color.charAt(0), 16); + g = parseInt(color.charAt(1) + color.charAt(1), 16); + b = parseInt(color.charAt(2) + color.charAt(2), 16); + } else { + r = parseInt(color.substring(0, 2), 16) || 0; + g = parseInt(color.substring(2, 4), 16) || 0; + b = parseInt(color.substring(4, 6), 16) || 0; + } + + return { r, g, b }; +} + +// Define interfaces for the renderer return types +interface ShapeRectConfig { + id: string; + fill: string; + border: ChartBorderProps; + opacity: number; + rect: Rect; + rx: number; + ry: number; + transform: string; + stroke: string | undefined; +} +/** + * Defines the DataLabelRenderer interface for rendering data labels in charts. + */ +interface IDataLabelRenderer { + /** + * Renders all data labels for the given series. + * + * @param series The series to render labels for. + * @param dataLabel Chart data label settings. + * @param chartProps Chart-level event props/context. + * @returns Array of DataLabelRendererResults representing each data label. + */ + render: (series: SeriesProperties, dataLabel: DataLabelProperties, + ) => ( DataLabelRendererResults[] | DataLabelRendererResult[]); + /** + * Renders an individual data label for a point. + * + * @param series The parent series. + * @param point The data point to render the label for. + * @param dataLabel Chart data label settings. + * @param chartProps Chart-level event props/context. + * @returns DataLabelRendererResult for the label. + */ + renderDataLabel: (series: SeriesProperties, point: Points, dataLabel: DataLabelProperties) => + DataLabelRendererResults; + /** + * Determines if a data label should have a shape (background/stroke) and sets flag on dataLabel. + * + * @param style Label style (color and border). + * @param dataLabel Chart data label props to update. + */ + isDataLabelShape: (style: { color: string, border: ChartBorderProps }, dataLabel: DataLabelProperties) => void; + /** + * Calculates the rectangle region for a data label's text. + * + * @param point The data point. + * @param series The parent series. + * @param textSize The size of the label text. + * @param dataLabel Chart data label settings. + * @param labelIndex Label index if multiple per point. + * @returns Rectangle where text should be rendered. + */ + calculateTextPosition: (point: Points, series: SeriesProperties, textSize: ChartSizeProps, + dataLabel: DataLabelProperties, labelIndex: number) => Rect; + /** + * Gets the anchor location (x, y) for a data label. + * + * @param point The data point to label. + * @param series Parent series. + * @param labelIndex Label index (for multi-line/or multi-label scenarios). + * @returns ChartLocationProps where label should be anchored. + */ + getLabelLocation: (point: Points, series: SeriesProperties, labelIndex: number) => ChartLocationProps; + /** + * Calculates label alignment offset. + * + * @param value Offset value (height/width, etc). + * @param labelLocation Original label location. + * @param alignment Alignment mode (Near/Far). + * @param isMinus Is value negative. + * @param isInverted Is chart axis inverted. + * @returns New label location with alignment applied. + */ + calculateAlignment: (value: number, labelLocation: number, + alignment: HorizontalAlignment, isMinus: boolean, isInverted: boolean) => number; + /** + * Calculates the y position for path-based series (Line/Spline, etc). + * + * @param labelLocation Original y position. + * @param position Label position (Top/Bottom/Auto). + * @param size Text size. + * @param dataLabel Data label settings. + * @param series Parent series. + * @param point The point data. + * @param labelIndex Label index. + * @returns Calculated label y. + */ + calculatePathPosition: (labelLocation: number, position: LabelPosition, size: ChartSizeProps, dataLabel: DataLabelProperties + , series: SeriesProperties, point: Points, labelIndex: number) => number; + /** + * Chooses the best actual y position to avoid data label overlap for path-based series. + * + * @param y Original y position. + * @param series Parent series. + * @param point Point data. + * @param size Label text size. + * @param labelIndex Label index. + * @returns Y position to avoid overlap. + */ + calculatePathActualPosition: (y: number, series: SeriesProperties, point: Points, + size: ChartSizeProps, labelIndex: number, dataLabel: DataLabelProperties) => number; + /** + * Calculates the label's position for rectangle/bar/column series. + * + * @param labelLocation Original position. + * @param rect The series symbol rectangle. + * @param isMinus Is value negative. + * @param position Label position (Top/Bottom/Outer/Middle). + * @param series Parent series. + * @param textSize Text size. + * @param labelIndex Label index. + * @param point Point data. + * @returns Calculated label position. + */ + calculateRectPosition: (labelLocation: number, rect: Rect, isMinus: boolean, position: LabelPosition, series: SeriesProperties + , textSize: ChartSizeProps, labelIndex: number, point: Points, dataLabel: DataLabelProperties) => number; +} + +export interface DataLabelRendererResults { + shapeRect?: ShapeRectConfig; + textOption: TextOption; +} + +/** + * Applies a custom content callback to modify the data label text. + * + * This function invokes the `content` callback defined in the data label configuration, + * allowing developers to customize the label text dynamically based on the data value and index. + * + * @param {string} text - The original text content of the data label. + * @param {number} index - The index of the data point associated with the label. + * @param {DataLabelProperties} dataLabel - The data label configuration object to which the label belongs. + * @returns {string | boolean} The modified label content after applying the callback. If the callback fails, the original text is returned. + * @private + */ +export function applyDataLabelContentCallback( + text: string, + index: number, + dataLabel: DataLabelProperties +): string | boolean { + const contentCallback: DataLabelContentFunction = dataLabel?.formatter as DataLabelContentFunction; + if (contentCallback && typeof contentCallback === 'function') { + try { + const customProps: string | boolean = contentCallback(index, text); + return customProps; + } catch (error) { + return text; + } + } + return text; +} + +// Define the DataLabelRenderer module +export const DataLabelRenderer: IDataLabelRenderer = { + + render: (series: SeriesProperties, dataLabel: DataLabelProperties ) => { + let dataLabelProps: DataLabelRendererResults; + const dataLabels: DataLabelRendererResults[] = []; + dataLabel.markerHeight = 0; + const index: number | string = series.index as number; + dataLabel.commonId = series.chart.element.id + '_Series_' + index + '_Point_'; + dataLabel.chartBackground = series.chart.chartArea.background === 'transparent' ? + series.chart.background : series.chart.chartArea.background; + const visiblePoints: Points[] = getVisiblePoints(series); + void (series.visible && (() => { + for (let i: number = 0; i < visiblePoints.length; i++) { + dataLabelProps = DataLabelRenderer.renderDataLabel(series, visiblePoints[i as number], dataLabel); + dataLabels.push(dataLabelProps); + } + })()); + return dataLabels; + }, + + renderDataLabel: (series: SeriesProperties, point: Points, dataLabel: DataLabelProperties) => { + void (!dataLabel.showZero && ((point.y === 0) || (point.y === 0)) && (null)); + dataLabel.margin = dataLabel.margin as MarginModel; + let labelText: string[] = []; + let labelLength: number; + let xPos: number; + let yPos: number; + let degree: number; + let labelLocation: LabelLocation = { x: 0, y: 0 }; + let textSize: ChartSizeProps; + const clip: Rect = series.clipRect as Rect; + let shapeRect: ShapeRectConfig | undefined; + let isDataLabelOverlap: boolean = false; + dataLabel.rotationAngle = dataLabel.intersectMode === 'Rotate90' ? 90 : dataLabel.rotationAngle; + dataLabel.enableRotation = dataLabel.intersectMode === 'Rotate90' ? true : dataLabel.enableRotation; + const angle: number = degree = dataLabel.rotationAngle as number; + const border: ChartBorderProps = { width: dataLabel.border?.width, color: dataLabel.border?.color }; + const dataLabelFont: ChartFontProps = dataLabel.font as ChartFontProps; + const isBorder: boolean = Number(border.width) > 0 && border.color !== 'Transparent' && border.color !== ''; + const dataLabelPosition: LabelPosition | undefined = series.isRectSeries && dataLabel.position === 'Auto' + ? ((series?.type!.indexOf('Bar') > -1 || series?.type!.indexOf('Column') > -1) ? ((series?.type!.indexOf('Stacking') === -1) + ? 'Outer' : 'Top') : 'Top') : dataLabel.position; + if ( + (point.symbolLocations?.length && point.symbolLocations[0]) + ) { + labelText = getDataLabelText(point, series, series.chart) as string[]; + labelLength = labelText.length; + for (let i: number = 0; i < labelLength; i++) { + const pointFont: ChartFontProps = { + fontSize: dataLabelFont.fontSize, + color: dataLabelFont.color, + fontFamily: dataLabelFont.fontFamily, + fontStyle: dataLabelFont.fontStyle, + fontWeight: dataLabelFont.fontWeight + }; + const argsData: DataLabelContentProps = { + seriesName: series.name as string, + point: point, text: labelText[i as number], border: border, + color: dataLabel.fill as string, + font: pointFont, location: labelLocation, + textSize: measureText( + labelText[i as number], dataLabel.font as TextStyleModel, + series.chart.themeStyle.datalabelFont) + }; + + const customText: string | boolean = applyDataLabelContentCallback(argsData.text, point.index, dataLabel); + if (customText && typeof customText !== 'boolean') { + dataLabel.fontBackground = argsData.color; + DataLabelRenderer.isDataLabelShape(argsData, dataLabel); + dataLabel.markerHeight = dataLabel.markerHeight!; + } + + const rotatedTextSize: ChartSizeProps = rotateTextSize( + dataLabel.font as ChartFontProps, customText as string, + dataLabel.rotationAngle as number, series.chart, + series.chart.themeStyle.datalabelFont); + + textSize = measureText( + customText as string, dataLabel.font as TextStyleModel, series.chart.themeStyle.datalabelFont); + const rect: Rect = DataLabelRenderer.calculateTextPosition(point, series, textSize, dataLabel, i); + const actualRect: Rect = { x: rect.x + clip.x, y: rect.y + clip.y, width: rect.width, height: rect.height }; + const labelRect: Rect = { + x: rect.x + (rect.width / 2) - rotatedTextSize.width / 2, + y: rect.y + (rect.height / 2) - rotatedTextSize.height / 2, + width: rotatedTextSize.width, + height: rotatedTextSize.height + }; + const actualLabelRect: Rect = { + x: labelRect.x + clip.x, y: labelRect.y + clip.y, width: labelRect.width, height: labelRect.height + }; + series.chart.rotatedDataLabelCollections = []; + if (dataLabel.enableRotation) { + const rectCoordinates: ChartLocationProps[] = getRectanglePoints(isBorder ? actualRect : actualLabelRect); + isDataLabelOverlap = (dataLabel.intersectMode === 'Rotate90' || angle === -90) ? false : isDataLabelOverlapWithChartBound(rectCoordinates, series.chart, { x: 0, y: 0, width: 0, height: 0 }); + if (!isDataLabelOverlap) { + series.chart.rotatedDataLabelCollections.push(rectCoordinates); + const currentPointIndex: number = series.chart.rotatedDataLabelCollections.length - 1; + for (let index: number = currentPointIndex; index >= 0; index--) { + if (series.chart.rotatedDataLabelCollections[currentPointIndex as number] && + series.chart.rotatedDataLabelCollections[index - 1] && + isRotatedRectIntersect( + series.chart.rotatedDataLabelCollections[currentPointIndex as number] as ChartLocationProps[], + series.chart.rotatedDataLabelCollections[index - 1] as ChartLocationProps[]) + ) { + isDataLabelOverlap = true; + series.chart.rotatedDataLabelCollections[currentPointIndex as number] = []; + break; + } + } + } + } else { + isDataLabelOverlap = isCollide( isBorder ? rect : labelRect, (series.chart).dataLabelCollections as Rect[], clip); + } + + const CenterX: number = rect.x + rect.width / 2; + const CenterY: number = rect.y + (rect.height / 2); + xPos = CenterX; + yPos = CenterY; + if (!isDataLabelOverlap && series.isRectSeries && point.regions && point.regions[0] && dataLabelPosition !== 'Outer') { + const pointRegion: Rect = point.regions[0]; + const rectCheck: Rect = isBorder ? rect : labelRect; + + const case1: boolean = series.chart.requireInvertedAxis ? rectCheck.x < pointRegion.x : + rectCheck.y < pointRegion.y; + const case2: boolean = series.chart.requireInvertedAxis ? rectCheck.x + + rectCheck.width > pointRegion.x + pointRegion.width : rectCheck.y + + rectCheck.height > pointRegion.y + pointRegion.height; + + if (case1 || case2) { + isDataLabelOverlap = true; + } + } + if (!isDataLabelOverlap || dataLabel.intersectMode === 'None') { + series.chart.dataLabelCollections.push(isBorder ? actualRect as Rect : actualLabelRect as Rect); + const backgroundColor: string = dataLabel.fontBackground === 'transparent' ? + ((series.chart.theme.indexOf('Dark') > -1 || series.chart.theme.indexOf('HighContrast') > -1) ? 'black' : 'white') : + dataLabel.fontBackground as string; + const rgbValue: ColorValue = convertHexToColor(colorNameToHex(backgroundColor)); + const LUMINANCE_RED_COEFFICIENT: number = 299; + const LUMINANCE_GREEN_COEFFICIENT: number = 587; + const LUMINANCE_BLUE_COEFFICIENT: number = 114; + const LUMINANCE_DIVISOR: number = 1000; + + const contrast: number = Math.round( + (rgbValue.r * LUMINANCE_RED_COEFFICIENT + + rgbValue.g * LUMINANCE_GREEN_COEFFICIENT + + rgbValue.b * LUMINANCE_BLUE_COEFFICIENT) / LUMINANCE_DIVISOR + ); + labelLocation = { x: 0, y: 0 }; + + degree = 0; + + xPos -= xPos + (textSize.width / 2) > clip.width ? (!series.chart.requireInvertedAxis && xPos > clip.width) ? 0 : + (xPos + textSize.width / 2) - clip.width : 0; + yPos -= (yPos + textSize.height > clip.y + clip.height && !(series.type!.indexOf('Bar') > -1)) ? (yPos + textSize.height) - (clip.y + clip.height) : 0; + + if (dataLabel.border?.color !== '' && dataLabel.border?.width !== 0 && !dataLabel.enableRotation) { + if (rect.x + rect.width > clip.width) { + rect.x = rect.x - (rect.x + rect.width - (clip.width - border.width!)); + xPos = rect.x + rect.width / 2; + } + } + shapeRect = dataLabel.isShape ? { + id: dataLabel.commonId as string + point.index + '_TextShape_' + i, + fill: argsData.color, + border: argsData.border, + opacity: dataLabel.opacity as number, // Provide a default value of 1 when opacity is undefined + rect: rect, + rx: dataLabel.borderRadius?.x as number, + ry: dataLabel.borderRadius?.y as number, + transform: dataLabel.enableRotation ? 'rotate(' + dataLabel.rotationAngle + ', ' + CenterX + ', ' + CenterY + ')' : '', + stroke: dataLabel.border?.dashArray + } : undefined; + + if (point.originalY !== 0 && !point.textValue) { + point.textValue = customText as string; + } + if (typeof point.y === 'number' && point.originalY === 0) { + point.originalY = point.y; + } + const textOption: TextOption = textElement( + { + id: dataLabel.commonId as string + (point.index) + '_Text_' + i, + x: xPos, + y: yPos, + anchor: 'middle', + text: customText as string, + transform: dataLabel.enableRotation ? 'rotate(' + dataLabel.rotationAngle + ', ' + CenterX + ', ' + CenterY + ')' : '', + baseLine: 'auto', + labelRotation: degree + }, + argsData.font, + argsData.font.color || ((contrast >= 128) ? + '#1E192B' : '#E8DEF8'), + false, + series.clipRect, + false + ) as TextOption; + + return dataLabel.isShape ? { shapeRect, textOption } : { textOption }; + } + } + } + return { textOption: {} as TextOption }; + }, + isDataLabelShape: (style: { color: string, border: ChartBorderProps }, dataLabel: DataLabelProperties) => { + dataLabel.isShape = (style.color !== 'transparent' || (style.border?.width ?? 0) > 0); + dataLabel.borderWidth = style.border.width; + void (!dataLabel.isShape && (dataLabel.margin = { left: 0, right: 0, bottom: 0, top: 0 })); + }, + + calculateTextPosition: (point: Points, series: SeriesProperties, textSize: ChartSizeProps, + dataLabel: DataLabelProperties, labelIndex: number): Rect => { + const labelRegion: Rect = point.regions?.[0] as Rect; + const location: ChartLocationProps = DataLabelRenderer.getLabelLocation(point, series, labelIndex); + const padding: number = 5; + const clipRect: Rect = series.clipRect as Rect; + const dataLabelPosition: LabelPosition | undefined = series.isRectSeries && dataLabel.position === 'Auto' ? ((series?.type!.indexOf('Bar') > -1 || series?.type!.indexOf('Column') > -1) ? ((series?.type!.indexOf('Stacking') === -1) ? 'Outer' : 'Top') : 'Top') : dataLabel.position; + + if (!series.chart.requireInvertedAxis || !series.isRectSeries) { + dataLabel.locationX = location.x; + const alignmentValue: number = textSize.height + (dataLabel.borderWidth as number * 2) + (dataLabel.markerHeight as number) + + (dataLabel.margin?.bottom as number) + (dataLabel.margin?.top as number) + padding; + location.x = + DataLabelRenderer.calculateAlignment( + alignmentValue, location.x, dataLabel.textAlign as HorizontalAlignment, + series.isRectSeries ? Number(point.yValue) < 0 : false, series.chart.requireInvertedAxis + ); + location.y = !series.isRectSeries ? + DataLabelRenderer.calculatePathPosition( + location.y, dataLabelPosition as LabelPosition, textSize, dataLabel, series, point, labelIndex + ) : DataLabelRenderer.calculateRectPosition( + location.y, labelRegion, Number(point.yValue) < 0, + dataLabelPosition as LabelPosition, series, textSize, labelIndex, point, dataLabel + ); + } + else { + + const alignmentValue: number = textSize.width + Number(dataLabel?.borderWidth) + + Number(dataLabel.margin?.left) + Number(dataLabel.margin?.right) - padding; + + location.x = DataLabelRenderer.calculateAlignment( + alignmentValue, + location.x, + dataLabel.textAlign as HorizontalAlignment, + Number(point.yValue) < 0, + series.chart.requireInvertedAxis + ); + + location.x = DataLabelRenderer.calculateRectPosition( + location.x, + labelRegion, + (Number(point.yValue)) < 0 !== (series.yAxis?.inverted ?? false), + dataLabelPosition as LabelPosition, + series, + textSize, + labelIndex, + point, + dataLabel + ); + } + + const rect: Rect = calculateRect(location, textSize, dataLabel.margin as MarginModel); + + if (!(dataLabel.enableRotation === true && dataLabel.rotationAngle !== 0) && + !((rect.y > (clipRect.y + clipRect.height)) || (rect.x > (clipRect.x + clipRect.width)) || + (rect.x + rect.width < 0) || (rect.y + rect.height < 0))) { + rect.x = rect.x < 0 ? (series.type === 'StackingColumn' && !series.chart.requireInvertedAxis ? 0 : padding) : rect.x; + rect.y = (rect.y < 0 && !series.chart.requireInvertedAxis) && !(dataLabel.intersectMode === 'None') ? padding : rect.y; + rect.x -= (rect.x + rect.width) > (clipRect.x + clipRect.width) ? (rect.x + rect.width) + - (clipRect.x + clipRect.width) + padding : 0; + rect.y -= (rect.y + rect.height) > (clipRect.y + clipRect.height) ? (rect.y + rect.height) + - (clipRect.y + clipRect.height) + padding : 0; + dataLabel.fontBackground = dataLabel.fontBackground === 'transparent' ? dataLabel.chartBackground : dataLabel.fontBackground; + } + + let dataLabelOutRegion: boolean | undefined = false; + dataLabelOutRegion = (dataLabel.inverted && series.isRectSeries && (rect.x + rect.width > labelRegion.x + labelRegion.width)); + dataLabel.fontBackground = dataLabelOutRegion ? dataLabel.chartBackground : dataLabel.fontBackground; + return rect; + }, + + getLabelLocation: (point: Points, series: SeriesProperties, labelIndex: number): ChartLocationProps => { + let location: ChartLocationProps = { x: 0, y: 0 }; + const labelRegion: Rect = point.regions?.[0] as Rect; + const isInverted: boolean = series.chart.requireInvertedAxis; + location = + (isInverted && point.yValue === 0) ? + { x: labelRegion.x + labelRegion.width, y: labelRegion.y + (labelRegion.height) / 2 } : + (labelIndex === 0 || labelIndex === 1) ? + { x: point.symbolLocations?.[0]?.x as number, y: point.symbolLocations?.[0]?.y as number } : + ((labelIndex === 2 || labelIndex === 3)) ? + { x: point.symbolLocations?.[1]?.x as number, y: point.symbolLocations?.[1]?.y as number } : + isInverted ? + { x: labelRegion.x + (labelRegion.width) / 2, y: labelRegion.y } : + { x: labelRegion.x + labelRegion.width, y: labelRegion.y + (labelRegion.height) / 2 }; + + return location; + }, + + calculateAlignment: (value: number, labelLocation: number, + alignment: HorizontalAlignment, isMinus: boolean, isInverted: boolean): number => { + switch (alignment) { + case 'Right': labelLocation = !isInverted ? (isMinus ? labelLocation + value : labelLocation - value) : + (isMinus ? labelLocation - value : labelLocation + value); break; + case 'Left': labelLocation = !isInverted ? (isMinus ? labelLocation - value : labelLocation + value) : + (isMinus ? labelLocation + value : labelLocation - value); break; + } + return labelLocation; + }, + + calculatePathPosition: ( + labelLocation: number, position: LabelPosition, + size: ChartSizeProps, dataLabel: DataLabelProperties, series: SeriesProperties, point: Points, labelIndex: number): number => { + const padding: number = 10; + dataLabel.fontBackground = dataLabel.fontBackground === 'transparent' ? dataLabel.chartBackground : dataLabel.fontBackground; + switch (position) { + case 'Top': + case 'Outer': + labelLocation = labelLocation - (dataLabel.markerHeight as number) + - (dataLabel.borderWidth as number) - size.height / 2 - (dataLabel.margin?.bottom as number) - padding; + break; + case 'Bottom': + labelLocation = labelLocation + (dataLabel.markerHeight as number) + + (dataLabel.borderWidth as number) + size.height / 2 + (dataLabel.margin as MarginModel).top + padding; + break; + case 'Auto': + labelLocation = DataLabelRenderer.calculatePathActualPosition( + labelLocation, series, point, size, labelIndex, dataLabel + ); + break; + } + return labelLocation; + }, + + calculatePathActualPosition: ( + y: number, + series: SeriesProperties, + point: Points, + size: ChartSizeProps, + labelIndex: number, + dataLabel: DataLabelProperties + ): number => { + const points: Points[] = series.points || []; + const index: number = point.index; + const yValue: number = points[index as number].yValue as number; + let position: LabelPosition; + const nextPoint: Points | null = points.length - 1 > index ? points[index + 1] : null; + const previousPoint: Points | null = index > 0 ? points[index - 1] : null; + let yLocation: number = 0; + let isOverLap: boolean = true; + let labelRect: Rect; + let isBottom: boolean; + let positionIndex: number; + const collection: Rect[] = series.chart.dataLabelCollections || []; + const yAxisInversed: boolean = series.yAxis?.inverted || false; + + if (series.type === 'Bubble') { + position = 'Top'; + } else if (series.type && series.type.indexOf('Step') > -1) { + position = 'Top'; + if (index) { + position = (!previousPoint || !previousPoint.visible || (yValue > (previousPoint.yValue as number) !== yAxisInversed) + || yValue === previousPoint.yValue) ? 'Top' : 'Bottom'; + } + } else { + if (index === 0) { + position = (!nextPoint || !nextPoint.visible || yValue > (nextPoint.yValue as number) || + (yValue < (nextPoint.yValue as number) && yAxisInversed)) ? 'Top' : 'Bottom'; + } else if (index === points.length - 1) { + position = (!previousPoint || !previousPoint.visible || yValue > (previousPoint.yValue as number) || + (yValue < (previousPoint.yValue as number) && yAxisInversed)) ? 'Top' : 'Bottom'; + } else { + if (nextPoint && !nextPoint.visible && !(previousPoint && previousPoint.visible)) { + position = 'Top'; + } else if (nextPoint && (!nextPoint.visible || !previousPoint)) { + position = ((nextPoint.yValue as number) > yValue || (previousPoint && (previousPoint.yValue as number) > yValue)) ? + 'Bottom' : 'Top'; + } else if (nextPoint && previousPoint) { + const slope: number = ((nextPoint.yValue as number) - (previousPoint.yValue as number)) / 2; + const intersectY: number = (slope * index) + ((nextPoint.yValue as number) - (slope * (index + 1))); + position = !yAxisInversed ? intersectY < yValue ? 'Top' : 'Bottom' : + intersectY < yValue ? 'Bottom' : 'Top'; + } else { + position = 'Top'; // Default fallback + } + } + } + + isBottom = position === 'Bottom'; + positionIndex = ['Outer', 'Top', 'Bottom', 'Middle', 'Auto'].indexOf(position); + + while (isOverLap && positionIndex < 4) { + const currentPosition: LabelPosition = getPosition(positionIndex); + yLocation = DataLabelRenderer.calculatePathPosition( + y, + currentPosition, + size, + series.marker?.dataLabel as DataLabelProperties, + series, + point, + labelIndex + ); + + const locationX: number = dataLabel?.locationX || 0; + labelRect = calculateRect( + { x: locationX, y: yLocation }, + size, + (series.marker?.dataLabel?.margin || { left: 0, right: 0, top: 0, bottom: 0 }) as MarginModel + ); + + isOverLap = labelRect.y < 0 || + isCollide(labelRect, collection as Rect[], series.clipRect as Rect) || + (labelRect.y + labelRect.height) > (series.clipRect?.height || 0); + + positionIndex = isBottom ? positionIndex - 1 : positionIndex + 1; + isBottom = false; + } + + return yLocation; + }, + + calculateRectPosition: ( + labelLocation: number, rect: Rect, isMinus: boolean, + position: LabelPosition, series: SeriesProperties, textSize: ChartSizeProps, + labelIndex: number, point: Points, dataLabel: DataLabelProperties + ): number => { + + const padding: number = 5; + const margin: MarginModel = position === 'Middle' ? + { left: 0, right: 0, top: 0, bottom: 0 } : + series.marker?.dataLabel?.margin as MarginModel; + + let extraSpace: number; + const textLength: number = (series?.marker?.dataLabel?.enableRotation ? textSize.width : + (!series.chart.requireInvertedAxis ? textSize.height : textSize.width)); + if (position === 'Bottom' && series.type === 'StackingColumn' && !series.chart.requireInvertedAxis && rect.height < textSize.height) { + extraSpace = Number(dataLabel?.borderWidth) + + ((Math.abs(rect.height - textSize.height / 2) < padding) ? 0 : padding); + } else { + extraSpace = (dataLabel.isShape ? dataLabel?.borderWidth as number : 0) + textLength / 2 + (position !== 'Outer' && series.type!.indexOf('Column') > -1 && + (Math.abs(rect.height - textSize.height) < padding) ? 0 : padding); + } + + const inverted: boolean = series.chart.requireInvertedAxis; + const yAxisInversed: boolean = series.yAxis?.inverted ?? false; + + switch (position) { + case 'Bottom': + labelLocation = !inverted ? + isMinus ? (labelLocation + ((-rect.height + extraSpace + margin.top))) : + (labelLocation + rect.height - extraSpace - margin.bottom) : + isMinus ? (labelLocation + ((+ rect.width - extraSpace - margin.left))) : + (labelLocation - rect.width + extraSpace + margin.right); + break; + case 'Middle': + labelLocation = !inverted ? + (isMinus ? labelLocation - (rect.height / 2) : labelLocation + (rect.height / 2)) : + (isMinus ? labelLocation + (rect.width / 2) : labelLocation - (rect.width / 2)); + break; + default: + labelLocation = calculateTopAndOuterPosition(labelLocation, rect, position, series, labelIndex + , extraSpace, isMinus, point, inverted, yAxisInversed); + break; + } + + const check: boolean = !inverted ? + (labelLocation < rect.y || labelLocation > rect.y + rect.height) : + (labelLocation < rect.x || labelLocation > rect.x + rect.width); + + // Set font background based on position + const fontBackground: string | undefined = check ? + (dataLabel?.fontBackground === 'transparent' ? + (series.chart?.chartArea?.background ?? 'white') : + dataLabel?.fontBackground) : + dataLabel?.fontBackground === 'transparent' ? (point.color || series.interior) : + dataLabel?.fontBackground; + + if (series.marker?.dataLabel) { + dataLabel.fontBackground = fontBackground; + } + + return labelLocation; + } +}; + +/** + * Calculates the position for data labels in 'Top' or 'Outer' positions + * Adjusts the label location based on marker visibility, series type, and chart orientation + * + * @param {number} location - The initial label location (x or y) to adjust + * @param {Rect} _rect - The rectangle representing the data point region + * @param {LabelPosition} position - The desired label position ('Top', 'Outer', etc.) + * @param {SeriesProperties} series - The series object containing styling and configuration + * @param {number} _index - The index of the label (for multiple labels per point) + * @param {number} extraSpace - Additional spacing to apply + * @param {boolean} isMinus - Whether the value is negative + * @param {Points} point - The data point object + * @param {boolean} inverted - Whether the chart axes are inverted + * @param {boolean} _yAxisInversed - Whether the Y-axis direction is inversed + * @returns {number} The calculated position for the label + * @private + */ +export function calculateTopAndOuterPosition( + location: number, _rect: Rect, position: LabelPosition, series: SeriesProperties, _index: number, + extraSpace: number, isMinus: boolean, point: Points, inverted: boolean, _yAxisInversed: boolean +): number { + const margin: MarginModel = (series.marker?.dataLabel?.margin) as MarginModel; + + if (((isMinus && position === 'Top') || (!isMinus && position === 'Outer')) || + (position === 'Top' && series.visiblePoints![point.index].yValue === 0)) { + location = !inverted ? + location + ((-extraSpace - margin.bottom - (series.marker?.visible ? Number(series.marker?.height) / 2 : 0))) : + location + ((+ extraSpace + margin.left + (series.marker?.visible ? Number(series.marker?.height) / 2 : 0))); + } else { + location = !inverted ? + location + ((+ extraSpace + margin.top + (series.marker?.visible ? Number(series.marker?.height) / 2 : 0))) : + location + ((- extraSpace - margin.right - (series.marker?.visible ? Number(series.marker?.height) / 2 : 0))); + } + + return location; +} + +/** + * Applies an easing function to make the animation smoother + * Uses the same quadratic ease-in-out function as the chart animation + * + * @param {number} progress - Raw animation progress (0-1) + * @returns {number} Eased animation progress + * @private + */ +function easeInOutQuad(progress: number): number { + return progress < 0.5 + ? 2 * progress * progress + : 1 - Math.pow(-2 * progress + 2, 2) / 2; +} + +/** + * Global store for tracking data label positions across chart animations + * Maps chart and series ids to data label positions and transforms + */ +const previousDataLabelPositions: Map> = new Map>(); + +/** + * Renders shape elements (e.g., rectangle backgrounds) for a set of data labels for a series, with animated transitions. + * + * @param {DataLabelRendererResult[]} dataLabel - Array of data label renderer results (shapes and text info). + * @param {number} index - The index of the series. + * @param {React.RefObject} layoutRef - Reference to the layout map/chart context. + * @param {number} [animationProgress=1] - Animation progress (0-1, default 1 = no animation). + * @returns {React.ReactNode} SVG element with child elements representing shape backgrounds, or null if nothing to render. + * @private + */ +export function renderDataLabelShapesJSX( + dataLabel: DataLabelRendererResult[], + index: number, + layoutRef: React.RefObject, + animationProgress: number = 1 +): React.ReactNode { + if (!dataLabel || dataLabel.length === 0 || !(layoutRef.current?.chart as Chart)?.visibleSeries?.[index as number]) { + return null; + } + + const series: SeriesProperties = (layoutRef.current.chart as Chart).visibleSeries[index as number]; + const clipX: number = series.clipRect?.x as number; + const clipY: number = series.clipRect?.y as number; + const chartId: string = (layoutRef.current.chart as Chart).element.id; + + // Get or initialize positions map for this chart + const chartKey: string = chartId; + if (!previousDataLabelPositions.has(chartKey)) { + previousDataLabelPositions.set(chartKey, new Map()); + } + + const chartPositions: Map = previousDataLabelPositions.get(chartKey)!; + + return ( + + {dataLabel.map((labelData: DataLabelRendererResult, labelIndex: number) => { + if (!labelData || labelData.shapeRect === undefined) {return null; } + + // Create a unique key for the label to track its position + const labelKey: string = `${index}_shape_${labelIndex}`; + + // Get current position + const currentX: number = labelData.shapeRect.rect.x; + const currentY: number = labelData.shapeRect.rect.y; + + // Get previous position + let previousX: number = currentX; + let previousY: number = currentY; + + // Check if we have previous position data + if (chartPositions.has(labelKey)) { + const pos: { + x: number; + y: number; + transform?: string; + width?: number; + height?: number; + rx?: number; + ry?: number; + } = chartPositions.get(labelKey)![0]; + previousX = pos.x; + previousY = pos.y; + } + + // Calculate interpolated position for animation + let x: number = currentX; + let y: number = currentY; + + // Apply eased interpolation for smoother animation + if (animationProgress < 1) { + const easedProgress: number = easeInOutQuad(animationProgress); + x = interpolateWithEasing(previousX, currentX, easedProgress); + y = interpolateWithEasing(previousY, currentY, easedProgress); + } + + // Get shape dimensions for animation + const currentWidth: number = labelData.shapeRect.rect.width; + const currentHeight: number = labelData.shapeRect.rect.height; + const currentRx: number = labelData.shapeRect.rx; + const currentRy: number = labelData.shapeRect.ry; + + // Look up previous dimensions + let width: number = currentWidth; + let height: number = currentHeight; + let rx: number = currentRx; + let ry: number = currentRy; + + // If we have previous position data with dimensions + if (chartPositions.has(labelKey)) { + const previousDimensions: dataLabelOptions = chartPositions.get(labelKey)![0]; + if (previousDimensions.width !== undefined && + previousDimensions.height !== undefined && + animationProgress < 1) { + width = interpolateWithEasing(previousDimensions.width, currentWidth, animationProgress); + height = interpolateWithEasing(previousDimensions.height, currentHeight, animationProgress); + } + + if (previousDimensions.rx !== undefined && + previousDimensions.ry !== undefined && + animationProgress < 1) { + rx = interpolateWithEasing(previousDimensions.rx, currentRx, animationProgress); + ry = interpolateWithEasing(previousDimensions.ry, currentRy, animationProgress); + } + } + + // Store current position and dimensions for future animations if animation is complete + if (animationProgress === 1) { + if (!chartPositions.has(labelKey)) { + chartPositions.set(labelKey, []); + } + const positions: dataLabelOptions[] = chartPositions.get(labelKey)!; + positions[0] = { + x: currentX, + y: currentY, + transform: labelData.shapeRect.transform, + width: currentWidth, + height: currentHeight, + rx: currentRx, + ry: currentRy + }; + } + + return ( + + ); + })} + + ); +} + +/** + * Parses the formatted value from label text to extract the numeric value, prefix and suffix for use in animation. + * + * @param {string|number} text - The label text as string or number. + * @returns {{ value: number, format: string, prefix: string, suffix: string }} Object with value, format, prefix, suffix. + * @private + */ +function parseFormattedText(text: string | number): { + value: number; + format: string; + prefix: string; + suffix: string; +} { + if (typeof text === 'number') { + return { value: text, format: text.toString(), prefix: '', suffix: '' }; + } + + const textStr: string = String(text).trim(); + const chars: string[] = [...textStr]; + let numberStart: number = -1; + let numberEnd: number = -1; + + for (let i: number = 0; i < chars.length; i++) { + const c: string = chars[i as number]; + if ((c >= '0' && c <= '9') || c === '-' || c === '.') { + if (numberStart === -1) {numberStart = i; } + numberEnd = i; + } else if (numberStart !== -1) { + break; + } + } + + if (numberStart === -1) { + return { value: 0, format: textStr, prefix: '', suffix: textStr }; + } + + const numStr: string = textStr.slice(numberStart, numberEnd + 1); + const value: number = parseFloat(numStr); + const prefix: string = textStr.slice(0, numberStart).trim(); + const suffix: string = textStr.slice(numberEnd + 1).trim(); + + return { + value: isNaN(value) ? 0 : value, + format: textStr, + prefix, + suffix + }; +} + +/** + * Renders text elements for a set of data labels for a series, including number text animation for updated data points. + * + * @param {DataLabelRendererResult[]} dataLabel - Array of data label renderer results (text and shape info). + * @param {number} index - Series index. + * @param {React.RefObject} layoutRef - Reference to layout map/chart context. + * @param {number} [animationProgress=1] - Animation progress (0-1). + * @param {number} [_legendClick] - Used to rerender after legend click (not used directly). + * @returns {React.ReactNode} SVG element with labels, or null. + * @private + */ +export function renderDataLabelTextJSX( + dataLabel: DataLabelRendererResult[], + index: number, + layoutRef: React.RefObject, + animationProgress: number = 1, + _legendClick?: number +): React.ReactNode { + if (!dataLabel || dataLabel.length === 0 || !(layoutRef.current?.chart as Chart)?.visibleSeries?.[index as number]) { + return null; + } + + const series: SeriesProperties = (layoutRef.current.chart as Chart).visibleSeries[index as number]; + const clipX: number = series.clipRect?.x as number; + const clipY: number = series.clipRect?.y as number; + const chartId: string = (layoutRef.current.chart as Chart).element.id; + + // Get or initialize positions map for this chart + const chartKey: string = chartId; + if (!previousDataLabelPositions.has(chartKey)) { + previousDataLabelPositions.set(chartKey, new Map()); + } + + const chartPositions: Map = previousDataLabelPositions.get(chartKey)!; + return ( + + ); +} + +/** + * Renders complete set of data label shapes and texts for a series, grouped together. + * + * @param {DataLabelRendererResult[]} dataLabel - The array of label rendering results. + * @param {number} index - Series index. + * @param {React.RefObject} layoutRef - Reference to layout context/List. + * @param {number} _labelOpacity - Not used directly (label opacity is computed inside shapes/text). + * @param {number} [animationProgress=1] - Animation progress (0-1). + * @returns {React.ReactNode} React fragment with both shape and text elements for data labels. + * @private + */ +export function renderDataLabelJSX( + dataLabel: DataLabelRendererResult[], + index: number, + layoutRef: React.RefObject, + _labelOpacity: number, + animationProgress: number = 1 +): React.ReactNode { + if (!dataLabel || dataLabel.length === 0) { + return null; + } + + return ( + <> + {renderDataLabelShapesJSX(dataLabel, index, layoutRef, animationProgress)} + {renderDataLabelTextJSX(dataLabel, index, layoutRef, animationProgress)} + + ); +} + +export const calculateAlignment: (value: number, labelLocation: number, alignment: HorizontalAlignment, + isMinus: boolean, isInverted: boolean) => number = DataLabelRenderer.calculateAlignment; +export const isDataLabelShape: (style: { color: string, border: ChartBorderProps }, + dataLabel: DataLabelProperties) => void = DataLabelRenderer.isDataLabelShape; +export const getLabelLocation: (point: Points, series: SeriesProperties, labelIndex: number) => +ChartLocationProps = DataLabelRenderer.getLabelLocation; +export const calculateTextPosition: (point: Points, series: SeriesProperties, textSize: ChartSizeProps, + dataLabel: DataLabelProperties, labelIndex: number) => Rect = DataLabelRenderer.calculateTextPosition; +export const calculateRectPosition: ( + labelLocation: number, rect: Rect, isMinus: boolean, + position: LabelPosition, series: SeriesProperties, textSize: ChartSizeProps, + labelIndex: number, point: Points, dataLabel: DataLabelProperties +) => number = DataLabelRenderer.calculateRectPosition; +export default DataLabelRenderer; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/LineBase.tsx b/components/charts/src/chart/renderer/SeriesRenderer/LineBase.tsx new file mode 100644 index 0000000..3adf16c --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/LineBase.tsx @@ -0,0 +1,228 @@ +import { isNullOrUndefined } from '@syncfusion/react-base'; +import { StepPosition } from '../../base/enum'; +import { Points, Rect, SeriesProperties, VisibleRangeProps } from '../../chart-area/chart-interfaces'; +import { ChartLocationProps } from '../../base/interfaces'; + + +/** + * Interface defining the functions available in the LineBase utility. + * + * @private + */ +export type LineBaseReturnType = { + /** + * Stores the point location in the chart and calculates its region + * + * @param {Points} point - The data point to store location for + * @param {SeriesProperties} series - Series configuration properties + * @param {boolean} isInverted - Flag indicating if chart orientation is inverted + * @param {GetLocationFunction} getLocation - Function to convert data coordinates to chart pixel coordinates + * @returns {void} This function doesn't return a value + */ + storePointLocation: (point: Points, series: SeriesProperties, isInverted: boolean, getLocation: Function) => void; + + /** + * Processes series points and enables complex properties for rendering optimization + * + * @param {SeriesProperties} series - Series configuration properties including axes information + * @returns {Points[]} Array of processed points with optimized visibility + */ + enableComplexProperty: (series: SeriesProperties) => Points[]; + + /** + * Gets the step line direction path between two points + * + * @param {ChartLocationProps} currentPoint - The current point coordinates + * @param {ChartLocationProps} previousPoint - The previous point coordinates + * @param {StepPosition} stepLineType - The type of step line (Left, Right, Center) + * @param {string} command - SVG path command for the starting point (typically 'M' or 'L') + * @param {SeriesProperties} series - Series configuration properties + * @param {boolean} isBorder - Flag indicating if this is a border path + * @returns {string} SVG path string representing the step line direction + */ + getStepLineDirection: (currentPoint: ChartLocationProps, previousPoint: ChartLocationProps, + stepLineType: StepPosition, command: string, series: SeriesProperties, isBorder?: boolean) => string; + + /** + * Removes empty points from the border direction path + * + * @param {string} borderDirection - The SVG path string to process + * @returns {string} Processed SVG path with empty points removed + */ + removeEmptyPointsBorder: (borderDirection: string) => string; +}; + + +/** + * Processes series points and enables complex properties for rendering optimization + * + * @param {SeriesProperties} series - Series configuration properties including axes information + * @returns {Points[]} Array of processed points with optimized visibility based on tolerance values + */ +const enableComplexProperty: (series: SeriesProperties) => Points[] = (series: SeriesProperties): Points[] => { + const tempPoints: Points[] = []; + const tempPointsTwo: Points[] = []; + const xVisibleRange: VisibleRangeProps = series.xAxis.visibleRange; + const yVisibleRange: VisibleRangeProps = series.yAxis.visibleRange; + const seriesPoints: Points[] | undefined = series.points; + const areaBounds: Rect | undefined = series.clipRect; + + const xTolerance: number = (series.chart && !series.chart.delayRedraw && series.xTolerance) ? + series.xTolerance : Math.abs(xVisibleRange.delta / areaBounds!.width); + const yTolerance: number = (series.chart && !series.chart.delayRedraw && series.yTolerance) ? + series.yTolerance : Math.abs(yVisibleRange.delta / areaBounds!.height); + series.xTolerance = xTolerance; + series.yTolerance = yTolerance; + + let prevXValue: number = (Number(seriesPoints?.[0]?.xValue) > xTolerance!) ? 0 : xTolerance!; + let prevYValue: number = (Number(seriesPoints?.[0]?.y) > yTolerance!) ? 0 : yTolerance!; + + for (const currentPoint of seriesPoints!) { + currentPoint.symbolLocations = []; + const xVal: number = !isNullOrUndefined(currentPoint.xValue) ? (currentPoint.xValue) as number : xVisibleRange.minimum; + const yVal: number = !isNullOrUndefined(currentPoint.yValue) ? (currentPoint.yValue) as number : yVisibleRange.minimum; + void ((Math.abs(prevXValue! - xVal) >= xTolerance! || Math.abs(prevYValue! - yVal) >= yTolerance!) && ( + tempPoints.push(currentPoint), + prevXValue = xVal, + prevYValue = yVal + )); + } + + for (let i: number = 0; i < tempPoints.length; i++) { + const tempPoint: Points = tempPoints[i as number]; + tempPointsTwo.push(tempPoint); + } + + return tempPointsTwo; +}; + +/** + * Stores the point location in the chart and calculates its region for interaction + * + * @param {Points} point - The data point to store location for + * @param {SeriesProperties} series - Series configuration properties including marker information + * @param {boolean} isInverted - Flag indicating if chart orientation is inverted + * @param {GetLocationFunction} getLocation - Function to convert data coordinates to chart pixel coordinates + * @returns {void} This function doesn't return a value + */ +const storePointLocation: (point: Points, series: SeriesProperties, isInverted: boolean, getLocation: Function) => void + = (point: Points, series: SeriesProperties, isInverted: boolean, getLocation: Function): void => { + const markerWidth: number = (series.marker && series.marker.width) ? series.marker.width : 0; + const markerHeight: number = (series.marker && series.marker.height) ? series.marker.height : 0; + point.symbolLocations!.push( + getLocation( + point.xValue, point.yValue, + series.xAxis, series.yAxis, isInverted, series + ) + ); + point.regions!.push({ + x: point.symbolLocations![0].x - markerWidth, + y: point.symbolLocations![0].y - markerHeight, + width: 2 * markerWidth, + height: 2 * markerHeight + }); + }; + +/** + * To get the point location for step line type series. + * + * @param {ChartLocationProps} currentPoint - Defines the current point. + * @param {ChartLocationProps} previousPoint - Defines the previous point. + * @param {StepPosition} stepLineType - Defines the step line type. + * @param {string} command - Defines the command. + * @param {Series} series - Defines the series. + * @param {boolean} isBorder - Defines the isBorder. + * @returns {string} - Returns a string of path. + */ +const getStepLineDirection: (currentPoint: ChartLocationProps, + previousPoint: ChartLocationProps, + stepLineType: StepPosition, + command: string, + series: SeriesProperties, + isBorder?: boolean) => string = ( + currentPoint: ChartLocationProps, + previousPoint: ChartLocationProps, + stepLineType: StepPosition, + command: string = 'L', + series: SeriesProperties +): string => { + // Input validation + if (!currentPoint || !previousPoint || !series) { + return ''; + } + + // Validate point coordinates + if (currentPoint.x === undefined || currentPoint.y === undefined || + previousPoint.x === undefined || previousPoint.y === undefined) { + return ''; + } + + // Ensure command is valid + command = command || 'L'; + + const useRisers: boolean = !(series.noRisers === true); + const riserCommand: string = useRisers ? ' L ' : ' M '; + + if (stepLineType === 'Right') { + const startCommand: string = command === 'M' ? 'M' : (useRisers ? 'L' : 'M'); + return `${startCommand} ${previousPoint.x} ${currentPoint.y} L ${currentPoint.x} ${currentPoint.y} `; + } + if (stepLineType === 'Center') { + const midpointX: number = previousPoint.x + (currentPoint.x - previousPoint.x) / 2; + return `${command} ${midpointX} ${previousPoint.y}${riserCommand}${midpointX} ${currentPoint.y} L ${currentPoint.x} ${currentPoint.y} `; + } + return `${command} ${currentPoint.x} ${previousPoint.y}${riserCommand}${currentPoint.x} ${currentPoint.y} `; +}; + +/** + * Removes empty points from the border direction path + * + * @param {string} borderDirection - The SVG path string to process + * @returns {string} Processed SVG path with empty points removed + */ +const removeEmptyPointsBorder: ( + borderDirection: string +) => string = (borderDirection: string): string => { + // Input validation + if (!borderDirection || typeof borderDirection !== 'string') { + return ''; + } + + let startIndex: number = 0; + const coordinates: string[] = borderDirection.split(' ').filter((item: string) => item !== ''); + let point: number; + + // Early return for simple paths + if (coordinates.length <= 4) { + return coordinates.join(' '); + } + + do { + point = coordinates.indexOf('M', startIndex); + if (point > -1) { + // Ensure we don't go out of bounds + if (point + 3 < coordinates.length) { + coordinates.splice(point + 1, Math.min(3, coordinates.length - (point + 1))); + } + startIndex = point + 1; + + if (point - 6 > 0 && coordinates.length >= point - 6 + 6) { + coordinates.splice(point - 6, 6); + startIndex -= 6; + } + } + } while (point !== -1 && startIndex < coordinates.length); + + return coordinates.join(' '); +}; + +/** + * LineBase utility object providing functions for line-based chart series rendering. + */ +export const LineBase: LineBaseReturnType = { + storePointLocation, + enableComplexProperty, + getStepLineDirection, + removeEmptyPointsBorder +}; +export default LineBase; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/MarkerBase.tsx b/components/charts/src/chart/renderer/SeriesRenderer/MarkerBase.tsx new file mode 100644 index 0000000..34b4956 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/MarkerBase.tsx @@ -0,0 +1,43 @@ +import { PathOptions } from '../../chart-area/chart-interfaces'; + + + +/** + * Creates a PathOption object with the specified properties for marker rendering. + * + * @param {string} id - Unique identifier for the path element + * @param {string} fill - Fill color of the path + * @param {number} strokeWidth - Width of the stroke line + * @param {string} stroke - Stroke color of the path + * @param {number} opacity - Opacity value between 0 and 1 + * @param {string} strokeDasharray - SVG dash array pattern for the stroke + * @param {string} d - SVG path data string + * @returns {PathOptions} A complete path option configuration + */ +export const createMarkerPathOption: ( + id: string, + fill: string, + strokeWidth: number, + stroke: string, + opacity: number, + strokeDasharray: string, + d: string +) => PathOptions = ( + id: string, + fill: string, + strokeWidth: number, + stroke: string, + opacity: number, + strokeDasharray: string, + d: string +): PathOptions => { + return { + id, + fill, + stroke, + strokeWidth, + strokeDasharray, + opacity, + d + }; +}; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/MarkerRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/MarkerRenderer.tsx new file mode 100644 index 0000000..1f7e675 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/MarkerRenderer.tsx @@ -0,0 +1,779 @@ +// Import necessary types and modules +import { ChartBorderProps, ChartLocationProps, ChartSeriesProps } from '../../base/interfaces'; +import { createRectOption, drawSymbol, setBorderColor, setPointColor } from '../../utils/helper'; +import { createMarkerPathOption } from './MarkerBase'; +import { JSX } from 'react'; +import { doInitialAnimation, interpolatePathD } from './SeriesAnimation'; +import { ChartMarkerShape } from '../../base/enum'; +import { MarkerElementData, markerOptions, MarkerOptions, MarkerOptionsList, MarkerPosition, MarkerProperties, PathOptions, PointRenderingEvent, Points, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { isAxisZoomed } from '../Zooming/zooming'; + +/** + * Array of available marker shapes that can be used in chart series. + */ +export const markerShapes: ChartMarkerShape[] = ['Circle', 'Triangle', 'Diamond', 'Rectangle', 'Pentagon', 'InvertedTriangle', 'VerticalLine', 'Cross', 'Plus', 'HorizontalLine', 'Star']; + +/** + * Animates the marker element with easing effect. + * + * @param {MarkerElementData} elementData - The marker element data to show after delay + * @param {number} delay - The delay in milliseconds before showing the marker + * @param {number} duration - The duration of the animation in milliseconds + * @returns {void} + */ +export const markerAnimate: ( + elementData: MarkerElementData, + delay: number, + duration: number +) => void = ( + elementData: MarkerElementData, + delay: number, + duration: number +) => { + const originalOpacity: number = elementData.opacity !== undefined ? elementData.opacity : 1; + elementData.opacity = 0; + setTimeout(() => { + elementData.opacity = originalOpacity; + }, delay - duration / 10); +}; + +/** + * Responsible for rendering and animating data point markers in chart series + * + * The MarkerRenderer handles: + * - Creating SVG elements for markers + * - Positioning markers at the correct data points + * - Applying proper styling (fill, stroke, shape) + * - Animating markers when data changes + * - Managing marker visibility and clipping + * - Supporting custom point rendering through callbacks + */ +export const MarkerRenderer: { + markerElementsMap: Map; + render: (series: SeriesProperties ) => { + options: Object; + symbolGroup: Object; + markerOptionsList: Object[]; + }; + createElement(series: SeriesProperties): { + options: Object; + symbolGroup: Object; + }; + renderMarker(series: SeriesProperties, point: Points, + location: ChartLocationProps, index: number, symbolGroup: Object): Element | undefined; + doMarkerAnimation(series: SeriesProperties): void; + updateMarkerClipRect(series: SeriesProperties, progress: number): void; + getInitialClipPath(series: SeriesProperties): string; + +} = { + /** + * Map to store marker elements for each series. + * The key is a combination of chart element ID and series index. + */ + markerElementsMap: new Map(), + /** + * Renders markers for all visible points in a series. + * + * @param {SeriesProperties} series - The series containing the points + * @returns {Object} An object containing options, symbol group, and marker options list + */ + render: (series: SeriesProperties) => { + const markerOptionsList: Object[] = []; + const { options, symbolGroup } = MarkerRenderer.createElement(series); + const seriesKey: string = `${series.chart.element.id}_${series.index}`; + const markerElements: Element[] = []; + + // Use visiblePoints for consistent indexing with animation + const pointsToProcess: Points[] = series.visiblePoints as Points[]; + + for (const point of pointsToProcess) { + void (point.visible && point.symbolLocations?.length && + point.symbolLocations.forEach((location: ChartLocationProps, index: number) => { + const markerOptions: Element | undefined = series.marker?.shape !== 'None' + ? MarkerRenderer.renderMarker(series, point, location, index, symbolGroup) + : undefined; + + void (markerOptions && ( + markerOptionsList.push(markerOptions), + markerElements.push(markerOptions) + )); + }) + ); + } + + MarkerRenderer.markerElementsMap.set(seriesKey, markerElements); + void (series.marker!.shape && (markerOptionsList as Object[]).push(series.marker!.shape)); + + void (series.animation?.enable && MarkerRenderer.doMarkerAnimation(series)); + return { options, symbolGroup, markerOptionsList }; + }, + + + /** + * Creates the base elements needed for marker rendering. + * + * @param {SeriesProperties} series - The series for which to create elements + * @returns {Object} An object containing options and symbol group for the markers + */ + createElement(series: SeriesProperties): { options: Object; symbolGroup: Object } { + const marker: MarkerProperties = series.marker as MarkerProperties; + const explodeValue: number = ((marker.border?.width as number) + (series.chart.zoomRedraw && + isAxisZoomed(series.chart.axisCollection) ? 0 : 8 + 5)); + const index: number | string = series.index as number; + let options: Object = {}; + let symbolGroup: Object = {}; + const transform: string = 'translate(' + series.clipRect?.x + ',' + (series.clipRect?.y) + ')'; + + void (marker.visible && ( + ((markerHeight: number, markerWidth: number) => { + options = createRectOption( + series.chart.element.id + '_ChartMarkerClipRect_' + index, + 'transparent', + { width: 1, color: 'Gray' }, + 1, + { + x: -markerWidth, + y: -markerHeight, + width: series.clipRect!.width + (markerWidth * 2), + height: series.clipRect!.height + markerHeight * 2 + }, + 0, + 0, + '', + series.marker?.border?.dashArray + ); + + symbolGroup = { + 'id': series.chart.element.id + 'SymbolGroup' + index, + 'transform': transform, + 'clip-path': 'url(#' + series.chart.element.id + '_ChartMarkerClipRect_' + index + ')' + }; + })(((marker.height!) + explodeValue) / 2, ((marker.width!) + explodeValue) / 2) + )); + + return { options, symbolGroup }; + }, + + + /** + * Renders a single marker for a data point. + * + * @param {SeriesProperties} series - The series containing the point + * @param {Points} point - The data point for which to render a marker + * @param {ChartLocationProps} location - The location where the marker should be rendered + * @param {number} index - The index of the location when multiple locations exist + * @param {Object} symbolGroup - The SVG group element where the marker will be added + * @returns {Element|undefined} The marker element or undefined if marker is cancelled or not rendered + */ + renderMarker( + series: SeriesProperties, + point: Points, + location: ChartLocationProps, + index: number, + symbolGroup: SVGGElement + ): Element | undefined { const seriesIndex: number | string = series.index as number; + const marker: MarkerProperties = series.marker as MarkerProperties; + series.marker!.shape = series.marker!.shape ? series.marker!.shape : markerShapes[seriesIndex as number % 10]; + + const border: ChartBorderProps = { + color: marker.border!.color, + width: marker.border!.width + }; + const borderColor: string = marker.border!.color as string; + + location.x = location.x + (marker.offset?.x as number); + location.y = location.y - (marker.offset?.y as number); + const fill: string = marker.fill || ((series.marker!.filled) ? point.interior || series.interior : '#ffffff'); + let markerElementOptions: Element | undefined = undefined; + const parentElement: Element = symbolGroup; + border.color = borderColor || setPointColor(point, series.interior); + const symbolId: string = series.chart.element.id + '_Series_' + seriesIndex + '_Point_' + + (point.index) + '_Symbol' + (index); + + const argsData: PointRenderingEvent = { + cancel: false, + seriesName: series.name as string, + point: point, + fill: fill, + border: { + color: border.color, + width: border.width + }, + markerHeight: marker.height, + markerWidth: marker.width, + markerShape: marker.shape as ChartMarkerShape + }; + argsData.border = setBorderColor(point, { width: argsData.border.width, color: argsData.border.color }); + void (!series.isRectSeries && (point.color = argsData.fill)); + + const markerFill: string = argsData.fill; + const markerBorder: ChartBorderProps = { color: argsData.border.color, width: argsData.border.width }; + const markerWidth: number = argsData.markerWidth as number; + const markerHeight: number = argsData.markerHeight as number; + const markerOpacity: number = marker.opacity as number; + const markerShape: ChartMarkerShape = argsData.markerShape as ChartMarkerShape; + const imageURL: string = marker.imageUrl as string; + + const shapeOption: PathOptions = createMarkerPathOption( + symbolId, markerFill, markerBorder.width as number, markerBorder.color as string, markerOpacity, series.marker?.border!.dashArray as string, '' + ); + + void ((parentElement !== undefined && parentElement !== null) && (() => { + markerElementOptions = drawSymbol( + location, markerShape, + { width: markerWidth, height: markerHeight }, + imageURL, shapeOption + ); + })()); + if (markerElementOptions) { + (markerElementOptions as any).pointIndex = point.index; + } + point.marker = { + border: markerBorder, + fill: markerFill, + height: markerHeight, + visible: true, + shape: markerShape, + width: markerWidth, + imageUrl: imageURL + }; + + return markerElementOptions; + }, + + /** + * Performs animation for all markers in a series. + * For line series, markers are animated sequentially based on their distance. + * + * @param {SeriesProperties} series - The series containing the markers to animate + * @returns {void} Nothing is returned + */ + doMarkerAnimation(series: SeriesProperties): void { + if (series.propsChange || series.isLegendClicked || series.skipMarkerAnimation) { + return; + } + + // Store animation progress for this series in the series object + if (!series.markerAnimationProgress) { + series.markerAnimationProgress = 0; + } + series.markerClipPath = MarkerRenderer.getInitialClipPath(series); + const animationDelay: number = series.animation?.delay ?? 0; + const animationDuration: number = series.animation?.duration as number; + + // Start the clipRect animation + const startTime: number = performance.now(); + + const animateClipRect: (currentTime: number) => void = (currentTime: number) => { + const elapsed: number = currentTime - startTime - animationDelay; + + if (elapsed < 0) { + requestAnimationFrame(animateClipRect); + return; + } + + const progress: number = Math.min(elapsed / animationDuration, 1); + series.markerAnimationProgress = progress; + + // Update the marker clipRect based on animation progress + MarkerRenderer.updateMarkerClipRect(series, progress); + + if (progress < 1) { + requestAnimationFrame(animateClipRect); + } + }; + + requestAnimationFrame(animateClipRect); + }, + + /** + * Returns the initial CSS clip-path value for a chart series, + * used to hide the series before animation begins. + * + * @param {SeriesProperties} series - The series configuration containing chart and axis properties. + * @returns {string} A CSS clip-path string that hides the series initially. + */ + getInitialClipPath(series: SeriesProperties): string { + const isTransposed: boolean = series.chart?.iSTransPosed ?? false; + const isXAxisInverse: boolean = series.xAxis?.isAxisInverse ?? false; + const isYAxisInverse: boolean = series.yAxis?.isAxisInverse ?? false; + + if (!isTransposed) { + // Normal orientation - hide all by clipping horizontally + if (isXAxisInverse) { + return 'inset(0 0 0 100%)'; // Hide from left + } else { + return 'inset(0 100% 0 0)'; // Hide from right + } + } else { + // Inverted orientation - hide all by clipping vertically + // **KEY FIX: Transposed mode - hide all by clipping vertically** + if (isYAxisInverse) { + return 'inset(100% 0 0 0)'; // Hide from top + } else { + return 'inset(0 0 100% 0)'; // Hide from bottom + } + } + }, + + /** + * Updates the clip-path of markers in a chart series based on animation progress. + * This function ensures that markers are revealed progressively during animation. + * + * @param {SeriesProperties} series - The series configuration containing visible points. + * @param {number} progress - A value between 0 and 1 indicating animation progress. + * @returns {void} Nothing is returned. + */ + updateMarkerClipRect(series: SeriesProperties, progress: number): void { + if (!series.visiblePoints || series.visiblePoints.length === 0) { + return; + } + + // Get all marker positions + const markerPositions: ChartLocationProps[] = []; + series.visiblePoints.forEach((point: Points) => { + if (point.visible && point.symbolLocations?.length) { + markerPositions.push(...point.symbolLocations); + } + }); + + if (markerPositions.length === 0) { + return; + } + if (progress >= 1) { + series.markerClipPath = undefined; // Remove clipPath completely + return; + } + + // Calculate the range of marker positions + const xCoords: number[] = markerPositions.map((pos: ChartLocationProps) => pos.x); + const yCoords: number[] = markerPositions.map((pos: ChartLocationProps) => pos.y); + + const minX: number = Math.min(...xCoords); + const maxX: number = Math.max(...xCoords); + const minY: number = Math.min(...yCoords); + const maxY: number = Math.max(...yCoords); + + const isInverted: boolean = series.chart?.requireInvertedAxis ?? false; + const isXAxisInverse: boolean = series.xAxis?.isAxisInverse ?? false; + const isYAxisInverse: boolean = series.yAxis?.isAxisInverse ?? false; + + // Calculate animated clip dimensions + let clipPath: string = ''; + + if (!isInverted) { + // Normal orientation - clip horizontally + const range: number = maxX - minX; + const animWidth: number = range * progress; + + if (isXAxisInverse) { + // X-axis inverted - animate from right to left + clipPath = `inset(0 0 0 ${range - animWidth}px)`; + } else { + // X-axis normal - animate from left to right + clipPath = `inset(0 ${range - animWidth}px 0 0)`; + } + } else { + // Inverted orientation - clip vertically + const range: number = maxY - minY; + const animHeight: number = range * progress; + + if (isYAxisInverse) { + // Y-axis inverted - animate from top to bottom + clipPath = `inset(${Math.max(0, range - animHeight)}px 0 0 0)`; + } else { + // Y-axis normal - animate from bottom to top + clipPath = `inset(${Math.max(0, range - animHeight)}px 0 0 0)`; + } + } + + // Store the clip path in the series for use in renderMarkerJSX + series.markerClipPath = clipPath; + } + +}; + + + +/** + * Renders marker elements as React JSX components. + * + * @param {MarkerProperties[]} marker - Array containing marker options + * @param {number} index - The index of the marker in the array + * @param {number} animationProgress - The progress of the animation (optional). + * @param {string} seriesType - The type of the series (optional). + * @param {string} chartElementId - The ID of the chart element (optional). + * @returns {JSX.Element | null} React JSX element for rendering markers + */ +/** + * Track previous marker positions for smooth transitions + * Maps chart and series ids to marker positions + */ +/** + * Global store for tracking marker positions across chart animations + * This ensures markers can follow the exact same path as their series + */ +export const previousMarkerPositions: Map> = new Map>(); + +/** Sanitizes and returns a valid path 'd' attribute string. + * + * @param {string} d - The input path 'd' attribute string to sanitize. + * @returns {string} The sanitized path 'd' attribute string. + * @private + */ +export function sanitizePathD(d: string): string { + if (!d || typeof d !== 'string') {return ''; } + + let cleaned: string = d.replace(/[^MLHVCSQTAZmlhvcsqtaz0-9eE.,\s-]/g, ''); + cleaned = cleaned.replace(/[-+]?\d*\.?\d+e[-+]?\d+/gi, '0'); + cleaned = cleaned.replace(/([a-zA-Z])(?=\d)/g, '$1 '); + cleaned = cleaned.replace(/(\d)(?=[a-zA-Z])/g, '$1 '); + + return cleaned.replace(/\s+/g, ' ').trim(); +} + +export function formatAccessibilityDescription(point: Points, series: ChartSeriesProps): string { + const format: string = series.accessibility?.descriptionFormat as string; + return format + .replace('${series.name}', series && series.name ? series.name : '') + .replace('${point.x}', point && point.x ? point.x.toString() : '') + .replace('${point.y}', point && point.y ? point.y.toString() : ''); +} + +/** + * Renders marker elements as React JSX components. + * + * This function creates SVG elements for each marker in a chart series, handling + * proper positioning, animation, and transitions between states. + * + * @param {MarkerProperties[]} marker - Array of marker configuration objects + * @param {number} _seriesIndexes - Index of the series in the chart + * @param {number} [animationProgress=1] - Animation progress value between 0 and 1 + * @param {string} [seriesType] - Type of series (Line, Bar, Scatter, etc.) + * @param {string} [chartElementId] - Unique identifier of the chart element + * @param {boolean} [propsChange] - Whether the component props have changed + * @param {SeriesProperties} [series] - Series properties object + * @param {number} [addProgress] - Animation progress for adding new points + * @param {number} [removeProgress] - Animation progress for removing points + * @returns {JSX.Element | null} React JSX element for rendering markers or null if no markers + * + */ +export const renderMarkerJSX: ( + marker: MarkerProperties[], + seriesIndexes: number, + animationProgress?: number, + seriesType?: string, + chartElementId?: string, + propsChange?: boolean, + series?: SeriesProperties, + addProgress?: number, + removeProgress?: number +) => JSX.Element | null = ( + marker: MarkerProperties[], + _seriesIndexes: number, + animationProgress: number = 1, + seriesType?: string, + chartElementId?: string, + propsChange?: boolean, + series?: SeriesProperties, + addProgress: number = animationProgress, + removeProgress: number = animationProgress +): JSX.Element | null => { + if (!marker || marker.length === 0 ) { return null; } + if (!series?.marker?.visible && seriesType !== 'Bubble' && seriesType !== 'Scatter') { + return null; + } + // Get or initialize positions map for this chart + const chartKey: string = chartElementId as string; + const chartPositions: Map = + previousMarkerPositions.get(chartKey) || + (() => { + const newMap: Map = new Map(); + previousMarkerPositions.set(chartKey, newMap); + return newMap; + })(); + + return ( + <> + {marker.map((markerData: MarkerProperties, seriesIndex: number) => { + if (!markerData) { return null; } + const clipPathUrl: string = seriesType === 'Bubble' + ? `url(#${chartElementId}_ChartSeriesClipRect_${seriesIndex})` + : `url(#${markerData.options?.id})`; + + // Get the current series to check for marker animation progress + const currentSeries: SeriesProperties | undefined = series; + const isInitialAnimation: boolean | undefined = currentSeries?.animation?.enable && + !propsChange && + !currentSeries?.isLegendClicked && + !currentSeries?.skipMarkerAnimation; + + return ( + + + + + + + {markerData.markerOptionsList && markerData.markerOptionsList.length > 0 && + markerData.markerOptionsList.map((option: MarkerOptions, markerIndex: number) => { + const markerOptionsLength: number = markerData.markerOptionsList?.length as number; + const lastMarkerOption: MarkerOptions | undefined = markerData.markerOptionsList?.[markerOptionsLength - 1]; + const shape: string = typeof lastMarkerOption === 'string' ? lastMarkerOption : + (lastMarkerOption?.shape as string); + let seriesIndexes: number = seriesIndex; // fallback to loop index + let pointIndex: number = markerIndex; // fallback to marker index + if (option.id) { + const match: RegExpMatchArray = option.id.match(/_Series_(\d+)_Point_(\d+)_/) as RegExpMatchArray; + if (match) { + seriesIndexes = parseInt(match[1], 10); + pointIndex = parseInt(match[2], 10); + } + } + // Create a unique key for the marker to track its position using extracted indices + const markerKey: string = `${seriesIndexes}_${pointIndex}`; + + + // Get current position + const currentCx: number = option.cx as number; + const currentCy: number = option.cy as number; + + // Get previous position + let previousCx: number = option.previousCx as number; + let previousCy: number = option.previousCy as number; + const getEasingPower: (type?: string) => number = (type?: string): number => { + const QUADRATIC_EASING: number = 2.0; // Strong acceleration/deceleration for sharp movements + const LINEAR_EASING: number = 1.0; // Smooth consistent motion + const MEDIUM_EASING: number = 1.5; // Moderate acceleration/deceleration + // Different easing powers for different series types + switch (type?.toLowerCase()) { + case 'line': + case 'stepline': + case 'bar': + case 'column': + return QUADRATIC_EASING; + case 'spline': + case 'splinearea': + return LINEAR_EASING; // Linear movement for naturally smooth curves + + case 'area': + return MEDIUM_EASING; // Middle ground easing + + default: + return LINEAR_EASING; // Default linear easing + } + }; + + // Get the appropriate easing power for this series + const easingPower: number = getEasingPower(seriesType); + + // If no previous explicit position, check our tracking map + if (previousCx === undefined || previousCy === undefined) { + const seriesPositions: markerOptions[] | undefined = chartPositions.get(markerKey); + if (seriesPositions && seriesPositions[markerIndex as number]) { + const pos: markerOptions = seriesPositions[markerIndex as number]; + previousCx = pos.cx; + previousCy = pos.cy; + + if (pos.cx === undefined && pos.cy === undefined && series?.isPointAdded) { + series.isPointRemoved = false; + if (marker && marker[seriesIndex as number] && + (marker[seriesIndex as number] as + { markerOptionsList: MarkerOptions[] }).markerOptionsList) { + const markerObj: { + markerOptionsList: MarkerOptions[]; + } = marker[seriesIndex as number] as { markerOptionsList: MarkerOptions[] }; + if (markerObj.markerOptionsList && markerObj.markerOptionsList.length >= 3) { + const seriesPositionsAdd: MarkerPosition[] = chartPositions.get(`${seriesIndex}_${pointIndex - 1}`) as MarkerPosition[]; + previousCx = shape === 'Circle' ? seriesPositionsAdd[pointIndex - 1]?.cx as number : 0; + previousCy = + shape === 'Circle' ? seriesPositionsAdd[pointIndex - 1]?.cy as number : 0; + } + } + } + else if (series?.isPointRemoved) { + series.isPointAdded = false; + const seriesIdx: number = seriesIndex; + const markerList: MarkerOptionsList | undefined = marker && marker[seriesIdx as number] && + (marker[seriesIdx as number]).markerOptionsList; + const markerOpt: MarkerOptions | undefined = markerList && + markerList[markerIndex as number] as MarkerOptions; + const pointIdx: number = markerOpt?.pointIndex as number; + const seriesMap: MarkerPosition[] | undefined = chartPositions.get(`${seriesIdx}_${pointIdx as number + 1}`); + if ( + typeof pointIdx === 'number' && + seriesMap && + seriesMap[`${pointIdx + 1}`] + ) { + let pos: markerOptions = seriesMap[`${pointIdx + 1}`]; + previousCx = pos?.cx; + previousCy = pos?.cy; + if (previousCx === undefined) { + pos = seriesMap[`${pointIdx}`]; + previousCx = pos?.cx; + previousCy = pos?.cy; + } + } + } + } + } + + let progressValue: number = animationProgress; + if (series?.isPointAdded) {progressValue = addProgress; } + else if (series?.isPointRemoved) {progressValue = removeProgress; } + let cx: number = 0; + let cy: number = 0; + + if ((series?.type === 'Scatter' || series?.type === 'Bubble') && series.isLegendClicked) { + cx = currentCx; + cy = currentCy; + } else { + cx = previousCx !== undefined && !propsChange + ? previousCx + (currentCx - previousCx) * Math.pow(progressValue, + series?.isPointAdded || + series?.isPointRemoved ? 1 : easingPower) + : currentCx; + + cy = previousCy !== undefined && !propsChange + ? (previousCy + (currentCy - previousCy) * Math.pow(progressValue, + series?.isPointAdded || series?.isPointRemoved ? 1 : + easingPower)) as number + : currentCy as number; + } + + + const currentD: string = option.d as string; + // Get previous path data + let previousD: string = ''; + + previousD = chartPositions.get(markerKey)?.[markerIndex as number]?.d as string ?? previousD; + + // Store current position and path for future animations if animation is complete + if (animationProgress === 1 && series) { + void (chartPositions.has(markerKey) ? null : chartPositions.set(markerKey, [])); + const positions: markerOptions[] = chartPositions.get(markerKey)!; + positions[markerIndex as number] = { + cx: currentCx, + cy: currentCy, + d: currentD, + pathShape: shape as string + }; + if (series?.isPointRemoved) { + series.isPointAdded = false; + series.isPointRemoved = false; + } + } + let animatedOpacity: number = option.opacity; + let animatedRx: number = option.rx as number; + let animatedRy: number = option.ry as number; + + if ((seriesType === 'Scatter' || seriesType === 'Bubble') && + series?.animation?.enable && + !propsChange && + !series?.isLegendClicked && + !series?.skipMarkerAnimation) { + + const animationState: { + opacity: number; + scale: number; + } = doInitialAnimation(series, animationProgress); + animatedOpacity = (option.opacity as number) * animationState.opacity; + animatedRx = (option.rx as number) * animationState.scale; + animatedRy = (option.ry as number) * animationState.scale; + } + if ((shape === 'Circle' || option.shape === 'Circle') && option.d === '' && (option.href === undefined || option.href === '')) { + return ( + + ); + } else if ((shape === 'Image' || option.shape === 'Image') && option.d === '') { + return ( + + ); + } else { + // For non-Circle shapes like Triangle, interpolate the path + // Get current path data + const previousD: string = sanitizePathD(chartPositions.get(markerKey as string)?.[markerIndex as number]?.d ?? ''); + const currentD: string = sanitizePathD(option.d as string); + let interpolatedPath: string = currentD; + + + if (chartPositions.has(markerKey) && chartPositions.get(markerKey as string)?.[markerIndex as number] && + previousD !== currentD && series?.type !== 'Scatter') { + + interpolatedPath = interpolatePathD(previousD, + currentD, animationProgress, series?.isLegendClicked, series?.type); + } + return ( + + ); + } + })} + + ); + })} + + ); +}; + +export default MarkerRenderer; + + diff --git a/components/charts/src/chart/renderer/SeriesRenderer/ProcessData.tsx b/components/charts/src/chart/renderer/SeriesRenderer/ProcessData.tsx new file mode 100644 index 0000000..e2139b7 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/ProcessData.tsx @@ -0,0 +1,741 @@ +import { DataManager, DataUtil, Query } from '@syncfusion/react-data'; +import { useData } from '../../common/data'; +import { isNullOrUndefined } from '@syncfusion/react-base'; +import { EmptyPointMode } from '../../base/enum'; +import { findSeriesCollection, isRectangularSeriesType, setRange, useVisiblePoints } from '../../utils/helper'; +import { refreshAxisLabel } from '../AxesRenderer/AxisRender'; +import { Chart, Points, SeriesProperties } from '../../chart-area/chart-interfaces'; + +/** + * Constants used in the ProcessData component + */ +const PROCESS_DATA_CONSTANTS: { + readonly DEFAULT_PADDING: number; + readonly CHART_AREA_PADDING: number; + readonly COLLISION_PADDING: number; + readonly MAX_RENDER_COUNT: number; + readonly DEFAULT_RENDER_COUNT: number; + readonly INFINITY_VALUE: number; + readonly NEGATIVE_INFINITY_VALUE: number; +} = { + DEFAULT_PADDING: 5, + CHART_AREA_PADDING: 10, + COLLISION_PADDING: 5, + MAX_RENDER_COUNT: 100, + DEFAULT_RENDER_COUNT: 0, + INFINITY_VALUE: Infinity, + NEGATIVE_INFINITY_VALUE: -Infinity +} as const; + +type DataRecord = Record; + +/** + * Handles the success event when the DataManager fetches data for the circular 3D series. + * + * @private + * @param {Object} dataObject - Specifies the series data object. + * @param {Object} dataObject.result - The actual data. + * @param {number} dataObject.count - The count of data. + * @param {Series} [series] - Optional series instance to be updated with fetched data. + * @returns {void} + */ +const dataManagerSuccess: (dataObject: { result: Object; count: number }, series?: SeriesProperties) => void = + (dataObject: { result: Object; count: number }, series?: SeriesProperties) => { + [series].filter(Boolean).forEach((seriesArray: SeriesProperties | undefined) => { + // Skip if we already have points data + if (seriesArray && (!seriesArray.points || seriesArray.points.length === 0)) { + seriesArray.currentViewData = dataObject.count ? dataObject.result : []; + processJsonData(seriesArray); + seriesArray.recordsCount = dataObject.count; + seriesArray.currentViewData = null; + } + }); + }; + +/** + * Processes data for the series. + * + * @hidden + * @param {Series} series - The chart series object to process. + * @returns {void} + */ +export const processJsonData: (series: SeriesProperties) => void = (series: SeriesProperties) => { + let i: number = 0; + const point: Points = createPoint(); + const xName: string = series.xField as string; + const textMappingName: string = series.marker?.dataLabel?.labelField ? + series.marker.dataLabel.labelField : ''; + // Check if currentViewData exists and is an array before accessing length + if (!series.currentViewData || !Array.isArray(series.currentViewData)) { + series.points = []; + series.xMin = PROCESS_DATA_CONSTANTS.INFINITY_VALUE; + series.xMax = PROCESS_DATA_CONSTANTS.NEGATIVE_INFINITY_VALUE; + series.yMin = PROCESS_DATA_CONSTANTS.INFINITY_VALUE; + series.yMax = PROCESS_DATA_CONSTANTS.NEGATIVE_INFINITY_VALUE; + series.sizeMax = PROCESS_DATA_CONSTANTS.NEGATIVE_INFINITY_VALUE; + return; // Exit early if no valid data + } + + const len: number = (series.currentViewData as object[]).length; + series.points = []; + series.xMin = PROCESS_DATA_CONSTANTS.INFINITY_VALUE; + series.xMax = PROCESS_DATA_CONSTANTS.NEGATIVE_INFINITY_VALUE; + series.yMin = PROCESS_DATA_CONSTANTS.INFINITY_VALUE; + series.yMax = PROCESS_DATA_CONSTANTS.NEGATIVE_INFINITY_VALUE; + series.sizeMax = PROCESS_DATA_CONSTANTS.NEGATIVE_INFINITY_VALUE; + getSeriesType(series); + + // Check if xAxis exists before accessing its properties + if (!series.xAxis) { + return; // Exit if no xAxis is defined + } + + // Process points based on axis type + if (series.xAxis.valueType === 'Category') { + while (i < len) { + pushCategoryPoint(series, point, i, xName, textMappingName); + i++; + } + } else if (series.xAxis.valueType === 'DateTime') { + while (i < len) { + pushDateTimePoint(series, point, i, xName, textMappingName); + i++; + } + } else { + while (i < len) { + pushDoublePoint(series, point, i, xName, textMappingName); + i++; + } + } +}; + +/** + * Finds the type of the series. + * + * @private + * @param {Series} series - The series object whose type needs to be identified. + * @returns {void} + */ +const getSeriesType: (series: SeriesProperties) => void = (series: SeriesProperties): void => { + [series].filter(Boolean).forEach((s: SeriesProperties) => { + const type: string = 'XY'; + s.seriesType = type; + }); +}; + +/** + * Refreshes the data manager for the series. + * + * @param {Series} series — The series needing source data fetch/refresh. + * @returns {void} + */ +export const refreshDataManager: (series: SeriesProperties) => void = (series: SeriesProperties): void => { + if (series.points && series.points.length > 0 && (series.dataSource instanceof DataManager)) { + return; + } + + const dataSource: Object | DataManager | undefined = series.dataSource; + + // Handle local array data source directly + if (Array.isArray(dataSource) && !(dataSource instanceof DataManager) && !series.query) { + dataManagerSuccess({ result: dataSource, count: dataSource.length }, series); + triggerChartRerender(series); + return; + } + + // Handle remote data source using DataManager + if (series.dataModule && !series.dataFetchRequested) { + let dataQuery: Query; + + series.dataFetchRequested = true; + + if (typeof series.dataModule.generateQuery === 'function') { + dataQuery = series.dataModule.generateQuery(); + } else if (series.query instanceof Query) { + dataQuery = series.query; + } else { + dataQuery = new Query(); + } + + if (dataQuery instanceof Query && typeof dataQuery.requiresCount === 'function') { + dataQuery = dataQuery.requiresCount(); + } + + // Execute the query + if (typeof series.dataModule.dataManager?.executeQuery === 'function') { + const dataPromise: Promise = series.dataModule.dataManager.executeQuery(dataQuery); + + dataPromise + .then((e: Object) => { + // Reset the request flag after data is received + series.dataFetchRequested = false; + + // Process data only if points don't already exist + if (!series.points || series.points.length === 0) { + dataManagerSuccess(e as { result: Object; count: number }, series); + + if (series.chart?.element) { + triggerChartRerender(series); + } + } + }); + } + } +}; + +const dataCache: { + previousSignatures: Record; + pendingRenders: Record; + renderCount: Record; +} = { + previousSignatures: {} as Record, + pendingRenders: {} as Record, + renderCount: {} as Record +}; + +export const triggerChartRerender: (series: SeriesProperties) => void = (series: SeriesProperties): void => { + if (!series.chart?.triggerRemeasure) { + return; + } + + const chartId: string = series.chart.element?.id || + `chart-${series.name}-${Math.random().toString(36).substr(2, 9)}`; + + dataCache.renderCount[chartId as string] ||= PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + + // Skip update if we don't have points yet + if (!series.points || series.points.length === 0) { + return; + } + + const points: Points[] = series.points; + const dataSignature: string = JSON.stringify({ + count: points.length, + first: points[0]?.xValue, + last: points[points.length - 1]?.xValue, + yMin: series.yMin, + yMax: series.yMax, + xMin: series.xMin, + xMax: series.xMax + }); + + if (dataSignature !== dataCache.previousSignatures[chartId as string] && + !dataCache.pendingRenders[chartId as string]) { + + dataCache.previousSignatures[chartId as string] = dataSignature; + dataCache.pendingRenders[chartId as string] = true; + dataCache.renderCount[chartId as string]++; + + if (series.chart?.triggerRemeasure) { + series.chart.triggerRemeasure(); + } + dataCache.pendingRenders[chartId as string] = false; + } +}; + + +/** + * Creates and returns a new data point with default attribute values. + * + * @returns {Points} A new data point object with default values. + * @private + */ +export function createPoint(): Points { + return { + x: {}, + y: {}, + visible: true, + text: '', + tooltip: '', + color: '', + symbolLocations: null, + xValue: null, + yValue: null, + index: 0, + regions: null, + percentage: null, + isEmpty: false, + regionData: null, + minimum: 0, + maximum: 0, + interior: '', + series: {}, + isPointInRange: true, + marker: { + visible: false + }, + size: {}, + originalY: 0, + textValue: '' + }; +} + +/** + * Processes and manages chart series data. + * + * @param {Series[]} chartSeries - An array of Series objects to be processed. + * @returns {Series[]} - Returns the processed series array. + */ +export const processChartSeries: (chartSeries: SeriesProperties[]) => SeriesProperties[] = (chartSeries: SeriesProperties[]) => { + const visibleSeries: SeriesProperties[] = chartSeries; + + /** + * Processes the data for each visible series. + * + * @returns {void} + */ + const processData: () => void = () => { + for (let i: number = 0, len: number = visibleSeries.length; i < len; i++) { + const series: SeriesProperties = visibleSeries[i as number]; + + // Skip series that are already completely processed + const isDataManager: boolean = series.dataSource as boolean && + typeof series.dataSource === 'object' && + !Array.isArray(series.dataSource) && + 'executeQuery' in (series.dataSource as any); + + // Skip if already processed and has points + if (series.points && series.points.length > 0 && isDataManager) { + refreshAxisLabel(series); + continue; + } + + // Initialize data module for the series + initializeDataModule(series); + } + }; + + /** + * Initializes the data module for the series and refreshes data management. + * + * @param {Series} series - The series for which the data module is initialized. + * @returns {void} This function does not return a value. + */ + const initializeDataModule: (series: SeriesProperties) => void = (series: SeriesProperties) => { + series.xData = []; + series.yData = []; + const dataSource: Object | DataManager | undefined = series.dataSource; + series.dataModule = useData(dataSource, series.query as Query); + series.points = []; + refreshDataManager(series); + }; + + + processData(); + return visibleSeries; +}; + +/** + * Pushes a category point to the data collection. + * + * @param {Series} series - The series model + * @param {Points} point - The point to be pushed. + * @param {number} index - The index of the point. + * @param {string} xName - The name of the x-coordinate. + * @param {string} textMappingName - The name of dataLabel mapping. + * @returns {void} + */ +export const pushCategoryPoint: (series: SeriesProperties, point: Points, index: number, xName: string, textMappingName: string) => void = +(series: SeriesProperties, point: Points, index: number, xName: string, textMappingName: string): void => { + point = dataPoint(index, textMappingName, xName, series); + pushCategoryData(point, point.x as string, series, index); + pushData(point, index, series); + setEmptyPoint(point, series, index); +}; +/** + * Pushes category data into the series points. + * + * @param {Points} point - The point to which category data will be pushed. + * @param {string} pointX - The x-value of the point. + * @param {Series} series - The series object. + * @param {number} index - The index of the point. + * @returns {void} + * @private + */ +export const pushCategoryData: (point: Points, pointX: string, series: SeriesProperties, index: number) => void = +(point: Points, pointX: string, series: SeriesProperties, index: number): void => { + if (!series.xAxis.indexed) { + if (series.xAxis.indexLabels && (series.xAxis.indexLabels as Record)[pointX as string] === undefined) { + (series.xAxis.indexLabels as Record)[pointX as string] = series.xAxis.labels.length; + series.xAxis.labels.push(pointX as string); + } + point.xValue = (series.xAxis.indexLabels as Record)[pointX as string]; + } else { + if (series.xAxis.labels[index as number]) { + series.xAxis.labels[index as number] += ', ' + pointX; + } + else { + series.xAxis.labels.push(pointX); + } + point.xValue = index; + } +}; + +/** + * Pushes a DateTime point to the data collection. + * + * @param {Series} series - The series object + * @param {Points} point -The point to be pushed. + * @param {number} index -The index of the point. + * @param {string} xName -The name of the x-coordinate. + * @param {string} textMappingName - The name of dataLabel mapping. + * @returns {void} + * @private + */ +export const pushDateTimePoint: (series: SeriesProperties, point: Points, index: number, xName: string, textMappingName: string) => void = +(series: SeriesProperties, point: Points, index: number, xName: string, textMappingName: string): void => { + point = dataPoint(index, textMappingName as string, xName, series); + if (!isNullOrUndefined(point.x) && point.x !== '') { + point.x = new Date( + (DataUtil.parse as Required).parseJson({ val: point.x }).val + ); + point.xValue = Date.parse(point.x.toString()); + pushData(point, index, series); + setEmptyPoint(point, series, index); + } else { + point.visible = false; + } +}; + +/** + * Pushes a double point to the data collection. + * + * @param {Series} series - The series to which the point belongs. + * @param {Points} point - The point to be pushed. + * @param {number} index - The index of the point. + * @param {string} xName - The name of the x-coordinate. + * @param {string} textMappingName - The name of dataLabel mapping. + * @returns {void} + * @private + */ +export const pushDoublePoint: (series: SeriesProperties, point: Points, index: number, xName: string, textMappingName?: string) => void = + (series: SeriesProperties, point: Points, index: number, xName: string, textMappingName?: string): void => { + point = dataPoint(index, textMappingName as string, xName, series); + point.xValue = point.x as number; + pushData(point, index, series); + setEmptyPoint(point, series, index); + }; + +/** + * Retrieves the data point at the specified index with the given text mapping name and x-name. + * + * @param {number} i - The index of the data point to retrieve. + * @param {string} textMappingName - The name of dataLabel mapping. + * @param {string} xName - The name used for the x-axis. + * @param {Series} series - The series object containing points and data. + * @returns {Points} - The data point at the specified index. + * @private + */ +const dataPoint: (i: number, textMappingName: string, + xName: string, series: SeriesProperties) => Points = (i: number, textMappingName: string, xName: string, series: SeriesProperties) => { + series.points[i as number] = createPoint(); + const point: Points = series.points[i as number]; + const currentViewData: Object = (series.currentViewData as DataRecord[])[i as number]; + point.x = getObjectValueByMappingString(xName, currentViewData as DataRecord) as Object; + point.y = getObjectValueByMappingString(series.yField!, currentViewData as DataRecord) as Object; + point.size = getObjectValueByMappingString(series.sizeField as string, currentViewData as DataRecord) as string; + point.tooltip = getObjectValueByMappingString((series.tooltipField as string), + currentViewData as DataRecord) as string; + point.text = getObjectValueByMappingString(textMappingName, currentViewData as DataRecord) as string; + point.interior = getObjectValueByMappingString(series.colorField as string, + currentViewData as DataRecord) as string; + return point; +}; + + +/** + * Sets the empty point values. + * + * @param {Points} point - The point to be set. + * @param {number} i - The index of the point. + * @param {Series} series - The series to which the point belongs. + * @private + * @returns {void} + */ +export const pushData: (point: Points, i: number, series: SeriesProperties) => void = +(point: Points, i: number, series: SeriesProperties): void => { + point.index = i; + point.yValue = point.y as number; + point.series = series; + series.xMin = Math.min(series.xMin, point.xValue as number); + series.xMax = Math.max(series.xMax, point.xValue as number); + series.xData.push(point.xValue as number); +}; + +const getObjectValueByMappingString: (mappingName: string, data: DataRecord) => string | number | boolean | Date | null = ( + mappingName: string, + data: DataRecord +) => + data && Object.prototype.hasOwnProperty.call(data, mappingName) + ? data[mappingName as string] + : null; + +export const setEmptyPoint: (point: Points, series: SeriesProperties, i: number) => void = +(point: Points, series: SeriesProperties, i: number): void => { + if (!findVisibility(point, series)) { + point.visible = true; + return; + } + point.isEmpty = true; + const mode: EmptyPointMode | undefined = point.isPointInRange ? series.emptyPointSettings?.mode : 'Drop'; + switch (mode) { + case 'Zero': + point.visible = true; + point.y = point.yValue = series.yData[i as number] = PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + break; + case 'Average': + point.y = point.yValue = series.yData[i as number] = + getAverage(series.yField as string, i as number, series, series.currentViewData as Object); + point.visible = true; + break; + case 'Drop': + case 'Gap': + series.yData[i as number] = PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + point.visible = false; + break; + } +}; + +/** + * Gets the average value of a member in the specified data array or current view data. + * + * @param {string} member - The member whose average is to be calculated. + * @param {number} i - The index of the data point. + * @param {Series} series - The series object containing currentViewData. + * @param {Object} data - The data array from which to calculate the average. Defaults to the current view data. + * @returns {number} - The average value of the specified member. + */ +export const getAverage: (member: string, i: number, series: SeriesProperties, data: Object) => +number = (member: string, i: number, series: SeriesProperties, data: any): number => { + data = series.currentViewData as Object; + const previous: number = data[i - 1] ? (data[i - 1][member as string]) : PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + const next: number = data[i + 1] ? (data[i + 1][member as string]) : PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + return (previous + next) / 2; +}; + +const findVisibility: (point: Points, series: SeriesProperties) => boolean = (point: Points, series: SeriesProperties): boolean => { + setXYMinMax(point.yValue as number, series); + series.yData.push(point.yValue as number); + if (series.type === 'Bubble') { + series.sizeMax = Math.max(series.sizeMax, (isNullOrUndefined(point.size) || isNaN(+point.size)) ? + series.sizeMax : point.size as number); + } + return isNullOrUndefined(point.x) || isNullOrUndefined(point.y) || isNaN(+point.y); +}; + +/** + * Updates the y-axis minimum and maximum values for the given series based on the specified y-value. + * + * @private + * @param {number} yValue - The y value used to determine the min and max for the series. + * @param {Series} series - The series for which the yMin and yMax should be updated. + * @returns {void} + */ +const setXYMinMax: (yValue: number, series: SeriesProperties) => void = (yValue: number, series: SeriesProperties): void => { + const isLogAxis: boolean = (series.yAxis.valueType === 'Logarithmic' || series.xAxis.valueType === 'Logarithmic'); + const isNegativeValue: boolean = yValue < 0 || series.yAxis.rangePadding === 'None'; + let seriesMinY: number; + if (isRectangularSeriesType(series, series.type!.indexOf('100') > -1) && !setRange(series.yAxis)) { + seriesMinY = ((isLogAxis ? (yValue) : isNegativeValue ? yValue : 0)); + } + else { + seriesMinY = yValue; + } + series.yMin = isLogAxis ? + Math.min(series.yMin, (isNullOrUndefined(seriesMinY) || isNaN(seriesMinY) || (seriesMinY === 0) || + (seriesMinY.toString() === '0') || (seriesMinY.toString() === '')) ? series.yMin : seriesMinY) : + Math.min(series.yMin, seriesMinY || series.yMin); + series.yMax = Math.max(series.yMax, yValue || series.yMax); +}; + + +/** + * Retrieves the value of the given mapping name from the data object. + * + * @param {string} mappingName - The name of the property to retrieve. + * @param {Record} data - The object containing key-value pairs. + * @returns {Object} The value corresponding to the mapping name. + */ +export const getObjectValue: (mappingName: string, data: Record +) => Object = (mappingName: string, data: Record): Object => { + return data[mappingName as string]; +}; + + +/** + * Calculates the stack values for the chart. + * + * @param {Chart} chart - The chart object to calculate stack values for. + * @returns {void} + */ +export const calculateStackValues: (chart: Chart) => void = (chart: Chart): void => { + let series: SeriesProperties; + let isCalculateStacking: boolean = false; + + for (let i: number = 0, len: number = chart.visibleSeries.length; i < len; i++) { + series = chart.visibleSeries[i as number] as SeriesProperties; + + if (series.visible) { + series.position = undefined; + series.rectCount = undefined; + } + + if (((series.type?.indexOf('Stacking') !== -1) || + ( series.drawType && series.drawType.indexOf('Stacking') !== -1)) && + !isCalculateStacking) { + + calculateStackedValue(chart); + isCalculateStacking = true; + } + } +}; + +/** + * Calculates the stacked value for the chart. + * + * @param {Chart} chart - The chart for which the stacked value is calculated. + * @returns {void} + */ +export const calculateStackedValue: (chart: Chart) => void = (chart: Chart): void => { + for (const columnItem of chart.columns) { + for (const item of chart.rows) { + calculateStackingValues(findSeriesCollection(columnItem, item, true)); + } + } +}; + +/** + * Calculates stacking values for a collection of series. + * + * @param {Series[]} seriesCollection - Collection of series for which stacking values are calculated. + * @returns {void} + */ +export const calculateStackingValues: (seriesCollection: SeriesProperties[]) => void + = (seriesCollection: SeriesProperties[]): void => { + let startValues: number[]; + let endValues: number[]; + let yValues: number[] = []; + const lastPositive: Map = new Map(); + const lastNegative: Map = new Map(); + let stackingGroup: string; + let lastValue: number; + let value: number; + + const groupingValues: Map = new Map(); + let visiblePoints: Points[] = []; + + // Group series by stackingGroup + for (let i: number = 0; i < seriesCollection.length; i++) { + const series: SeriesProperties = seriesCollection[i as number]; + const key: string = String(series.stackingGroup || ''); + + if (!groupingValues.has(key)) { + groupingValues.set(key, [series]); + } else { + const existingSeries: SeriesProperties[] = groupingValues.get(key) || []; + existingSeries.push(series); + groupingValues.set(key, existingSeries); + } + } + + // Process each group + groupingValues.forEach((seriesList: SeriesProperties[]) => { + const stackingSeies: SeriesProperties[] = []; + const stackedValues: number[] = []; + + for (const series of seriesList) { + if (series.type?.indexOf('Stacking') !== -1 || (series.drawType?.indexOf('Stacking') !== -1)) { + + stackingGroup = String(series.stackingGroup); + + if (!lastPositive.has(stackingGroup)) { + lastPositive.set(stackingGroup, []); + lastNegative.set(stackingGroup, []); + } + + yValues = series.yData; + startValues = []; + endValues = []; + stackingSeies.push(series); + visiblePoints = useVisiblePoints(series); + + for (let j: number = 0, pointsLength: number = visiblePoints.length; j < pointsLength; j++) { + lastValue = 0; + value = +yValues[j as number]; // Fix for chart not rendering while y value is given as string issue + + const xValueKey: number = visiblePoints[j as number].xValue || j; + const posValues: number[] = lastPositive.get(stackingGroup as string) || []; + const negValues: number[] = lastNegative.get(stackingGroup as string) || []; + + if (posValues[xValueKey as number] === undefined) { + posValues[xValueKey as number] = PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + lastPositive.set(stackingGroup, posValues); + } + + if (negValues[xValueKey as number] === undefined) { + negValues[xValueKey as number] = PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + lastNegative.set(stackingGroup, negValues); + } + + + stackedValues[j as number] = stackedValues[j as number] ? + stackedValues[j as number] + Math.abs(value) : Math.abs(value); + + if (value >= 0) { + lastValue = posValues[xValueKey as number] || PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + posValues[xValueKey as number] = lastValue + value; + lastPositive.set(stackingGroup, posValues); + } else { + lastValue = negValues[xValueKey as number] || PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + negValues[xValueKey as number] = lastValue + value; + lastNegative.set(stackingGroup, negValues); + } + + startValues.push(lastValue); + endValues.push(value + lastValue); + + } + + series.stackedValues = { startValues: startValues, endValues: endValues }; + + const isLogAxis: boolean = series.yAxis.valueType === 'Logarithmic'; + const isColumnBarType: boolean = (series.type?.indexOf('Column') !== -1 || series.type?.indexOf('Bar') !== -1); + + series.yMin = isLogAxis && isColumnBarType && series.yMin < 1 ? + series.yMin : + (series.yAxis.startFromZero && series.yAxis.rangePadding === 'Auto' && series.yMin >= 0) ? + PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT : + parseFloat((Math.min.apply(0, endValues)).toFixed(PROCESS_DATA_CONSTANTS.CHART_AREA_PADDING)); + + series.yMax = Math.max.apply(0, endValues); + + if (series.yMin > Math.min.apply(0, endValues)) { + series.yMin = isLogAxis && isColumnBarType && series.yMin < 1 ? series.yMin : Math.min.apply(0, endValues); + } + + if (series.yMax < Math.max.apply(0, startValues)) { + series.yMax = PROCESS_DATA_CONSTANTS.DEFAULT_RENDER_COUNT; + } + } + } + + findPercentageOfStacking(stackingSeies, stackedValues); + }); + }; + +/** + * Calculates the percentage for stacking series. + * + * @param {Series[]} stackingSeies - Collection of stacking series. + * @param {number[]} values - The stacked values. + * @returns {void} + */ +export const findPercentageOfStacking: (stackingSeies: SeriesProperties[], values: number[]) => void + = (stackingSeies: SeriesProperties[], values: number[]): void => { + for (const item of stackingSeies) { + for (const point of useVisiblePoints(item)) { + point.percentage = Math.abs(+((point.y as number) / values[point.index] * 100).toFixed(2)); + } + } + }; + diff --git a/components/charts/src/chart/renderer/SeriesRenderer/ScatterSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/ScatterSeriesRenderer.tsx new file mode 100644 index 0000000..1cfc7b5 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/ScatterSeriesRenderer.tsx @@ -0,0 +1,280 @@ +import { ChartLocationProps, EmptyPointSettings } from '../../base/interfaces'; +import { drawSymbol, getPoint, withInRange } from '../../utils/helper'; +import { LineBase, LineBaseReturnType } from './LineBase'; +import { ChartMarkerShape } from '../../base/enum'; +import { MarkerElementData, MarkerOptions, MarkerProperties, PointRenderingEvent, Points, RenderOptions, ScatterSeriesType, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { markerAnimate } from './MarkerRenderer'; + +export const SCATTER_MARKER_SHAPES: ChartMarkerShape[] = [ + 'Circle', 'Triangle', 'Diamond', 'Rectangle', + 'Pentagon', 'InvertedTriangle', 'VerticalLine', + 'Cross', 'Plus', 'HorizontalLine', 'Star'] as const; +const isLineShapeMarker: (shape: string) => boolean = (shape: string) => + shape === 'HorizontalLine' || shape === 'VerticalLine' || shape === 'Cross'; + +const lineBaseInstance: LineBaseReturnType = LineBase; + +const ScatterSeriesRenderer: ScatterSeriesType = { + /** + * Renders the scatter series. + * + * @param {SeriesProperties} series - The series to be rendered. + * @param {boolean} isInverted - Specifies whether the chart is inverted. + * @returns {Object} Returns the final series with assigned data point properties. + */ + render: (series: SeriesProperties, isInverted: boolean): + { options: RenderOptions[]; marker: MarkerProperties } => { + series.isRectSeries = false; + const marker: MarkerProperties = series.marker as MarkerProperties; + + if (!series.marker) { + series.marker = { + visible: true, + shape: 'Circle', + width: 5, + height: 5, + opacity: 1 + }; + } else if (series.marker.visible === undefined) { + series.marker.visible = true; + } + const visiblePoints: Points[] = lineBaseInstance.enableComplexProperty(series); + const markerShape: ChartMarkerShape = marker?.shape || SCATTER_MARKER_SHAPES[series.index % SCATTER_MARKER_SHAPES.length]; + if (!visiblePoints || visiblePoints?.length === 0) { + return { + options: [], + marker: {} + }; + } + + const markerOptionsList: MarkerOptions[] = []; + + /** + * Determines the border properties of each scatter points. + * * color - whether the color of series or marker. + * * width - whether the width of series or marker. + * * dashArray - whether the dimension of series or marker. + */ + const scatterBorder: { + width: number; + color: string; + dashArray: string; + } = { + width: isLineShapeMarker(markerShape) + ? series.width as number + : series.border?.width as number, + color: isLineShapeMarker(markerShape) + ? series.interior + : series.border?.color ?? series.interior, + dashArray: isLineShapeMarker(markerShape) + ? series.dashArray as string + : series.border?.dashArray as string + }; + + for (const point of visiblePoints) { + point.regions = []; + + if ( + point.visible && + withInRange( + series.points[point.index - 1], + point, + series.points[point.index + 1], + series + ) + ) { + const markerOption: MarkerOptions = ScatterSeriesRenderer.renderPoint( + point, + series, + markerShape, + scatterBorder, + isInverted + ) as MarkerOptions; + + if (markerOption) { + markerOptionsList.push(markerOption); + } + } + } + + const pathOptions: RenderOptions[] = []; + return { + options: pathOptions, + marker: { + markerOptionsList, + symbolGroup: { + id: `${series.chart.element.id}_Series_${series.index}_SymbolGroup`, + transform: `translate(${series.clipRect?.x}, ${series.clipRect?.y})` + } + } + }; + }, + + /** + * Renders scatters in each point. + * + * @param {Points} point - Respective data points. + * @param {SeriesProperties} series - Indicates the current series. + * @param {ChartMarkerShape} markerShape - Represents the shape of markers. + * @param {Object} scatterBorder - Border properties of scatter points. + * @param {boolean} isInverted - Indicates whether the chart is inverted or not. + * @returns {MarkerOptions} Returns the data points with finally assigned marker properties. + */ + renderPoint: ( + point: Points, + series: SeriesProperties, + markerShape: ChartMarkerShape, + scatterBorder: { width: number; color: string; dashArray: string }, + isInverted: boolean + ): MarkerOptions | null => { + const marker: MarkerProperties = series.marker as MarkerProperties; + const location: ChartLocationProps = getPoint( + point?.xValue as number, + point?.yValue as number, + series?.xAxis, + series?.yAxis, + isInverted + ); + + const isEmptyPoint: boolean = point.isEmpty || point.yValue === null; + + const emptyPointFillColor: string = (series.emptyPointSettings as EmptyPointSettings).fill + || series.interior; + const emptyPointBorderColor: string = (series.emptyPointSettings as EmptyPointSettings).border?.color + || series.border?.color || series.interior; + const emptyPointBorderWidth: number = (series.emptyPointSettings as EmptyPointSettings).border?.width + ?? series.border?.width as number; + + let fill: string = isEmptyPoint ? emptyPointFillColor : (point.interior || series.interior); + let border: { color: string; width: number; } = { + color: isEmptyPoint ? emptyPointBorderColor : scatterBorder.color, + width: isEmptyPoint ? emptyPointBorderWidth : scatterBorder.width + }; + let height: number = marker.height as number; + let width: number = marker.width as number; + let shape: ChartMarkerShape = markerShape as ChartMarkerShape; + + point.marker = { + border: border, fill: fill, + height: marker.height, visible: true, + width: marker.width, shape: markerShape, imageUrl: marker.imageUrl + }; + + const argsData: PointRenderingEvent = { + cancel: false, + seriesName: series.name as string, + point: point, + fill: fill, + border: border, + markerHeight: marker.height as number, + markerWidth: marker.width as number, + markerShape: markerShape + }; + + fill = argsData.fill; + border = argsData.border as { + color: string; + width: number; + }; + + height = argsData.markerHeight as number; + width = argsData.markerWidth as number; + shape = argsData.markerShape as ChartMarkerShape; + + point.marker = { + border: argsData.border, fill: argsData.fill, + height: argsData.markerHeight, visible: true, + width: argsData.markerWidth, shape: argsData.markerShape, imageUrl: marker.imageUrl + }; + + point.color = argsData.fill; + + (point.symbolLocations as ChartLocationProps[])?.push(location); + + // Add region for the scatter point tooltip detection + point.regions?.push({ + x: location.x - (width / 2), + y: location.y - (height / 2), + width: width, + height: height + }); + + const pointId: string = `${series.chart.element.id}_Series_${series.index}_Point_${point.index}`; + + const shapeOpts: Element = drawSymbol( + location, + shape, + { width: width as number, height: height as number }, + marker.imageUrl as string, + { + fill: fill, + stroke: border.color, + id: pointId, + strokeWidth: border.width as number, + strokeDasharray: scatterBorder.dashArray as string, + opacity: series.opacity as number, + d: '' + } + ); + + return { + ...shapeOpts, + shape: shape, + fill: fill, + border: border, + opacity: series.opacity as number, + cx: location.x, + cy: location.y, + rx: width / 2, + ry: height / 2, + id: pointId, + stroke: border.color || series.border?.color || fill, + strokeWidth: border.width || series.border?.width + }; + }, + + /** + * Animates the scatter points. + * + * @param {SeriesProperties} series - Series which should be animated. + * @returns {Function} Returns the animated points. + */ + doAnimation: (series: SeriesProperties) => { + const duration: number = series.animation?.duration as number; + const delay: number = series.animation?.delay as number; + const rectElements: NodeList = series?.seriesElement?.childNodes; + let count: number = 1; + + for (const point of series.points) { + if (!(point.symbolLocations as ChartLocationProps[])?.length || !rectElements[count as number]) { + continue; + } + + // Create a MarkerElementData object + const markerData: MarkerElementData = { + // For elliptical markers (most common in scatter charts) + rx: point.marker?.width, + ry: point.marker?.height, + + // For circular markers + r: point.marker?.width, + + // Opacity + opacity: point.marker?.opacity !== undefined ? point.marker.opacity : 1, + + // Optional animation tracking data if needed + _animationData: { + originalX: point.symbolLocations?.[0]?.x, + originalY: point.symbolLocations?.[0]?.y, + targetX: point.symbolLocations?.[0]?.x, + targetY: point.symbolLocations?.[0]?.y + } + }; + + markerAnimate(markerData, delay as number, duration as number); + count++; + } + } +}; + +export default ScatterSeriesRenderer; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/SeriesAnimation.tsx b/components/charts/src/chart/renderer/SeriesRenderer/SeriesAnimation.tsx new file mode 100644 index 0000000..6a490e4 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/SeriesAnimation.tsx @@ -0,0 +1,1069 @@ +import { getPathLength, valueToCoefficient } from '../../utils/helper'; +import { PathCommand } from '../../common/base'; +import { interpolateSteplinePathD } from './StepLineSeriesRenderer'; +import { StepPosition } from '../../base/enum'; +import { interpolateSplinePathD } from './SplineSeriesRenderer'; +import { Points, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { interpolateBorderPath } from './AreaSeriesRenderer'; +import { ChartLocationProps } from '../../base/interfaces'; + +export interface AnimationState { + previousPathLengthRef: React.MutableRefObject; + isInitialRenderRef: React.MutableRefObject; + renderedPathDRef: React.MutableRefObject; + animationProgress: number; + isFirstRenderRef: React.MutableRefObject; + previousSeriesOptionsRef: React.MutableRefObject; +} + +/** + * Calculates the length of a path based on its SVG path data. + * + * @param {string} d - The SVG path data string + * @returns {number} The length of the path, or 0 if no path data is provided + */ +export const calculatePathLength: (d?: string | undefined) => number = (d?: string): number => { + return d ? getPathLength(d) : 0; +}; + + +/** + * Applies an easing function to the progress for smoother animations. + * + * @param {number} progress - The animation progress value between 0 and 1 + * @returns {number} The eased progress value + */ +export const easeIn: (progress: number) => number = (progress: number): number => progress * progress; + +/** + * Interpolates between two path data strings based on animation progress. + * + * @param {string} fromD - The starting path data string + * @param {string} toD - The ending path data string + * @param {number} progress - The animation progress value between 0 and 1 + * @param {boolean} legendClicked - Denotes whether legend is clicked + * @param {string} type - Type of series + * @returns {string} The interpolated path data at the specified progress + * @private + */ +export function interpolatePathD(fromD: string, toD: string, progress: number, legendClicked?: boolean, type?: string): string { + if (fromD !== undefined && toD !== undefined) { + // Check if this involves spline series (contains cubic bezier 'C' commands) + if (fromD.includes('C') || toD.includes('C')) { + return interpolateSplinePathD(fromD, toD, progress); + } + + const parsePath: (d: string) => [number, number][] = (d: string): [number, number][] => + d.replace(/[ML]/g, '') + .trim() + .split(/\s+/) + .reduce<[number, number][]>((acc: [number, number][], val: string, i: number, arr: string[]): [number, number][] => { + if (i % 2 === 0) { + acc.push([parseFloat(val), parseFloat(arr[i + 1])]); + } + return acc; + }, []); + + const interpolatePoints: (from: [number, number][], to: [number, number][], t: number) => [number, number][] = + (from: [number, number][], to: [number, number][], t: number): [number, number][] => { + const result: [number, number][] = []; + let toIndex: number = 0; + const EPSILON: number = 0.01; + + for (let i: number = 0; i < from.length; i++) { + const [fx, fy] = from[i as number]; + let matched: boolean = false; + + for (let k: number = toIndex; k < to.length; k++) { + const [tx, ty] = to[k as number]; + if (Math.abs(fx - tx) < EPSILON && Math.abs(fy - ty) < EPSILON) { + result.push([fx, fy]); + toIndex = k + 1; + matched = true; + break; + } + } + + void (!matched && + ( + () => { + const prev: [number, number] = to[toIndex - 1]; + const next: [number, number] = to[toIndex as number]; + const [tx, ty] = prev || next; + const x: number = fx + (tx - fx) * t; + const y: number = fy + (ty - fy) * t; + void (t < 1 && result.push([x, y])); + } + )() + ); + } + return result; + }; + + const fromParts: RegExpMatchArray | [] = fromD?.match(/[a-zA-Z]|-?\d+\.?\d*/g) as RegExpMatchArray; + const toParts: RegExpMatchArray | [] = toD?.match(/[a-zA-Z]|-?\d+\.?\d*/g) as RegExpMatchArray; + + if (fromParts?.length !== toParts?.length) { + const clamp: (v: number, min?: number, max?: number) => number = (v: number, min: number = 0, max: number = 1): number => + Math.max(min, Math.min(v, max)); + const t: number = clamp(progress); + const fromPoints: [number, number][] = parsePath(fromD); + const toPoints: [number, number][] = parsePath(toD); + + const interpolatedPoints: [number, number][] = interpolatePoints(fromPoints, toPoints, t); + + return interpolatedPoints + .map(([x, y]: [number, number], i: number) => (i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`)) + .join(' '); + } + + return fromParts.map((fromVal: string, i: number) => { + if (isNaN(Number(fromVal))) { return fromVal; } + const fromNum: number = parseFloat(fromVal); + const toNum: number = parseFloat(toParts[i as number]); + const easedProgress: number = easeIn(progress); + const interpolated: number = fromNum + (toNum - fromNum) * (legendClicked && type === 'Spline' ? progress : easedProgress); + return interpolated.toString(); + }).join(' '); + } + + // Default return value if conditions above aren't met + return toD || fromD || ''; +} + +/** + * Handles dashed line animation separately using path truncation + * + * @param {Object} pathOptions The path rendering options + * @param {AnimationState} state Animation state including references and animation progress + * @returns {Object} Object with animation properties for dashed lines + */ +export const calculateDashedPathAnimation: (pathOptions: RenderOptions, state: AnimationState) => { + strokeDasharray: string; + strokeDashoffset: number; + interpolatedD: string; +} = ( + pathOptions: RenderOptions, + state: AnimationState +): { strokeDasharray: string; strokeDashoffset: number; interpolatedD: string } => { + const { animationProgress } = state; + + if (/[cCsS]/.test(pathOptions.d as string)) { + return calculateSplineDashedPathAnimation(pathOptions, state); + } + + // Parse path commands to get points + const pathCommands: RegExpMatchArray = pathOptions.d.match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/gi) as RegExpMatchArray; + const points: { x: number; y: number }[] = []; + + pathCommands.forEach((cmd: string) => { + const coords: RegExpMatchArray = cmd.match(/-?\d+\.?\d*/g) as RegExpMatchArray; + void (coords.length >= 2 && coords[0] && coords[1] && void points.push({ + x: parseFloat(coords[0]), + y: parseFloat(coords[1]) + })); + }); + + // Calculate cumulative distances + const distances: number[] = [0]; + let totalDistance: number = 0; + + for (let i: number = 1; i < points.length; i++) { + const dx: number = points[i as number].x - points[i - 1].x; + const dy: number = points[i as number].y - points[i - 1].y; + const distance: number = Math.sqrt(dx * dx + dy * dy); + totalDistance += distance; + distances.push(totalDistance); + } + + // Find target distance based on animation progress + const targetDistance: number = totalDistance * animationProgress; + + // Build animated path up to target distance + let animatedPath: string = `M ${points[0].x} ${points[0].y}`; + let currentDistance: number = 0; + for (let i: number = 1; i < points.length; i++) { + const segmentDistance: number = distances[i as number] - distances[i - 1]; + if (currentDistance + segmentDistance <= targetDistance) { + // Include complete segment + animatedPath += ` L ${points[i as number].x} ${points[i as number].y}`; + currentDistance += segmentDistance; + } else { + // Include partial segment + const remainingDistance: number = targetDistance - currentDistance; + const ratio: number = remainingDistance / segmentDistance; + const prevPoint: { + x: number; + y: number; + } = points[i - 1]; + const currPoint: { + x: number; + y: number; + } = points[i as number]; + const interpX: number = prevPoint.x + (currPoint.x - prevPoint.x) * ratio; + const interpY: number = prevPoint.y + (currPoint.y - prevPoint.y) * ratio; + animatedPath += ` L ${interpX} ${interpY}`; + break; + } + } + return { + strokeDasharray: pathOptions.dashArray as string, + strokeDashoffset: 0, + interpolatedD: animatedPath + }; +}; + +/** + * Handles dashed line animation for spline paths by keeping the path cubic (M/C only). + * It truncates the last cubic at the exact arc-length using De Casteljau splitting, + * so the dash pattern remains consistent with the final rendered spline. + * + * @param {Object} pathOptions The path rendering options (expects 'd' and 'dashArray') + * @param {AnimationState} state Animation state including references and animation progress + * @returns {Object} Object with animation properties for dashed lines + */ +export const calculateSplineDashedPathAnimation: ( + pathOptions: RenderOptions, + state: AnimationState +) => { + strokeDasharray: string; + strokeDashoffset: number; + interpolatedD: string; +} = ( + pathOptions: RenderOptions, + state: AnimationState +): { strokeDasharray: string; strokeDashoffset: number; interpolatedD: string } => { + const { animationProgress }: { animationProgress: number } = state; + const pathData: string = String(pathOptions.d ?? ''); + + if (!pathData) { + return { + strokeDasharray: (pathOptions.dashArray as string) ?? 'none', + strokeDashoffset: 0, + interpolatedD: '' + }; + } + + const tGlobal: number = Math.max(0, Math.min(1, animationProgress)); + if (tGlobal >= 1) { + return { + strokeDasharray: (pathOptions.dashArray as string) ?? 'none', + strokeDashoffset: 0, + interpolatedD: pathData + }; + } + + const distanceBetweenPoints: (a: ChartLocationProps, b: ChartLocationProps) => number = + (a: ChartLocationProps, b: ChartLocationProps): number => Math.hypot(b.x - a.x, b.y - a.y); + const interpolatePoint: (a: ChartLocationProps, b: ChartLocationProps, t: number) => ChartLocationProps = + (a: ChartLocationProps, b: ChartLocationProps, t: number): ChartLocationProps => ({ + x: a.x + (b.x - a.x) * t, + y: a.y + (b.y - a.y) * t + }); + + const getCubicPoint: (p0: ChartLocationProps, p1: ChartLocationProps, p2: ChartLocationProps, p3: ChartLocationProps, + t: number) => ChartLocationProps = (p0: ChartLocationProps, p1: ChartLocationProps, p2: ChartLocationProps, + p3: ChartLocationProps, t: number): ChartLocationProps => { + const mt: number = 1 - t; + const a: number = mt * mt * mt; + const b: number = 3 * mt * mt * t; + const c: number = 3 * mt * t * t; + const d: number = t * t * t; + return { + x: a * p0.x + b * p1.x + c * p2.x + d * p3.x, + y: a * p0.y + b * p1.y + c * p2.y + d * p3.y + }; + }; + + const splitCubicFirstHalf: (p0: ChartLocationProps, p1: ChartLocationProps, p2: ChartLocationProps, p3: ChartLocationProps, + t: number) => [ChartLocationProps, ChartLocationProps, ChartLocationProps, ChartLocationProps] = (p0: ChartLocationProps + , p1: ChartLocationProps, p2: ChartLocationProps, p3: ChartLocationProps, t: number): [ChartLocationProps, + ChartLocationProps, ChartLocationProps, ChartLocationProps] => { + const p01: ChartLocationProps = interpolatePoint(p0, p1, t); + const p12: ChartLocationProps = interpolatePoint(p1, p2, t); + const p23: ChartLocationProps = interpolatePoint(p2, p3, t); + const p012: ChartLocationProps = interpolatePoint(p01, p12, t); + const p123: ChartLocationProps = interpolatePoint(p12, p23, t); + const p0123: ChartLocationProps = interpolatePoint(p012, p123, t); + return [p0, p01, p012, p0123]; + }; + + const getCubicLength: (p0: ChartLocationProps, p1: ChartLocationProps, p2: ChartLocationProps, p3: ChartLocationProps, + samples?: number) => number = (p0: ChartLocationProps, p1: ChartLocationProps, p2: ChartLocationProps, + p3: ChartLocationProps, samples: number = 32): number => { + let length: number = 0; + let previousPoint: ChartLocationProps = p0; + for (let i: number = 1; i <= samples; i++) { + const t: number = i / samples; + const currentPoint: ChartLocationProps = getCubicPoint(p0, p1, p2, p3, t); + length += distanceBetweenPoints(previousPoint, currentPoint); + previousPoint = currentPoint; + } + return length; + }; + + const getPartialCubicLength: (p0: ChartLocationProps, p1: ChartLocationProps, p2: ChartLocationProps, + p3: ChartLocationProps, t: number, samples?: number) => number = + (p0: ChartLocationProps, p1: ChartLocationProps, p2: ChartLocationProps, p3: ChartLocationProps, + t: number, samples: number = 32): number => { + if (t <= 0) {return 0; } + if (t >= 1) {return getCubicLength(p0, p1, p2, p3, samples); } + + let length: number = 0; + let previousPoint: ChartLocationProps = p0; + const steps: number = Math.max(2, Math.round(samples * t)); + for (let i: number = 1; i <= steps; i++) { + const ti: number = (i / steps) * t; + const currentPoint: ChartLocationProps = getCubicPoint(p0, p1, p2, p3, ti); + length += distanceBetweenPoints(previousPoint, currentPoint); + previousPoint = currentPoint; + } + return length; + }; + + const commands: PathCommand[] = parsePathCommands(pathData); + if (commands.length === 0) { + return { + strokeDasharray: (pathOptions.dashArray as string) ?? 'none', + strokeDashoffset: 0, + interpolatedD: pathData + }; + } + + let cursor: ChartLocationProps = { x: 0, y: 0 }; + let interpolatedPath: string = ''; + let totalPathLength: number = 0; + + { + let currentPoint: ChartLocationProps = cursor; + for (const command of commands) { + if (command.type === 'M' && command.params.length >= 2) { + currentPoint = { x: command.params[0], y: command.params[1] }; + } else if (command.type === 'L' && command.params.length >= 2) { + const nextPoint: ChartLocationProps = { x: command.params[0], y: command.params[1] }; + totalPathLength += distanceBetweenPoints(currentPoint, nextPoint); + currentPoint = nextPoint; + } else if (command.type === 'C' && command.params.length >= 6) { + const p0: ChartLocationProps = currentPoint; + const p1: ChartLocationProps = { x: command.params[0], y: command.params[1] }; + const p2: ChartLocationProps = { x: command.params[2], y: command.params[3] }; + const p3: ChartLocationProps = { x: command.params[4], y: command.params[5] }; + totalPathLength += getCubicLength(p0, p1, p2, p3); + currentPoint = p3; + } + } + } + + const targetLength: number = totalPathLength * tGlobal; + let accumulatedLength: number = 0; + let hasStarted: boolean = false; + cursor = { x: 0, y: 0 }; + + for (const command of commands) { + if (command.type === 'M' && command.params.length >= 2) { + cursor = { x: command.params[0], y: command.params[1] }; + interpolatedPath += hasStarted ? ` M ${cursor.x} ${cursor.y}` : `M ${cursor.x} ${cursor.y}`; + hasStarted = true; + } else if (command.type === 'L' && command.params.length >= 2) { + const nextPoint: ChartLocationProps = { x: command.params[0], y: command.params[1] }; + const segmentLength: number = distanceBetweenPoints(cursor, nextPoint); + if (accumulatedLength + segmentLength <= targetLength) { + interpolatedPath += ` L ${nextPoint.x} ${nextPoint.y}`; + accumulatedLength += segmentLength; + cursor = nextPoint; + } else { + const remainingLength: number = targetLength - accumulatedLength; + const ratio: number = segmentLength === 0 ? 0 : remainingLength / segmentLength; + const endPoint: ChartLocationProps = interpolatePoint(cursor, nextPoint, Math.max(0, Math.min(1, ratio))); + interpolatedPath += ` L ${endPoint.x} ${endPoint.y}`; + accumulatedLength = targetLength; + break; + } + } else if (command.type === 'C' && command.params.length >= 6) { + const p0: ChartLocationProps = cursor; + const p1: ChartLocationProps = { x: command.params[0], y: command.params[1] }; + const p2: ChartLocationProps = { x: command.params[2], y: command.params[3] }; + const p3: ChartLocationProps = { x: command.params[4], y: command.params[5] }; + const segmentLength: number = getCubicLength(p0, p1, p2, p3); + + if (accumulatedLength + segmentLength <= targetLength) { + interpolatedPath += ` C ${p1.x} ${p1.y} ${p2.x} ${p2.y} ${p3.x} ${p3.y}`; + accumulatedLength += segmentLength; + cursor = p3; + } else { + const requiredLength: number = targetLength - accumulatedLength; + let low: number = 0; + let high: number = 1; + for (let i: number = 0; i < 12; i++) { + const mid: number = (low + high) / 2; + const lengthAtMid: number = getPartialCubicLength(p0, p1, p2, p3, mid); + if (lengthAtMid < requiredLength) { + low = mid; + } else { + high = mid; + } + } + const tSplit: number = (low + high) / 2; + const [, q1, q2, q3]: [ChartLocationProps, ChartLocationProps, ChartLocationProps, + ChartLocationProps] = splitCubicFirstHalf(p0, p1, p2, p3, tSplit); + interpolatedPath += ` C ${q1.x} ${q1.y} ${q2.x} ${q2.y} ${q3.x} ${q3.y}`; + accumulatedLength = targetLength; + cursor = q3; + break; + } + } + } + + return { + strokeDasharray: (pathOptions.dashArray as string) ?? 'none', + strokeDashoffset: 0, + interpolatedD: interpolatedPath + }; +}; + +/** + * Handles the initial animation for a path. + * + * @param {RenderOptions} pathOptions - The path rendering options + * @param {AnimationState} state - Animation state including references and progress + * @param {number} index - The series index + * @param {number} pathLength - The calculated path length + * @param {boolean} shouldUseDashedAnimation - Whether to use dashed animation + * @returns {Object} Animation properties for initial state + */ +export const handleInitialAnimation: (pathOptions: RenderOptions, state: AnimationState, + index: number, pathLength: number, shouldUseDashedAnimation: boolean) => { + strokeDasharray: string | number; + strokeDashoffset: number; + interpolatedD?: string; +} = ( + pathOptions: RenderOptions, + state: AnimationState, + index: number, + pathLength: number, + shouldUseDashedAnimation: boolean +): { strokeDasharray: string | number; strokeDashoffset: number; interpolatedD?: string } => { + const { isInitialRenderRef, animationProgress } = state; + + // Handle completion of initial animation + if (animationProgress === 1) { + isInitialRenderRef.current[index as number] = false; + } + + // For dashed lines, use specialized animation + if (shouldUseDashedAnimation) { + return calculateDashedPathAnimation(pathOptions, state); + } + + // Standard initial animation using dash offset + return { + strokeDasharray: pathLength, + strokeDashoffset: pathLength * (1 - animationProgress) + }; +}; + +/** + * Handles path interpolation when the path structure remains the same. + * + * @param {string} renderedD - The previously rendered path + * @param {RenderOptions} pathOptions - The path rendering options + * @param {number} animationProgress - The animation progress + * @returns {Object} Animation properties for path interpolation + */ +export const handlePathInterpolation: (renderedD: string, pathOptions: RenderOptions, animationProgress: number) => { + strokeDasharray: string | number; + strokeDashoffset: number; + interpolatedD: string; +} = ( + renderedD: string, + pathOptions: RenderOptions, + animationProgress: number +): { strokeDasharray: string | number; strokeDashoffset: number; interpolatedD: string } => { + return { + interpolatedD: interpolatePathD(renderedD, pathOptions.d as string, animationProgress), + strokeDasharray: pathOptions.dashArray as string, + strokeDashoffset: 0 + }; +}; + +/** + * Handles animation when the start path has more commands than the end path. + * + * @param {string} renderedD - The previously rendered path + * @param {RenderOptions} pathOptions - The path rendering options + * @param {RegExpMatchArray} startPathCommands - The parsed start path commands + * @param {RegExpMatchArray} endPathCommands - The parsed end path commands + * @param {SeriesProperties} currentSeries - The current series properties + * @param {number} animationProgress - The animation progress + * @returns {Object} Animation properties when reducing path commands + */ +export const handlePathCommandReduction: (renderedD: string, pathOptions: RenderOptions, + startPathCommands: RegExpMatchArray, endPathCommands: RegExpMatchArray, + currentSeries: SeriesProperties | undefined, animationProgress: number) => { + strokeDasharray: string; + strokeDashoffset: number; + interpolatedD: string; +} = ( + renderedD: string, + pathOptions: RenderOptions, + startPathCommands: RegExpMatchArray, + endPathCommands: RegExpMatchArray, + currentSeries: SeriesProperties | undefined, + animationProgress: number +): { strokeDasharray: string; strokeDashoffset: number; interpolatedD: string } => { + let interpolatedD: string = ''; + + // Handle different series types differently + if (currentSeries?.type === 'StepLine') { + const stepPosition: StepPosition = currentSeries.step as StepPosition; + interpolatedD = interpolateSteplinePathD(renderedD, pathOptions.d as string, animationProgress, stepPosition); + } + else if (currentSeries?.type === 'Spline') { + const adjustedCommands: string[] = adjustSplineCommands(startPathCommands, endPathCommands, currentSeries); + interpolatedD = interpolatePathD(renderedD, adjustedCommands.join(''), animationProgress); + } + else if (currentSeries?.type === 'Line') { + const maxLength: number = Math.max(startPathCommands.length, endPathCommands.length); + const minLength: number = Math.min(startPathCommands.length, endPathCommands.length); + + for (let i: number = minLength; i < maxLength; i++) { + endPathCommands.splice(1, 0, endPathCommands[0].replace('M', 'L')); + } + + interpolatedD = interpolateBorderPath(renderedD, endPathCommands.join(''), animationProgress); + } + else { + interpolatedD = interpolatePathD(renderedD || '', pathOptions.d as string, animationProgress); + } + + return { + interpolatedD, + strokeDasharray: pathOptions.dashArray as string, + strokeDashoffset: 0 + }; +}; + +/** + * Adjusts spline commands to match count between start and end paths. + * + * @param {RegExpMatchArray} startCommands - The start path commands + * @param {RegExpMatchArray} endCommands - The end path commands + * @param {SeriesProperties} series - The series properties + * @returns {Array} The adjusted end commands + */ +export const adjustSplineCommands: (startCommands: RegExpMatchArray, + endCommands: RegExpMatchArray, series: SeriesProperties) => string[] = ( + startCommands: RegExpMatchArray, + endCommands: RegExpMatchArray, + series: SeriesProperties +): string[] => { + const endCmdsCopy: string[] = [...endCommands]; + const maxLength: number = Math.max(startCommands.length, endCmdsCopy.length); + const minLength: number = Math.min(startCommands.length, endCmdsCopy.length); + + for (let i: number = minLength; i < maxLength; i++) { + if (series && series.removedPointIndex === series.points.length && + endCmdsCopy.length !== startCommands.length) { + if (endCmdsCopy[endCmdsCopy.length - 1].indexOf('C') === 0) { + const points: string[] = endCmdsCopy[endCmdsCopy.length - 1].split(' ').slice(-3); + endCmdsCopy.push('C ' + points.join(' ') + points.join(' ') + points.join(' ')); + } + else { + const points: string = endCmdsCopy[endCmdsCopy.length - 1].replace('M', ''); + endCmdsCopy.push('C' + points + points + points); + } + } else { + if (endCmdsCopy.length !== startCommands.length) { + endCmdsCopy.splice(1, 0, 'C ' + + endCmdsCopy[0].split(' ').slice(-3).join(' ') + + endCmdsCopy[0].split(' ').slice(-3).join(' ') + + endCmdsCopy[0].split(' ').slice(-3).join(' ')); + } + } + } + + return endCmdsCopy; +}; + +/** + * Handles animation when the start path has fewer commands than the end path. + * + * @param {RegExpMatchArray} startPathCommands - The parsed start path commands + * @param {RegExpMatchArray} endPathCommands - The parsed end path commands + * @param {SeriesProperties} currentSeries - The current series properties + * @param {RenderOptions} pathOptions - The path rendering options + * @param {number} pathLength - The calculated path length + * @param {number} prevLength - The previous path length + * @param {number} animationProgress - The animation progress + * @returns {Object} Animation properties when adding path commands + */ +export const handlePathCommandAddition: (startPathCommands: RegExpMatchArray, + endPathCommands: RegExpMatchArray, currentSeries: SeriesProperties | undefined, + pathOptions: RenderOptions, pathLength: number, prevLength: number, animationProgress: number) => { + strokeDasharray: string | number; + strokeDashoffset: number; + interpolatedD?: string; +} = ( + startPathCommands: RegExpMatchArray, + endPathCommands: RegExpMatchArray, + currentSeries: SeriesProperties | undefined, + pathOptions: RenderOptions, + pathLength: number, + prevLength: number, + animationProgress: number +): { strokeDasharray: string | number; strokeDashoffset: number; interpolatedD?: string } => { + const result: { strokeDasharray: string | number; strokeDashoffset: number; interpolatedD?: string } = { + strokeDasharray: 'none', + strokeDashoffset: 0 + }; + + const addedLength: number = Math.max(pathLength - prevLength, 0); + + if (currentSeries?.type === 'Spline') { + const adjustedStartCommands: string[] = adjustStartCommandsForSpline(startPathCommands, endPathCommands); + result.interpolatedD = interpolatePathD( + adjustedStartCommands.join(' '), + pathOptions.d as string, + animationProgress + ); + result.strokeDasharray = pathOptions.dashArray || 'none'; + } + else if (currentSeries?.type === 'Line') { + const adjustedStartCommands: string[] = adjustStartCommandsForLine(startPathCommands, endPathCommands); + result.interpolatedD = interpolateBorderPath( + adjustedStartCommands.join(' '), + pathOptions.d as string, + animationProgress + ); + result.strokeDasharray = pathOptions.dashArray || 'none'; + } + else { + // For added points, apply dash array with animation offset + if (pathOptions.dashArray !== '') { + result.strokeDasharray = pathOptions.dashArray as string; + result.strokeDashoffset = addedLength * (1 - animationProgress); + } else { + result.strokeDasharray = `${pathLength}`; + result.strokeDashoffset = addedLength * (1 - animationProgress); + } + } + + return result; +}; + +/** + * Adjusts start commands for spline series when adding points. + * + * @param {RegExpMatchArray} startCommands - The start path commands + * @param {RegExpMatchArray} endCommands - The end path commands + * @returns {Array} The adjusted start commands + */ +export const adjustStartCommandsForSpline: (startCommands: RegExpMatchArray, endCommands: RegExpMatchArray) => string[] = ( + startCommands: RegExpMatchArray, + endCommands: RegExpMatchArray +): string[] => { + const startCmdsCopy: string[] = [...startCommands]; + + for (let i: number = startCmdsCopy.length; i < endCommands.length; i++) { + if (endCommands.length !== startCmdsCopy.length) { + if (endCommands.length === startCmdsCopy.length + 1 && + endCommands[endCommands.length - 1].indexOf('M') === 0) { + startCmdsCopy.push(endCommands[endCommands.length - 1]); + } + else if (startCmdsCopy[startCmdsCopy.length - 1].indexOf('C') === 0) { + const points: string[] = startCmdsCopy[startCmdsCopy.length - 1].split(' ').slice(-3); + startCmdsCopy.push('C ' + points.join(' ') + points.join(' ') + points.join(' ')); + } + else { + const points: string = startCmdsCopy[startCmdsCopy.length - 1].replace('M', ''); + startCmdsCopy.push('C' + points + points + points); + } + } + } + + return startCmdsCopy; +}; + +/** + * Adjusts start commands for line series when adding points. + * + * @param {RegExpMatchArray} startCommands - The start path commands + * @param {RegExpMatchArray} endCommands - The end path commands + * @returns {Array} The adjusted start commands + */ +export const adjustStartCommandsForLine: (startCommands: RegExpMatchArray, endCommands: RegExpMatchArray) => string[] = ( + startCommands: RegExpMatchArray, + endCommands: RegExpMatchArray +): string[] => { + const startCmdsCopy: string[] = [...startCommands]; + + const maxLength: number = Math.max(startCmdsCopy.length, endCommands.length); + const minLength: number = Math.min(startCmdsCopy.length, endCommands.length); + + for (let i: number = minLength; i < maxLength; i++) { + if (endCommands.length !== startCmdsCopy.length) { + startCmdsCopy.push(startCmdsCopy[startCmdsCopy.length - 1].replace('M', 'L')); + } + } + + return startCmdsCopy; +}; + +/** + * Updates animation state references when animation completes. + * + * @param {AnimationState} state - Animation state including references + * @param {number} index - The series index + * @param {number} seriesIndex - The series index extracted from ID + * @param {number} pathLength - The calculated path length + * @param {RenderOptions} pathOptions - The path rendering options + * @returns {void} + */ +export const updateAnimationReferences: (state: AnimationState, index: number, seriesIndex: number, + pathLength: number, pathOptions: RenderOptions) => void = ( + state: AnimationState, + index: number, + seriesIndex: number, + pathLength: number, + pathOptions: RenderOptions +): void => { + const { previousPathLengthRef, renderedPathDRef } = state; + + previousPathLengthRef.current[index as number] = pathLength; + renderedPathDRef.current[seriesIndex as number] = pathOptions.d as string; +}; + +/** + * Handles path animation calculations and returns animation properties. + * + * @param {Object} pathOptions The path rendering options + * @param {number} index The series index + * @param {AnimationState} state Animation state including references and animation progress + * @param {boolean} enableAnimation Whether animation is enabled for this series + * @param {Series[]} [visibleSeries] The visible series for the chart + * @returns {Object} Object with animation properties (strokeDasharray, strokeDashoffset, interpolatedD) + */ +export const calculatePathAnimation: (pathOptions: RenderOptions, index: number, + state: AnimationState, enableAnimation: boolean, visibleSeries?: SeriesProperties[]) => { + strokeDasharray: string | number; + strokeDashoffset: number; + interpolatedD?: string; +} = ( + pathOptions: RenderOptions, + index: number, + state: AnimationState, + enableAnimation: boolean, + visibleSeries?: SeriesProperties[] +): { strokeDasharray: string | number; strokeDashoffset: number; interpolatedD?: string } => { + const { previousPathLengthRef, isInitialRenderRef, renderedPathDRef, animationProgress } = state; + + // Get the series index from the ID + const match: RegExpMatchArray | null = (pathOptions.id as string).match(/Series_(\d+)/); + const seriesIndex: number = match ? parseInt(match[1], 10) : 0; + const currentSeries: SeriesProperties | undefined = visibleSeries?.[seriesIndex as number]; + + // Calculate current and previous path lengths + const pathLength: number = calculatePathLength(pathOptions.d); + const prevLength: number = previousPathLengthRef.current[index as number]; + const isInitial: boolean = isInitialRenderRef.current[index as number]; + const renderedD: string | undefined = renderedPathDRef.current[seriesIndex as number]; + + // Extract path commands for analysis + const startPathCommands: RegExpMatchArray = renderedD?.match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/g) as RegExpMatchArray; + const endPathCommands: RegExpMatchArray = pathOptions.d.match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/g) as RegExpMatchArray; + + // Default animation values + let result: { strokeDasharray: string | number; strokeDashoffset: number; interpolatedD?: string } = { + strokeDasharray: pathOptions.dashArray || 'none', + strokeDashoffset: 0 + }; + + if (enableAnimation) { + // Initial animation when the path is first rendered + if (isInitial) { + const shouldUseDashedAnimation: boolean = !!pathOptions.dashArray && + !(visibleSeries?.some((series: SeriesProperties) => series.isLegendClicked)) && + pathOptions.dashArray !== 'none' && + !(visibleSeries?.some((series: SeriesProperties) => series.skipMarkerAnimation)); + + result = handleInitialAnimation(pathOptions, state, index, pathLength, shouldUseDashedAnimation); + } + // Path structure remains the same, just interpolate values + else if ( + renderedD && + pathOptions.d && + renderedD.match(/[a-zA-Z]/g)?.join('') === pathOptions.d.match(/[a-zA-Z]/g)?.join('') + ) { + result = handlePathInterpolation(renderedD, pathOptions, animationProgress); + } + // Path has fewer commands in the end (points removed) + else if (startPathCommands?.length > endPathCommands?.length) { + result = handlePathCommandReduction( + renderedD as string, + pathOptions, + startPathCommands, + endPathCommands, + currentSeries, + animationProgress + ); + } + // Path has more commands in the end (points added) + else if (startPathCommands?.length < endPathCommands?.length) { + result = handlePathCommandAddition( + startPathCommands, + endPathCommands, + currentSeries, + pathOptions, + pathLength, + prevLength, + animationProgress + ); + + // Apply dash array during point addition animation if path is interpolated + if (result.interpolatedD) { + result.strokeDasharray = pathOptions.dashArray || 'none'; + } + } + } + + // Update reference values when animation completes + if (animationProgress === 1) { + result.strokeDasharray = pathOptions.dashArray || 'none'; + result.strokeDashoffset = 0; + updateAnimationReferences(state, index, seriesIndex, pathLength, pathOptions); + } + + return result; +}; + +/** + * Handles animation for rectangle-based series elements (columns/bars) + * + * @param {RenderOptions} pathOption - The rendering options for the current path + * @param {Series} currentSeries - The series that contains the point being animated + * @param {number} index - The index of the series in the chart + * @param {Points} currentPoint - The data point being animated + * @param {number} pointIndex - The index of the point within the series + * @param {AnimationState} state - Object containing animation state references and values + * @param {boolean} enableAnimation - Flag indicating whether animation is enabled + * @returns {Object} Object containing animation properties - animatedDirection and animatedTransform + * @private + */ +export function handleRectAnimation(pathOption: RenderOptions, currentSeries: +SeriesProperties, index: number, currentPoint: Points | undefined +, pointIndex: number, state: AnimationState, enableAnimation: boolean): { animatedDirection?: string; animatedTransform?: string; } { + const isFirstRenderRef: React.MutableRefObject = state.isFirstRenderRef; + const isInitialRenderRef: React.MutableRefObject = state.isInitialRenderRef; + const animationProgress: number = state.animationProgress; + const previousSeriesOptionsRef: React.MutableRefObject = state.previousSeriesOptionsRef; + const isInitial: boolean = isInitialRenderRef.current[index as number]; + let direction: string = pathOption.d; + + if (animationProgress === 1) { + previousSeriesOptionsRef.current[index as number] ||= []; + previousSeriesOptionsRef.current[index as number][pointIndex as number] = pathOption; + } + if (currentSeries && currentPoint && enableAnimation) { + if (isFirstRenderRef.current && isInitial) { + if (animationProgress === 1) { + isFirstRenderRef.current = false; + isInitialRenderRef.current[index as number] = false; + } + return { animatedTransform: animateRect(currentSeries, currentPoint, animationProgress), animatedDirection: undefined }; + } + + if (!isFirstRenderRef.current && + previousSeriesOptionsRef.current && + previousSeriesOptionsRef.current[index as number] && + previousSeriesOptionsRef.current[index as number][pointIndex as number] && + (previousSeriesOptionsRef.current[index as number][pointIndex as number].d !== pathOption.d)) { + + direction = calculateRectPathDirection( + previousSeriesOptionsRef.current[index as number][pointIndex as number].d, + pathOption.d, + animationProgress + ); + return { animatedTransform: '', animatedDirection: direction }; + } + } + + return {}; +} + +/** + * Calculates the transform string for animating a column/bar rect element. + * + * @param {Series} series - The series containing the point. + * @param {Points} point - The data point to animate. + * @param {number} progress - Animation progress (0-1). + * @returns {string} The SVG transform string. + */ +export const animateRect: (series: SeriesProperties, point: Points, progress?: number) => string = ( + series: SeriesProperties, + point: Points, + progress: number = 1 +): string => { + if (!point.regions || !point.regions[0]) { + return ''; + } + const isPlot: boolean = (point.yValue !== null && point.yValue < 0); + const x: number = +point.regions[0].x; + const y: number = +point.regions[0].y; + const elementHeight: number = +point.regions[0].height; + let elementWidth: number = +point.regions[0].width; + let centerX: number; + let centerY: number; + if (!series.chart.requireInvertedAxis) { + if (series?.type!.indexOf('Stacking') > -1) { + centerX = x; + centerY = (1 - valueToCoefficient(0, series.yAxis)) * (series.yAxis.rect.height); + } else { + centerY = (isPlot !== series.yAxis.isAxisInverse) ? y : y + elementHeight; + centerX = isPlot ? x : x + elementWidth; + } + } else { + if (series?.type!.indexOf('Stacking') > -1) { + centerX = (valueToCoefficient(0, series.yAxis)) * series.yAxis.rect.width; + centerY = y; + } else { + centerY = isPlot ? y : y + elementHeight; + centerX = (isPlot !== series.yAxis.isAxisInverse) ? x + elementWidth : x; + } + } + const value: number = (progress) * (series.chart.requireInvertedAxis ? elementWidth : elementHeight); + if (!series.chart.requireInvertedAxis) { + return `translate(${centerX} ${centerY}) scale(1,${value / elementHeight}) translate(${-centerX} ${-centerY})`; + } else { + elementWidth = elementWidth || 1; + return `translate(${centerX} ${centerY}) scale(${value / elementWidth}, 1) translate(${-centerX} ${-centerY})`; + } +}; + +/** + * Calculates the path direction based on animation progress for both X and Y coordinates. + * Smoothly animates from start path to end path by interpolating all coordinates. + * Works with rectangle-like SVG path elements. + * + * @param {string} startDirection - Starting path direction + * @param {string} endDirection - Ending path direction + * @param {number} progress - Animation progress (0-1) + * @returns {string} Interpolated path direction + * @private + */ +export function calculateRectPathDirection( + startDirection: string, + endDirection: string, + progress: number +): string { + if (!startDirection || !endDirection) { + return endDirection || startDirection || ''; + } + const startCommands: PathCommand[] = parsePathCommands(startDirection); + const endCommands: PathCommand[] = parsePathCommands(endDirection); + if (!startCommands.length || !endCommands.length) { + return endDirection; + } + let result: string = ''; + const commandCount: number = Math.min(startCommands.length, endCommands.length); + + for (let i: number = 0; i < commandCount; i++) { + const startCmd: PathCommand = startCommands[i as number]; + const endCmd: PathCommand = endCommands[i as number]; + if (startCmd.type !== endCmd.type) { + continue; + } + result += startCmd.type + ' '; + switch (startCmd.type) { + case 'M': + case 'L': { + const x: number = interpolate(startCmd.params[0], endCmd.params[0], progress); + const y: number = interpolate(startCmd.params[1], endCmd.params[1], progress); + result += `${x} ${y} `; + break; + } + case 'Q': { + const cx: number = interpolate(startCmd.params[0], endCmd.params[0], progress); + const cy: number = interpolate(startCmd.params[1], endCmd.params[1], progress); + const ex: number = interpolate(startCmd.params[2], endCmd.params[2], progress); + const ey: number = interpolate(startCmd.params[3], endCmd.params[3], progress); + result += `${cx} ${cy} ${ex} ${ey} `; + break; + } + } + } + return result.trim(); +} + +/** + * Helper function to parse SVG path commands into structured objects + * + * @param {string} path - SVG path string + * @returns {Array} Array of command objects + * @private + */ +export function parsePathCommands(path: string): PathCommand[] { + const commands: PathCommand[] = []; + const parts: string[] = path.split(/([MLHVCSQTAZ])/i).filter(Boolean); + let currentType: string = ''; + for (let i: number = 0; i < parts.length; i++) { + const part: string = parts[i as number].trim(); + if (/^[MLHVCSQTAZ]$/i.test(part)) { + currentType = part; + continue; + } + if (currentType && part) { + const params: number[] = part.split(/[\s,]+/) + .filter(Boolean) + .map(parseFloat); + if (params.length > 0) { + commands.push({ + type: currentType, + params: params + }); + } + } + } + return commands; +} + +/** + * Interpolate between two numbers based on progress + * + * @param {number} start - Starting value + * @param {number} end - Ending value + * @param {number} progress - Progress value (0-1) + * @returns {number} Interpolated value + * @private + */ +export function interpolate(start: number, end: number, progress: number): number { + return start + (end - start) * progress; +} + + +/** + * Helper function for smooth scatter & bubble transitions. + * + * @param {SeriesProperties} series - The series containing scatter and bubble points and animation settings + * @param {number} animationProgress - Represents the calculated animation progress. + * @returns {Object} Return the interpolated opacity and scale for smooth rendering. + */ +export const doInitialAnimation: (series: SeriesProperties, animationProgress: number) => { + opacity: number; + scale: number; +} = ( + series: SeriesProperties, + animationProgress: number +): { opacity: number; scale: number } => { + if (!series.animation?.enable || series.propsChange || series.isLegendClicked) { + return { opacity: 1, scale: 1 }; + } + + // Easing function for smooth growth animation + const easeOutCubic: (time: number) => number = (time: number): number => 1 - Math.pow(1 - time, 3); + const easedProgress: number = easeOutCubic(animationProgress); + + return { + opacity: animationProgress, + scale: easedProgress + }; +}; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/SeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/SeriesRenderer.tsx new file mode 100644 index 0000000..3294337 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/SeriesRenderer.tsx @@ -0,0 +1,1091 @@ + +import { forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { ChartSeriesProps, ChartDataLabelProps, ChartMarkerProps, SeriesAccessibility } from '../../base/interfaces'; +import { areDataSourcesEqual, checkTabindex, firstToLowerCase, useVisiblePoints } from '../../utils/helper'; +import ColumnSeries from './ColumnSeriesRenderer'; +import * as React from 'react'; +import { useLayout } from '../../layout/LayoutContext'; +import { useAxesRendereVersion, useRegisterLegendShapeRender, useSeriesRenderVersion } from '../../hooks/useClipRect'; +import { + animatePath, + addPoint, + removePoint, + setData +} from './updatePoint'; +import LineSeriesRenderer from './lineSeriesRenderer'; +import BarSeries from './BarSeriesRenderer'; +import { processJsonData } from './ProcessData'; +import SplineSeriesRenderer from './SplineSeriesRenderer'; +import StepLineSeriesRenderer from './StepLineSeriesRenderer'; +import AreaSeriesRenderer from './AreaSeriesRenderer'; +import MarkerRenderer, { renderMarkerJSX } from './MarkerRenderer'; +import DataLabelRenderer, { + renderDataLabelShapesJSX, + renderDataLabelTextJSX +} from './DataLabelRender'; +import StackingColumnSeriesRenderer from './StackingColumnSeriesRenderer'; +import StackingBarSeriesRenderer from './StackingBarSeriesRenderer'; +import ScatterSeriesRenderer from './ScatterSeriesRenderer'; +import BubbleSeriesRenderer from './BubbleSeriesRenderer'; +import SplineAreaSeriesRenderer from './SplineAreaSeriesRenderer'; +import { ChartSeriesType } from '../../base/enum'; +import { Chart, DataLabelRendererResult, DataPoint, Points, Rect, RenderOptions, SeriesModules, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { isEqual, useStableDataLabelProps, useStableDataSources, useStableMarkerProps } from '../../hooks/useDeepCompare'; +// Create a global chart instance for storing options +export const chart: Chart = {} as Chart; +// Add our custom properties to the chart object +/** + * Constants for animation durations + */ +const LEGEND_ANIMATION_DURATION: number = 300; +const DEFAULT_ANIMATION_DURATION: number = 500; +/** + * Extensions to the Chart type with additional properties for rendering. + * These properties store options needed for rendering various chart elements. + */ +type ChartExtensions = { + /** Array of render options for each series */ + seriesOptions: RenderOptions[][]; + /** Marker properties for series data points */ + markerOptions: ChartMarkerProps[]; + /** Data label configuration and position information */ + dataLabelOptions: DataLabelRendererResult[][]; +}; + +(chart as Chart & ChartExtensions).seriesOptions = []; +(chart as Chart & ChartExtensions).markerOptions = []; +(chart as Chart & ChartExtensions).dataLabelOptions = []; + +/** + * Storage for chart-specific rendering options, organized by chart ID. + * This allows multiple charts to maintain their own configuration state. + */ +const seriesOptionsByChartId: { [chartId: string]: RenderOptions[][] } = {}; +const markersOptionsByChartId: { [chartId: string]: ChartMarkerProps[] } = {}; +const dataLabelOptionsByChartId: { [chartId: string]: DataLabelRendererResult[][] } = {}; + +export const seriesModules: SeriesModules = { + 'lineSeriesModule': LineSeriesRenderer, + 'splineSeriesModule': SplineSeriesRenderer, + 'columnSeriesModule': ColumnSeries, + 'barSeriesModule': BarSeries, + 'stepLineSeriesModule': StepLineSeriesRenderer, + 'areaSeriesModule': AreaSeriesRenderer, + 'stackingColumnSeriesModule': StackingColumnSeriesRenderer, + 'stackingBarSeriesModule': StackingBarSeriesRenderer, + 'scatterSeriesModule': ScatterSeriesRenderer, + 'bubbleSeriesModule': BubbleSeriesRenderer, + 'splineAreaSeriesModule': SplineAreaSeriesRenderer +}; + +const processRenderResult: (renderResult: RenderOptions[] | { + options: RenderOptions[]; + marker: ChartMarkerProps; +}, chartId: string, series: SeriesProperties) => RenderOptions[] = ( + renderResult: RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }, + chartId: string, + series: SeriesProperties +): RenderOptions[] => { + // Check if the result has options property (indicating it's an object with options and marker) + const hasMarkerData: boolean = typeof renderResult === 'object' && 'options' in renderResult; + const options: RenderOptions[] = hasMarkerData ? + (renderResult as { options: RenderOptions[] }).options : + renderResult as RenderOptions[]; + if (!seriesOptionsByChartId[chartId as string]) { + seriesOptionsByChartId[chartId as string] = []; + } + // Process marker data if it exists + if (hasMarkerData && 'marker' in renderResult) { + if (!markersOptionsByChartId[chartId as string] && !series.skipMarkerAnimation) { + markersOptionsByChartId[chartId as string] = []; + } + const markerData: ChartMarkerProps = (renderResult as { marker: ChartMarkerProps }).marker; + markersOptionsByChartId[chartId as string].push(markerData); + (chart as Chart).markerOptions = [...((chart as Chart).markerOptions as ChartMarkerProps[]), markerData]; + } + // Store and return the render options + seriesOptionsByChartId[chartId as string].push(options); + (chart as Chart & ChartExtensions as { seriesOptions: RenderOptions[][] }).seriesOptions = seriesOptionsByChartId[chartId as string]; + + return options; +}; + +/** + * Calculates and assigns the clip rectangle for a series based on its axis properties. + * + * @param {SeriesProperties} series - The series for which to calculate the clip rectangle. + * @returns {void} + */ +const findClipRect: (series: SeriesProperties) => void = (series: SeriesProperties): void => { + const rect: Rect = { x: 0, y: 0, width: 0, height: 0 }; + if (series.chart?.requireInvertedAxis) { + rect.x = series.yAxis.rect.x; + rect.y = series.xAxis.rect.y; + rect.width = series.yAxis.rect.width; + rect.height = series.xAxis.rect.height; + } else { + rect.x = series.xAxis.rect.x; + rect.y = series.yAxis.rect.y; + rect.width = series.xAxis.rect.width; + rect.height = series.yAxis.rect.height; + } + series.clipRect = rect; +}; + +/** + * Updates the collection of series to be rendered based on changes to the x or y axes. + * + * @param {boolean} xAxis - Indicates if the update is related to the x-axis. + * @param {boolean} yAxis - Indicates if the update is related to the y-axis. + * @param {SeriesProperties} series - The series to update. + * @param {boolean} [isLegendClicked] - Whether the update was triggered by a legend click event. + * @returns {void} + */ +const updateSeries: (xAxis: boolean, yAxis: boolean, series: SeriesProperties, isLegendClicked?: boolean) => void = + (xAxis: boolean, yAxis: boolean, series: SeriesProperties, isLegendClicked?: boolean): void => { + const chartId: string = series.chart ? series.chart.element.id : 'default'; + let seriesCollection: SeriesProperties[] = []; + seriesCollection = (xAxis && yAxis) + ? Array.from(new Set(series.xAxis.series.concat(series.yAxis.series))) + : (xAxis ? series.xAxis.series : series.yAxis.series).slice(); + if (isLegendClicked) { + if (series.visible || series.type?.indexOf('Stacking') !== -1) { + findClipRect(series); + let seriesType: string = firstToLowerCase(series.type as string); + seriesType = seriesType.replace('100', ''); + const renderResult: RenderOptions[] | { + options: RenderOptions[]; + marker: ChartMarkerProps; + } = seriesModules[seriesType + 'SeriesModule' as keyof typeof seriesModules] + .render(series, series.chart.requireInvertedAxis); + processRenderResult(renderResult, chartId, series); + } + } + else { + seriesOptionsByChartId[chartId as string] = []; + (chart as Chart).seriesOptions = []; + if (series.skipMarkerAnimation) { + markersOptionsByChartId[chartId as string] = []; + } + for (const series of seriesCollection) { + if (series.visible || series.type?.indexOf('Stacking') !== -1) { + findClipRect(series); + let seriesType: string = firstToLowerCase(series.type as string); + seriesType = seriesType.replace('100', ''); + const renderResult: RenderOptions[] | { + options: RenderOptions[]; + marker: ChartMarkerProps; + } = seriesModules[seriesType + 'SeriesModule' as keyof typeof seriesModules] + .render(series, series.chart.requireInvertedAxis); + processRenderResult(renderResult, chartId, series); + } + } + } + }; + +/** + * Component responsible for rendering all chart series types. + * Handles animations, markers, data labels, and series paths. + * + * @param {SeriesProperties} props - The properties for the series + * @param {React.ForwardedRef} ref - Reference to the SVG group element + * @returns {React.ReactElement | null} The rendered series elements or null during measuring phase + */ +export const SeriesRenderer: React.ForwardRefExoticComponent> = + forwardRef((props: ChartSeriesProps[], ref: React.ForwardedRef) => { + const [_markerVersion, setMarkerVersion] = useState(0); + const [labelOpacity, setLabelOpacity] = useState(0); + const [_dataLabelVersion, setDataLabelVersion] = useState(0); + const [_seriesVersion, setSeriesVersion] = useState(0); + const { layoutRef, reportMeasured, setDisableAnimation, phase, triggerRemeasure + , animationProgress, setAnimationProgress } = useLayout(); + const animationFrameRef: React.RefObject = useRef(0); + const previousPathLengthRef: React.RefObject = useRef([]); + const previousSeriesOptionsRef: React.RefObject = useRef([]); + const isInitialRenderRef: React.RefObject = useRef([]); + const isFirstRenderRef: React.RefObject = useRef(true); + const renderedPathDRef: React.RefObject = useRef([]); + const internalDataUpdateRef: React.RefObject = useRef(false); + const previousDataSourcesRef: React.RefObject = useRef([]); + + if (phase !== 'measuring') { + previousDataSourcesRef.current = (layoutRef.current.chart as Chart).visibleSeries?. + map((s: SeriesProperties) => s.dataSource) as Object[]; + } + /** + * Analyzes changes in a series data source and applies appropriate animations. + * + * @param {DataPoint[]} newDataSource - The updated data source to analyze. + * @param {number} index - The index of the series in the chart. + * @returns {void} + */ + const observeDataSourceChange: (newDataSource: DataPoint[], index: number) => void = + (newDataSource: DataPoint[], index: number) => { + const series: SeriesProperties | undefined = (layoutRef.current.chart as Chart)?.visibleSeries?.[index as number]; + const prevDataSource: DataPoint[] = previousDataSourcesRef.current[index as number] as DataPoint[]; + let xValueChanged: boolean = false; + if (prevDataSource && newDataSource && prevDataSource.length < newDataSource.length) { + for (let id: number = 0; id < newDataSource.length; id++) { + if (prevDataSource && id < prevDataSource.length && prevDataSource[id as number] && + prevDataSource[id as number].x !== newDataSource[id as number].x) { + xValueChanged = true; + break; + } + } + } + if (series?.animation?.enable && !xValueChanged) { + if (newDataSource.length > prevDataSource.length && (newDataSource.length - prevDataSource.length) === 1) { + const addedPoint: DataPoint | undefined = newDataSource.find((np: DataPoint) => + !prevDataSource.some((p: DataPoint) => p.x === np.x && p.y === np.y) + ); + + if (addedPoint) { + series.skipMarkerAnimation = true; + series.isPointAdded = true; + addPoint( + addedPoint as DataPoint, + series.animation.duration || DEFAULT_ANIMATION_DURATION, + series, + internalDataUpdateRef, + setAnimationProgress, + animationFrameRef, + updateSeries + ); + } + } else if (newDataSource.length < prevDataSource.length && (prevDataSource.length - newDataSource.length) === 1) { + const removedIndex: number = prevDataSource.findIndex((p: DataPoint) => + !newDataSource.some((np: DataPoint) => np.x === p.x && np.y === p.y) + ); + if (removedIndex >= 0) { + series.isPointRemoved = true; + series.skipMarkerAnimation = true; + removePoint( + removedIndex, + series.animation.duration || DEFAULT_ANIMATION_DURATION, + series, + internalDataUpdateRef, + setAnimationProgress, + animationFrameRef, + updateSeries + ); + if (previousSeriesOptionsRef && + previousSeriesOptionsRef.current && + previousSeriesOptionsRef.current[series.index] && + Array.isArray(previousSeriesOptionsRef.current[series.index])) { + previousSeriesOptionsRef.current[series.index].splice(removedIndex, 1); + } + } + } else { + let hasUpdate: boolean = false; + const updatedData: Object[] = Array.isArray(prevDataSource) + ? prevDataSource.map((prevPoint: DataPoint, idx: number) => { + const newPoint: DataPoint = newDataSource[idx as number]; + if (prevPoint.x === newPoint.x && prevPoint.y !== newPoint.y) { + hasUpdate = true; + return newPoint; + } + return prevPoint; + }) + : []; + + if (hasUpdate) { + series.skipMarkerAnimation = true; + series.pointUpdated = true; + setData( + updatedData, + series.animation.duration || DEFAULT_ANIMATION_DURATION, + series, + internalDataUpdateRef, + setAnimationProgress, + animationFrameRef, + updateSeries + ); + } + } + } + previousDataSourcesRef.current[index as number] = [newDataSource]; + }; + + const seriesList: ChartSeriesProps[] = Object.values(props); + type SeriesLike = SeriesProperties | null | undefined | object; + + function isSeriesProperties(item: SeriesLike): item is SeriesProperties { + return ( + typeof item === 'object' && + item !== null && + 'visible' in item && + 'fill' in item && + 'name' in item + ); + } + /** + * Effect that monitors changes to series data sources and triggers appropriate animations. + * Detects point additions, removals, and updates to animate them smoothly. + */ + useEffect(() => { + if (phase !== 'measuring') { + seriesList.forEach((seriesConfig: ChartSeriesProps, index: number) => { + if (Array.isArray(seriesConfig.dataSource) && seriesConfig.dataSource.length > 0) { + const newDataSource: DataPoint[] = seriesConfig.dataSource as DataPoint[]; + const prevDataSource: Object[] = previousDataSourcesRef.current[index as number] as Object[]; + void (!areDataSourcesEqual(prevDataSource, newDataSource) && observeDataSourceChange(newDataSource, index)); + previousDataSourcesRef.current[index as number] = newDataSource; + } + }); + } + }, [useStableDataSources(seriesList)]); + const visibleSeries: ChartSeriesProps | undefined = seriesList.find( + (series: ChartSeriesProps) => series.visible + ); + const isDataLabelEnabled: boolean = !!visibleSeries?.marker?.dataLabel?.visible; + const isAnimationEnabled: boolean = !!visibleSeries?.animation?.enable; + const durations: number = visibleSeries?.animation?.duration || 1000; + const firstAnimationCompletedRef: React.RefObject = useRef(false); + + useEffect(() => { + if ( + isDataLabelEnabled && + isAnimationEnabled && + animationProgress === 1 && + !firstAnimationCompletedRef.current + ) { + firstAnimationCompletedRef.current = true; + const dataLabelAnimationDuration: number = durations * 0.2; + let raf: number; + let start: number | null = null; + /** + * Animates the opacity of data labels from 0 to 1 over time. + * + * @param {number} timestamp - The current animation timestamp + * @returns {void} + */ + const animateOpacity: (timestamp: number) => void = (timestamp: number) => { + if (!start) {start = timestamp; } + const elapsed: number = timestamp - start; + const eased: number = Math.min(elapsed / (dataLabelAnimationDuration), 1); + setLabelOpacity(eased); + if (eased < 1) { + raf = requestAnimationFrame(animateOpacity); + } + }; + raf = requestAnimationFrame(animateOpacity); + return () => cancelAnimationFrame(raf); + } + if (!isDataLabelEnabled || !isAnimationEnabled) { + setLabelOpacity(isAnimationEnabled ? 0 : 1); + } + return undefined; + }, [isDataLabelEnabled, animationProgress, isAnimationEnabled, durations]); + + /** + * Effect that detects and responds to changes in marker properties. + * Updates marker rendering when properties like fill, shape, size, etc. change. + */ + useEffect(() => { + if (phase !== 'measuring') { + const hasMarkerChanges: boolean | undefined = + (layoutRef.current.chart as Chart)?.visibleSeries?.some((series: SeriesProperties, index: number) => { + const currentSeries: ChartSeriesProps = seriesList[index as number]; + + const currentMarker: ChartMarkerProps = currentSeries?.marker as ChartMarkerProps; + const previousMarker: ChartMarkerProps = series.marker as ChartMarkerProps; + + return ( + currentMarker?.fill !== previousMarker?.fill || + currentMarker?.shape !== previousMarker?.shape || + currentMarker?.width !== previousMarker?.width || + currentMarker?.height !== previousMarker?.height || + currentMarker?.visible !== previousMarker?.visible || + currentMarker?.opacity !== previousMarker?.opacity || + !isEqual(currentMarker?.border, previousMarker?.border) + ); + }); + + if (hasMarkerChanges) { + // Reset marker options + const chartId: string = (layoutRef.current.chart as Chart)?.element.id; + markersOptionsByChartId[chartId as string] = []; + (chart as Chart).markerOptions = []; + + (layoutRef.current.chart as Chart)?.visibleSeries?.forEach((series: SeriesProperties, index: number) => { + series.marker = { + ...series.marker, + ...seriesList[index as number]?.marker + }; + + void (series.visible && ( + ((marker: Object) => { + void (marker && ( + (markersOptionsByChartId[chartId as string] ??= []), + markersOptionsByChartId[chartId as string].push(marker as ChartMarkerProps) + )); + })(MarkerRenderer.render(series)) + )); + }); + setMarkerVersion((prev: number) => prev + 1); + } + } + }, [ + useStableMarkerProps(Object.values(props)) + ]); + /** + * Effect that handles updates to data label configurations. + * Manages data label rendering and appearance based on series configuration. + */ + useEffect(() => { + if (phase !== 'measuring') { + if (layoutRef.current.chart as Chart) { + (layoutRef.current.chart as Chart).dataLabelCollections = []; + (chart as Chart).dataLabelOptions = []; + const chartId: string = (layoutRef.current.chart as Chart)?.element.id; + dataLabelOptionsByChartId[chartId as string] = []; + + (layoutRef.current.chart as Chart)?.visibleSeries?.forEach((series: SeriesProperties, index: number) => { + if (!series.visible || (!series.marker?.visible && !series.marker?.dataLabel?.visible)) { + return; + } + // Merge updated dataLabel for each series (if applicable) + series.marker = { + ...series.marker, + dataLabel: { + ...series.marker?.dataLabel as ChartDataLabelProps, + ...seriesList[index as number]?.marker?.dataLabel + } + }; + void (series.visible && ( + ((dataLabel: DataLabelRendererResult[]) => { + void (dataLabel && ( + (dataLabelOptionsByChartId[chartId as string] ??= []), + dataLabelOptionsByChartId[chartId as string].push(dataLabel), + ((chart as Chart & ChartExtensions as ChartExtensions).dataLabelOptions = + [...((chart as Chart & ChartExtensions as ChartExtensions).dataLabelOptions), dataLabel]) + )); + })(DataLabelRenderer.render( + series, + series.marker?.dataLabel as ChartDataLabelProps + ) as DataLabelRendererResult[]) + )); + }); + } + setDataLabelVersion((prev: number) => prev + 1); + } + }, [ + // This dependency array ensures the effect watches ALL essential style changes in dataLabelOptions + useStableDataLabelProps(Object.values(props)), + animationProgress + ]); + + const seriesDelayTimeoutRef: React.RefObject<{ + [key: number]: number; + }> = useRef<{ [key: number]: number }>({}); + useEffect(() => { + return () => { + // Clear all delay timers on unmount + Object.values(seriesDelayTimeoutRef.current).forEach((timerId: number) => clearTimeout(timerId)); + seriesDelayTimeoutRef.current = {}; + }; + }, []); + + /** + * Renders a given series based on its axis type and available points, including animations if enabled. + * + * @param {SeriesProperties} series - The series to render. + * @returns {void} This function does not return a value. + */ + const renderSeries: (series: SeriesProperties) => void = (series: SeriesProperties): void => { + const chartId: string = series.chart.element.id; + let seriesType: string = firstToLowerCase(series.type as string); + seriesType = seriesType.replace('100', ''); + let options: RenderOptions[] = []; + let marker: ChartMarkerProps | null = null; + series.skipMarkerAnimation = false; + void (series.marker && ( + ((result: RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }) => { + options = 'options' in result ? result.options as RenderOptions[] : result; + marker = 'marker' in result ? result.marker as ChartMarkerProps : null; + })(seriesModules[seriesType + 'SeriesModule' as keyof typeof seriesModules].render(series, series.chart.requireInvertedAxis)) + )); + series.visiblePoints = useVisiblePoints(series); + let labelOptions: DataLabelRendererResult[] | undefined; + + // Always check for and render dataLabels if they should be visible + const hasDataLabel: boolean = series.marker?.dataLabel?.visible === true; + if (hasDataLabel && series.marker?.dataLabel) { + labelOptions = DataLabelRenderer.render(series, series.marker.dataLabel) as + DataLabelRendererResult[]; + + // Always ensure data labels are stored in the chart options + // This is crucial to prevent them from disappearing on resize + if (labelOptions && labelOptions.length > 0) { + // Store data label options directly in chart + dataLabelOptionsByChartId[chartId as string] = dataLabelOptionsByChartId[chartId as string] ?? []; + + // Find and replace existing data labels for this series + const seriesIndex: number = series.index as number; + const existingIndex: number = dataLabelOptionsByChartId[chartId as string]. + findIndex((_: DataLabelRendererResult[], i: number) => i === seriesIndex); + + if (existingIndex !== -1) { + dataLabelOptionsByChartId[chartId as string][existingIndex as number] = labelOptions; + } else { + while (dataLabelOptionsByChartId[chartId as string].length < seriesIndex) { + dataLabelOptionsByChartId[chartId as string].push([]); + } + dataLabelOptionsByChartId[chartId as string][seriesIndex as number] = labelOptions; + } + (chart as Chart & ChartExtensions as { dataLabelOptions: DataLabelRendererResult[][] }).dataLabelOptions = + dataLabelOptionsByChartId[chartId as string]; + } + } + + if (series.animation?.enable && (layoutRef.current.chart as Chart).animateSeries) { + const seriesDelay: number = Number(series.animation?.delay) || 0; + /** + * Starts the animation for a series from the specified start time. + * + * @param {DOMHighResTimeStamp} startTime - The timestamp when the animation starts + * @returns {void} + */ + const startAnimation: (startTime: DOMHighResTimeStamp) => void = (startTime: DOMHighResTimeStamp) => { + seriesOptionsByChartId[chartId as string] = seriesOptionsByChartId[chartId as string] ?? []; + seriesOptionsByChartId[chartId as string].push(options); + (chart as Chart & ChartExtensions as ChartExtensions).seriesOptions = seriesOptionsByChartId[chartId as string]; + + if (marker) { + // Store marker options directly in chart + markersOptionsByChartId[chartId as string] = markersOptionsByChartId[chartId as string] ?? []; + markersOptionsByChartId[chartId as string].push(marker); + (chart as Chart).markerOptions = markersOptionsByChartId[chartId as string]; + } + animatePath(startTime, series.animation?.duration as number, setAnimationProgress, animationFrameRef, startTime); + }; + + if (seriesDelay > 0) { + seriesDelayTimeoutRef.current[series.index as number] = + setTimeout(() => { + requestAnimationFrame(startAnimation); + delete seriesDelayTimeoutRef.current[series.index as number]; + }, seriesDelay); + } else { + requestAnimationFrame(startAnimation); + } + } else { + // Store series options directly in chart + seriesOptionsByChartId[chartId as string] = seriesOptionsByChartId[chartId as string] ?? []; + seriesOptionsByChartId[chartId as string].push(options); + (chart as Chart & ChartExtensions as ChartExtensions).seriesOptions = seriesOptionsByChartId[chartId as string]; + + if (marker) { + // Store marker options directly in chart + markersOptionsByChartId[chartId as string] = markersOptionsByChartId[chartId as string] ?? []; + markersOptionsByChartId[chartId as string].push(marker); + (chart as Chart).markerOptions = markersOptionsByChartId[chartId as string]; + } + } + }; + + const triggerLegendShapeRender: (chartId?: string) => void = useRegisterLegendShapeRender(); + /** + * Effect that updates legend shapes when series properties change. + * Ensures legend items correctly reflect the visual appearance of series. + */ + useEffect(() => { + if (phase !== 'measuring' && layoutRef.current.chart) { + (layoutRef.current.chart as Chart).visibleSeries?.forEach((series: SeriesProperties, index: number) => { + series.legendShape = seriesList[index as number]?.legendShape || series.legendShape; + }); + triggerLegendShapeRender((layoutRef.current.chart as Chart).element.id); + } + }, [JSON.stringify(seriesList.map((s: ChartSeriesProps) => s.legendShape)), phase]); + /** + * Effect that runs during the measuring phase to initialize series rendering. + * Resets chart options and prepares series for rendering. + */ + useLayoutEffect(() => { + if (phase === 'measuring') { + // Reset options when measuring + const chartId: string = (layoutRef.current.chart as Chart).element.id; + + seriesOptionsByChartId[chartId as string] = []; + markersOptionsByChartId[chartId as string] = []; + + // Don't reset data label options - this causes them to disappear on resize + if (!dataLabelOptionsByChartId[chartId as string]) { + dataLabelOptionsByChartId[chartId as string] = []; + } + + (chart as Chart).seriesOptions = []; + (chart as Chart).markerOptions = []; + // Don't reset global data labels either + if (!(chart as Chart).dataLabelOptions || (chart as Chart).dataLabelOptions?.length === 0) { + (chart as Chart).dataLabelOptions = []; + } + + isInitialRenderRef.current = (layoutRef.current.chart as Chart).visibleSeries?.map(() => true) as boolean[]; + previousPathLengthRef.current = (layoutRef.current.chart as Chart).visibleSeries?.map(() => 0) as number[]; + if (isFirstRenderRef.current) { + previousSeriesOptionsRef.current = (layoutRef.current.chart as Chart).visibleSeries?.map(() => []) as RenderOptions[][]; + isFirstRenderRef.current = true; + } else { + isFirstRenderRef.current = false; + } + + (layoutRef.current.chart as Chart).visibleSeries?.map((series: SeriesProperties) => { + if (series.visible) { + series.chart = layoutRef.current.chart as Chart; + findClipRect(series); + renderSeries(series); + } + }); + reportMeasured('ChartSeries'); + } + }, [phase, layoutRef]); + + /** + * Extracts and processes essential properties from each series in the props. + * This creates a lightweight representation of series data for change detection. + * + * @returns {Array} Array of processed series data objects + */ + const processedSeriesData: { + fill: string | undefined | null; + name: string | undefined; + width: number | undefined; + dashArray: string | undefined; + opacity: number | undefined; + splineType: string | undefined; + type: string | undefined; + emptyPointSettings: { + mode?: string, + }, + columnSpacing: number | undefined; + columnWidth: number | undefined; + columnWidthInPixel: number | undefined; + }[] = useMemo(() => { + return Object.keys(props) + .filter((key: string) => !isNaN(Number(key))) + .map((key: string): SeriesLike => props[(Number(key))]) + .filter(isSeriesProperties) + .map((series: SeriesProperties) => ({ + fill: series.fill, + name: series.name, + width: series.width, + dashArray: series.dashArray, + opacity: series.opacity, + splineType: series.splineType, + type: series.type, + step: series.step, + emptyPointSettings: { + mode: series.emptyPointSettings?.mode + }, + columnSpacing: series.columnSpacing, + columnWidth: series.columnWidth, + columnWidthInPixel: series.columnWidthInPixel + })); + }, [props]); + + const serializedSeriesData: string = useMemo(() => { + return JSON.stringify(processedSeriesData); + }, [processedSeriesData]); + + /** + * Effect that responds to changes in series data to update rendering. + * Handles series visibility, property updates, and re-rendering. + */ + useEffect(() => { + if (phase !== 'measuring' && layoutRef.current.chart) { + let reRender: boolean = false; + const chartId: string = (layoutRef.current.chart as Chart).element.id; + seriesOptionsByChartId[chartId as string] = []; + markersOptionsByChartId[chartId as string] = []; + dataLabelOptionsByChartId[chartId as string] = []; + (layoutRef.current.chart as Chart).dataLabelCollections = []; + (chart as Chart).seriesOptions = []; + (chart as Chart).markerOptions = []; + (chart as Chart).dataLabelOptions = []; + + (layoutRef.current.chart as Chart).visibleSeries?.forEach((visibleSeries: SeriesProperties) => { + const matchingSeries: ChartSeriesProps = seriesList[visibleSeries.index]; + const newType: ChartSeriesType | undefined = matchingSeries.type || visibleSeries.type; + matchingSeries.visible = visibleSeries.visible; + if (visibleSeries.type !== newType) { + reRender = true; + } + visibleSeries.type = newType; + }); + + if ((layoutRef.current.chart as Chart).visibleSeries?.length !== processedSeriesData.length || reRender) { + triggerRemeasure(); + setDisableAnimation?.(false); + internalDataUpdateRef.current = true; + } else { + (layoutRef.current.chart as Chart).visibleSeries?.forEach((series: SeriesProperties) => { + series.propsChange = true; + }); + (layoutRef.current.chart as Chart).animateSeries = false; + (layoutRef.current.chart as Chart).visibleSeries?.map((visibleSeries: SeriesProperties) => { + const matchingSeries: ChartSeriesProps = seriesList[visibleSeries.index]; + + // Update properties directly on visibleSeries + visibleSeries.interior = matchingSeries.fill || visibleSeries.interior || ''; + visibleSeries.width = matchingSeries.width || visibleSeries.width; + visibleSeries.dashArray = matchingSeries.dashArray || visibleSeries.dashArray; + visibleSeries.name = matchingSeries.name || visibleSeries.name; + visibleSeries.visible = matchingSeries.visible !== undefined ? matchingSeries.visible : visibleSeries.visible; + visibleSeries.opacity = matchingSeries.opacity || visibleSeries.opacity; + visibleSeries.splineType = matchingSeries.splineType || visibleSeries.splineType; + visibleSeries.type = matchingSeries.type || visibleSeries.type; + visibleSeries.emptyPointSettings = matchingSeries.emptyPointSettings || visibleSeries.emptyPointSettings; + visibleSeries.step = matchingSeries.step || visibleSeries.step; + visibleSeries.columnWidth = matchingSeries.columnWidth || visibleSeries.columnWidth; + visibleSeries.columnSpacing = matchingSeries.columnSpacing || visibleSeries.columnSpacing; + visibleSeries.columnWidthInPixel = matchingSeries.columnWidthInPixel || visibleSeries.columnWidthInPixel; + // Render if visible + if (visibleSeries.visible) { + visibleSeries.chart = layoutRef.current.chart as Chart; + findClipRect(visibleSeries); + visibleSeries.currentViewData = visibleSeries.dataSource || []; + processJsonData(visibleSeries); + renderSeries(visibleSeries); + } + + return visibleSeries; + }); + } + setSeriesVersion((prev: number) => prev + 1); + } + }, [serializedSeriesData]); + + useEffect(() => { + if (internalDataUpdateRef.current) { + internalDataUpdateRef.current = false; + return; + } + if (phase !== 'measuring') { + const chartId: string = (layoutRef.current.chart as Chart)?.element.id; + seriesOptionsByChartId[chartId as string] = []; + (chart as Chart).seriesOptions = []; + triggerRemeasure(); + setDisableAnimation?.(true); + } + + }, [ + JSON.stringify( + seriesList.map((series: ChartSeriesProps) => ({ + dataSource: series?.dataSource + })) + ) + ]); + + // Use the correct type annotation for the version info objects + const legendClickedInfo: { version: number; id: string } = useSeriesRenderVersion(); + const axesChanged: { version: number; id: string } = useAxesRendereVersion(); + + useEffect(() => { + if (phase !== 'measuring' && ((legendClickedInfo && legendClickedInfo.id === (layoutRef.current.chart as Chart)?.element.id) || + (axesChanged && axesChanged.id === (layoutRef.current.chart as Chart)?.element.id))) { + const chartId: string = (layoutRef.current.chart as Chart).element.id; + seriesOptionsByChartId[chartId as string] = []; + markersOptionsByChartId[chartId as string] = []; + dataLabelOptionsByChartId[chartId as string] = []; + internalDataUpdateRef.current = true; + (layoutRef.current.chart as Chart).dataLabelCollections = []; + + (layoutRef.current.chart as Chart).visibleSeries?.map((series: SeriesProperties) => { + + const startTimestamp: number = performance.now(); + if (series.visible) { + series.position = undefined; + } + if (series.chart && series.chart.isGestureZooming) { + updateSeries(true, true, series, false); + if (series.visible) { + if (series.marker?.dataLabel?.visible) { + const dataLabel: DataLabelRendererResult[] = DataLabelRenderer. + render(series, series.marker?.dataLabel as + ChartDataLabelProps) as DataLabelRendererResult[]; + if (dataLabel) { + if (!dataLabelOptionsByChartId[chartId as string]) { + dataLabelOptionsByChartId[chartId as string] = []; + } + dataLabelOptionsByChartId[chartId as string].push(dataLabel); + } + } + } + if ((layoutRef.current.chart as Chart).visibleSeries + && series.index === (layoutRef.current.chart as Chart).visibleSeries.length - 1) { + (layoutRef.current.chart as Chart).zoomRedraw = false; + } + series.chart.isGestureZooming = false; + setSeriesVersion((prev: number) => prev + 1); + } + else { + requestAnimationFrame(() => { + updateSeries(true, true, series, true); + if (series.visible) { + if (legendClickedInfo?.version !== 0) { + series.isLegendClicked = true; + } + if (series.marker?.dataLabel?.visible) { + const dataLabel: DataLabelRendererResult[] = DataLabelRenderer. + render(series, series.marker?.dataLabel as + ChartDataLabelProps) as DataLabelRendererResult[]; + if (dataLabel) { + // Store data label options + if (!dataLabelOptionsByChartId[chartId as string]) { + dataLabelOptionsByChartId[chartId as string] = []; + } + dataLabelOptionsByChartId[chartId as string].push(dataLabel); + } + } + } + animatePath(startTimestamp, (series && series.type + && series.type?.indexOf('Stacking') > -1) ? DEFAULT_ANIMATION_DURATION + : LEGEND_ANIMATION_DURATION, setAnimationProgress, animationFrameRef, startTimestamp); + if ((layoutRef.current.chart as Chart).visibleSeries + && series.index === (layoutRef.current.chart as Chart).visibleSeries.length - 1) { + (layoutRef.current.chart as Chart).zoomRedraw = false; + } + }); + } + }); + } + }, [legendClickedInfo?.version, axesChanged]); + + if (phase === 'measuring') { + return null; + } + + // Get the current chart ID + const currentChartId: string = (layoutRef.current.chart as Chart).element.id; + // Get the series options for the current chart - prioritize the per-chartId collections + const currentChartSeriesOptions: RenderOptions[][] = seriesOptionsByChartId[currentChartId as string]; + + return ( + + {currentChartSeriesOptions.length > 0 && currentChartSeriesOptions.map( + (_options: RenderOptions[], index: number) => { + const pathOptions: RenderOptions[] = currentChartSeriesOptions[index as number]; + if (pathOptions !== undefined) { + const seriesIndex: number = pathOptions[0] && pathOptions[0].id ? parseInt(pathOptions[0].id.split('_Series_')[1]?.split('_')[0], 10) : index; + if (!Array.isArray(pathOptions) || !(layoutRef.current.chart as Chart).visibleSeries![seriesIndex as number]) { + return null; + } + const visibleSeries: ChartSeriesProps[] | undefined = (layoutRef.current?.chart as Chart)?.visibleSeries; + const animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + } = { + previousPathLengthRef, + isInitialRenderRef, + renderedPathDRef, + animationProgress, + isFirstRenderRef, + previousSeriesOptionsRef + }; + const accessibility: SeriesAccessibility = + visibleSeries![seriesIndex as number].accessibility as SeriesAccessibility; + let tabIndex: number = accessibility?.focusable ? accessibility?.tabIndex as number : -1; + tabIndex = accessibility?.focusable ? (index === 0 ? 0 : + !checkTabindex(visibleSeries as ChartSeriesProps[], seriesIndex) ? + accessibility?.tabIndex as number : -1) : -1; + // Prepare animation state for series rendering + return ( + <> + + + + + + + + {pathOptions.map((pathOption: RenderOptions, pathIndex: number) => { + const currentSeries: SeriesProperties = (layoutRef.current.chart as Chart). + visibleSeries?.[seriesIndex as number] as SeriesProperties; + let seriesType: string = firstToLowerCase(currentSeries.type as string); + seriesType = seriesType.replace('100', ''); + const currentPoint: Points | undefined = currentSeries?.visiblePoints?.[pathIndex as number]; + if (currentPoint !== undefined && currentSeries && seriesType && pathOption) { + const animationProps: { + strokeDasharray: string | number; + strokeDashoffset: number; + interpolatedD?: string; + animatedDirection?: string; + animatedTransform?: string; + animatedClipPath?: string; + scatterTransform?: string; + } = currentSeries.propsChange ? { + strokeDasharray: 'none', + strokeDashoffset: 0 + } : seriesModules[seriesType + 'SeriesModule' as keyof typeof seriesModules].doAnimation( + pathOption, + seriesIndex, + animationState, + visibleSeries?.[seriesIndex as number]?.animation?.enable || false, + currentSeries, + currentPoint, + pathIndex, + visibleSeries as SeriesProperties[] + ); + + return ( + + ); + } + return null; + })} + + {/* Render markers with exact same animation progress as the series */} + {markersOptionsByChartId[currentChartId as string] && + markersOptionsByChartId[currentChartId as string].length !== 0 + && (((layoutRef.current.chart as Chart).visibleSeries as SeriesProperties[])[seriesIndex as number] + ?.isRectSeries && ((layoutRef.current.chart as Chart) + .visibleSeries as SeriesProperties[])[seriesIndex as number] + ?.animation?.enable ? animationProgress === 1 : true) && + ( + renderMarkerJSX( + markersOptionsByChartId[currentChartId as string], + seriesIndex, + animationProgress, + ((layoutRef.current.chart as Chart).visibleSeries as + SeriesProperties[])[seriesIndex as number]?.type, + (layoutRef.current.chart as Chart).element.id, + (((layoutRef.current.chart as Chart).visibleSeries as SeriesProperties[])[seriesIndex as number] + ?.propsChange), + ((layoutRef.current.chart as Chart).visibleSeries as SeriesProperties[])[seriesIndex as number], + // Pass distinct progress for add/remove + ((layoutRef.current.chart as Chart).visibleSeries as + SeriesProperties[])[seriesIndex as number]?.isPointAdded ? animationProgress : 1, + ((layoutRef.current.chart as Chart).visibleSeries as + SeriesProperties[])[seriesIndex as number]?.isPointRemoved ? animationProgress : 1 + ) + )} + + ); + } + return null; + } + )} + + {/* Render a single data label collection for all series */} + {dataLabelOptionsByChartId[currentChartId as string] && + dataLabelOptionsByChartId[currentChartId as string].length > 0 && ( + series.skipMarkerAnimation) ? 1 : labelOpacity + }}> + {/* First render all shape groups */} + {(dataLabelOptionsByChartId[currentChartId as string]) + .map((dataLabel: DataLabelRendererResult[], mapIndex: number) => { + + // Extract series index from ID dynamically + let foundId: string | undefined; + for (const lbl of dataLabel) { + if (lbl && lbl.textOption && lbl.textOption.renderOptions && lbl.textOption.renderOptions.id) { + foundId = lbl.textOption.renderOptions.id; + break; + } + } + + let actualSeriesIndex: number = mapIndex; + if (foundId) { + const match: RegExpMatchArray = foundId.match(/_Series_(\d+)_/) as RegExpMatchArray; + if (match) {actualSeriesIndex = parseInt(match[1], 10); } + } + + if (!dataLabel || + dataLabel.length === 0 || + !(layoutRef.current?.chart as Chart)?.visibleSeries?.[actualSeriesIndex as number] || + !(layoutRef.current?.chart as Chart)?.visibleSeries?.[actualSeriesIndex as number]?. + marker?.dataLabel?.visible) { + return null; + } + + return ( + + {renderDataLabelShapesJSX(dataLabel, actualSeriesIndex, layoutRef, animationProgress)} + + ); + })} + + {(dataLabelOptionsByChartId[currentChartId as string]) + .map((dataLabel: DataLabelRendererResult[], seriesIndex: number) => { + let id: string | undefined; + let actualSeriesIndex: number = seriesIndex; + for (const lbl of dataLabel) { + if (lbl && lbl.textOption && lbl.textOption.renderOptions && lbl.textOption.renderOptions.id) { + id = lbl.textOption.renderOptions.id; + break; + } + } + + if (id) { + const match: RegExpMatchArray = id.match(/_Series_(\d+)_/) as RegExpMatchArray; + if (match) {actualSeriesIndex = parseInt(match[1], 10); } + } + if (!dataLabel || + dataLabel.length === 0 || + !(layoutRef.current?.chart as Chart)?.visibleSeries?.[actualSeriesIndex as number] || + !(layoutRef.current?.chart as Chart)?.visibleSeries?. + [actualSeriesIndex as number]?.marker?.dataLabel?.visible) { + return null; + } + + return ( + + {renderDataLabelTextJSX(dataLabel, actualSeriesIndex, layoutRef, animationProgress)} + + ); + })} + + + )} + + + ); + }); + + + diff --git a/components/charts/src/chart/renderer/SeriesRenderer/SplineAreaSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/SplineAreaSeriesRenderer.tsx new file mode 100644 index 0000000..124859f --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/SplineAreaSeriesRenderer.tsx @@ -0,0 +1,833 @@ +import { ChartLocationProps, ChartMarkerProps } from '../../base/interfaces'; +import { PathCommand } from '../../common/base'; +import { getPoint, withInRange } from '../../utils/helper'; +import { LineBase, LineBaseReturnType } from './LineBase'; +import { interpolatePathD, parsePathCommands } from './SeriesAnimation'; +import MarkerRenderer from './MarkerRenderer'; +import { CommandValues, ControlPoints, Points, RenderOptions, SeriesProperties, SplineAreaSeriesAnimateState, SplineAreaSeriesInterface } from '../../chart-area/chart-interfaces'; +import SplineSeriesRenderer from './SplineSeriesRenderer'; + +const lineBaseInstance: LineBaseReturnType = LineBase; + +/** + * Implementation of the spline area series renderer for smooth curved area charts. + * This renderer creates area charts with cubic Bezier spline interpolation between data points, + * providing visually appealing smooth curves instead of straight line segments. + * Supports filled areas with optional borders, animations, and marker integration. + */ +const SplineAreaSeriesRenderer: SplineAreaSeriesInterface = { + + naturalSplineCoefficients: (points: Points[]): number[] => { + // Use the same natural spline calculation as SplineSeriesRenderer + return SplineSeriesRenderer.naturalSplineCoefficients(points); + }, + + getControlPoints: (point1: Points, point2: Points, ySpline1: number, ySpline2: number, series?: SeriesProperties): ControlPoints => { + // Use the same control point calculation as SplineSeriesRenderer + return SplineSeriesRenderer.getControlPoints(point1, point2, ySpline1, ySpline2, series as SeriesProperties); + }, + + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + } | SplineAreaSeriesAnimateState, + enableAnimation: boolean, + currentSeries: SeriesProperties + ) => { + // Extract animation state + const { isInitialRenderRef, renderedPathDRef, animationProgress } = animationState; + const isInitial: boolean = isInitialRenderRef.current[index as number]; + + // Get path data + const pathD: string = pathOptions.d as string; + const match: RegExpMatchArray | null = pathOptions.id ? + pathOptions.id.toString().match(/Series_(?:border_)?(\d+)/) : null; + const seriesIndex: number = match ? parseInt(match[1], 10) : 0; + + // Check if this is a border path + const isBorder: boolean = pathOptions.id?.toString().indexOf('border') > -1; + const storedKey: string = `${isBorder ? 'border' : 'area'}_${seriesIndex}`; + const pathDataRef: React.RefObject> = renderedPathDRef as React.RefObject>; + // Ensure we have the proper storage object + if (!pathDataRef.current) { + pathDataRef.current = {}; + } + + if (enableAnimation) { + // For initial render animation + if (isInitial) { + if (animationProgress === 1) { + isInitialRenderRef.current[index as number] = false; + pathDataRef.current[storedKey as string] = pathD; + } + + const sharedRangeKey: string = `animRange_${seriesIndex}`; + + let minX: number; let maxX: number; let range: number; + + interface CachedRange { + minX: number; + maxX: number; + range: number; + } + // Get chart properties from series + const isInverted: boolean = currentSeries.chart?.requireInvertedAxis; + const isXAxisInverse: boolean = currentSeries.xAxis?.isAxisInverse; + const isYAxisInverse: boolean = currentSeries.yAxis?.isAxisInverse; + // If this is a border path, try to use the cached range from area path + if (isBorder && pathDataRef.current[sharedRangeKey as string]) { + const cachedRange: CachedRange = JSON.parse( + pathDataRef.current[sharedRangeKey as string]); + minX = cachedRange.minX; + maxX = cachedRange.maxX; + range = cachedRange.range; + } else { + // Calculate range (this will be the area path on first call) + const commands: PathCommand[] = parsePathCommands(pathD); + const xCoords: number[] = commands + .filter((cmd: PathCommand) => cmd.type !== 'Z' && cmd.params && cmd.params.length >= 2) + .map((cmd: PathCommand) => cmd.params[0]); + + if (xCoords.length === 0) { + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0 + }; + } + + minX = Math.min(...xCoords); + maxX = Math.max(...xCoords); + if (!isInverted) { + // Normal orientation - use X coordinates + const xCoordinates: number[] = xCoords.map((coord: number) => coord); + minX = Math.min(...xCoordinates); + maxX = Math.max(...xCoordinates); + } else { + const yCoords: number[] = commands + .filter((cmd: PathCommand) => cmd.type !== 'Z' && cmd.params && cmd.params.length >= 2) + .map((cmd: PathCommand) => cmd.params[1]); // Get Y coordinates for transposed mode + + if (yCoords.length === 0) { + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0 + }; + } + + minX = Math.min(...yCoords); + maxX = Math.max(...yCoords); + } + range = maxX - minX; + + // **Cache the range for border path to use** + if (!isBorder) { // Only cache when processing area path + pathDataRef.current[sharedRangeKey as string] = JSON.stringify({ + minX, maxX, range, + isInverted, + isXAxisInverse, + isYAxisInverse + }); + } + } + + // Create animated clip path based on progress + let clipPathStr: string = ''; + + // Create animated clip path based on progress and axis configuration + if (!isInverted) { + // Normal orientation - clip horizontally based on X values + if (isXAxisInverse) { + // X-axis is inverted - clip from right to left + const animWidth: number = range * animationProgress; + clipPathStr = `inset(0 0 0 ${range - animWidth}px)`; + } else { + // X-axis is normal - clip from left to right + const animWidth: number = range * animationProgress; + clipPathStr = `inset(0 ${range - animWidth}px 0 0)`; + } + } else { + const animHeight: number = range * animationProgress; + if (isYAxisInverse) { + // Y-axis is inverted - clip from top to bottom + clipPathStr = `inset(${Math.max(0, range - animHeight)}px 0 0 0)`; + } else { + // Y-axis is normal - clip from bottom to top + clipPathStr = `inset(${Math.max(0, range - animHeight)}px 0 0 0)`; + } + } + + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0, + animatedClipPath: clipPathStr + }; + } + // For path animation during updates + else if (pathD) { + const storedD: string = pathDataRef.current[storedKey as string]; + + if (storedD && pathD !== storedD) { + // Use different interpolation methods based on whether it's a border or area + const startPathCommands: string[] = storedD.match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/g) as string[]; + const endPathCommands: string[] = (pathD).match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/g) as string[]; + const maxLength: number = Math.max(startPathCommands.length, endPathCommands.length); + const minLength: number = Math.min(startPathCommands.length, endPathCommands.length); + let endPath: string = pathD; + if (startPathCommands.length > endPathCommands.length) { + for (let i: number = minLength; i < maxLength; i++) { + if (endPathCommands.length !== startPathCommands.length) { + let firstPointBeforeCurve: string; + if (currentSeries.removedPointIndex === currentSeries.points.length) { + if ((startPathCommands[startPathCommands.length - 1]).split(' ').length === 4 && isBorder) { + firstPointBeforeCurve = endPathCommands[endPathCommands.length - (isBorder ? 1 : 2)].split(' ').slice(1).join(' '); + } + else { + firstPointBeforeCurve = endPathCommands[endPathCommands.length - (isBorder ? 1 : 2)].split(' ').slice(5).join(' '); + } + const curveCommand: string = 'C ' + firstPointBeforeCurve + firstPointBeforeCurve + firstPointBeforeCurve; + if (isBorder) { + endPathCommands.push(curveCommand); + } + else { + endPathCommands.splice(endPathCommands.length - 1, 0, curveCommand); + } + } else { + if ((startPathCommands[startPathCommands.length - 1]).split(' ').length === 4) { + firstPointBeforeCurve = 'C ' + endPathCommands[isBorder ? 0 : 1].split(' ').slice(-3).join(' ') + endPathCommands[isBorder ? 0 : 1].split(' ').slice(1).join(' ') + endPathCommands[isBorder ? 0 : 1].split(' ').slice(1).join(' '); + } + else { + firstPointBeforeCurve = 'C ' + endPathCommands[isBorder ? 0 : 1].split(' ').slice(-3).join(' ') + endPathCommands[isBorder ? 0 : 1].split(' ').slice(-3).join(' ') + endPathCommands[isBorder ? 0 : 1].split(' ').slice(-3).join(' '); + } + endPathCommands.splice((isBorder ? 1 : 2), 0, firstPointBeforeCurve); + } + } + } + endPath = endPathCommands.join(''); + } + + const interpolatedD: string = isBorder ? + interpolateSplineBorderPath(storedD, endPath, animationProgress) : + interpolateSplineAreaPath(storedD, endPath, animationProgress); + + if (animationProgress === 1) { + pathDataRef.current[storedKey as string] = pathD; + } + + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0, + interpolatedD + }; + } + } + + // Always store path when animation is complete + if (animationProgress === 1) { + pathDataRef.current[storedKey as string] = pathD; + + const sharedRangeKey: string = `animRange_${seriesIndex}`; + if (pathDataRef.current[sharedRangeKey as string]) { + delete (pathDataRef).current[sharedRangeKey as string]; + } + } + } + + return { + strokeDasharray: isBorder ? (pathOptions.dashArray || 'none') : 'none', + strokeDashoffset: 0 + }; + }, + + render: (series: SeriesProperties, isInverted: boolean ): + RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps } => { + if (!series || !series.points || series.points.length < 1) { + return []; + } + let firstPoint: Points | null = null; + let direction: string = ''; + let topLine: string = ''; + let startPoint: ChartLocationProps | null = null; + let pt2: ChartLocationProps = { x: 0, y: 0 }; + let bpt1: ChartLocationProps = { x: 0, y: 0 }; + let bpt2: ChartLocationProps = { x: 0, y: 0 }; + let controlPt1: ChartLocationProps; + let controlPt2: ChartLocationProps; + const points: Points[] = []; + let point: Points; + let pointIndex: number = 0; + let hasPoints: boolean = false; + const isDropMode: boolean = (series.emptyPointSettings && series.emptyPointSettings.mode === 'Drop') as boolean; + const segmentStartIndices: number[] = []; + const segmentBaselinePoints: ChartLocationProps[] = []; + + const visiblePoints: Points[] = lineBaseInstance.enableComplexProperty(series); + + for (let i: number = 0; i < visiblePoints.length; i++) { + point = visiblePoints[i as number]; + if (point.xValue === null) { + continue; + } else { + point.index = pointIndex; + pointIndex++; + points.push(point); + } + } + + series.visiblePoints = visiblePoints; + + const pointsLength: number = points.length; + let previous: number; + const getCoordinate: Function = getPoint; + const origin: number = Math.max(series.yAxis.visibleRange.minimum, 0); + + const splineCoefficients: number[] = SplineSeriesRenderer.findSplineCoefficients(points, series); + + if (!series.drawPoints) { + series.drawPoints = []; + } + + for (let i: number = 1; i < pointsLength; i++) { + const controlPoints: ControlPoints = SplineAreaSeriesRenderer.getControlPoints( + points[i - 1], + points[i as number], + splineCoefficients[i - 1] || 0, + splineCoefficients[i as number] || 0, + series + ); + series.drawPoints[i - 1] = controlPoints; + } + + const startNewSegment: (index: number) => void = (index: number): void => { + segmentStartIndices.push(index); + const baselinePoint: ChartLocationProps = getCoordinate( + points[index as number].xValue, origin, series.xAxis, series.yAxis, isInverted, series); + segmentBaselinePoints.push(baselinePoint); + + startPoint = baselinePoint; + direction += ('M ' + baselinePoint.x + ' ' + baselinePoint.y + ' '); + + const dataPoint: ChartLocationProps = getCoordinate( + points[index as number].xValue, points[index as number].yValue, series.xAxis, series.yAxis, isInverted, series); + direction += ('L ' + dataPoint.x + ' ' + dataPoint.y + ' '); + topLine += ('M ' + dataPoint.x + ' ' + dataPoint.y + ' '); + }; + + const closeSegment: (index: number) => void = (index: number): void => { + if (!startPoint) { return; } + + const segmentIndex: number = segmentStartIndices.length - 1; + if (segmentIndex < 0) { return; } + + const baselinePoint: ChartLocationProps = segmentBaselinePoints[segmentIndex as number]; + + const currentBaselinePoint: ChartLocationProps = getCoordinate( + points[index as number].xValue, origin, series.xAxis, series.yAxis, isInverted, series); + + direction = direction.concat('L ' + currentBaselinePoint.x + ' ' + currentBaselinePoint.y + ' '); + + direction = direction.concat('L ' + baselinePoint.x + ' ' + baselinePoint.y + ' '); + }; + + for (let i: number = 0; i < pointsLength; i++) { + point = points[i as number]; + point.symbolLocations = []; + point.regions = []; + previous = i > 0 ? i - 1 : 0; + + if ( + point.visible && + (i === 0 || withInRange(points[previous as number], point, points[i < pointsLength - 1 ? i + 1 : i], series)) + ) { + hasPoints = true; + + if (firstPoint === null) { + startNewSegment(i); + } else { + if ( + isDropMode && + previous < i && + !points[previous as number].visible + ) { + controlPt1 = series.drawPoints[previous - 1]?.controlPoint1 ?? firstPoint; + controlPt2 = series.drawPoints[previous - 1]?.controlPoint2 ?? firstPoint; + } else { + controlPt1 = series.drawPoints[previous as number].controlPoint1; + controlPt2 = series.drawPoints[previous as number].controlPoint2; + } + pt2 = getCoordinate(point.xValue, point.yValue, series.xAxis, series.yAxis, isInverted, series); + bpt1 = getCoordinate(controlPt1.x, controlPt1.y, series.xAxis, series.yAxis, isInverted, series); + bpt2 = getCoordinate(controlPt2.x, controlPt2.y, series.xAxis, series.yAxis, isInverted, series); + + const curveCommand: string = 'C ' + bpt1.x + ' ' + bpt1.y + ' ' + bpt2.x + ' ' + bpt2.y + ' ' + pt2.x + ' ' + pt2.y + ' '; + direction = direction.concat(curveCommand); + topLine = topLine.concat(curveCommand); + } + + lineBaseInstance.storePointLocation(point, series, isInverted, getCoordinate); + firstPoint = point; + } else { + if (!isDropMode && firstPoint) { + closeSegment(previous); + firstPoint = null; + } + point.symbolLocations = []; + } + } + + if (firstPoint && hasPoints) { + closeSegment(pointsLength - 1); + } + + const name: string = series.chart.element.id + '_Series_' + series.index; + const options: RenderOptions[] = [{ + id: name, + fill: series.interior, + strokeWidth: 0, + stroke: 'transparent', + opacity: series.opacity, + dashArray: series.dashArray, + d: direction + }]; + + if (series.border && series.border.width && series.border.width > 0) { + const borderName: string = series.chart.element.id + '_Series_border_' + series.index; + const borderOptions: RenderOptions = { + id: borderName, + fill: 'transparent', + strokeWidth: series.border.width, + stroke: series.border.color ? series.border.color : series.interior, + opacity: 1, + dashArray: series.border.dashArray, + d: topLine.trim() + }; + options.push(borderOptions); + } + + const marker: Object | null = series.marker?.visible ? MarkerRenderer.render(series) as Object : null; + return marker ? { options, marker } : options; + } +}; + +export default SplineAreaSeriesRenderer; + +/** + * Specialized interpolation for spline area paths with proper animation + * for point additions and removals + * + * @param {string} startD - Starting path data + * @param {string} endD - Ending path data + * @param {number} progress - Animation progress (0-1) + * @returns {string} Interpolated path + * @private + */ +export function interpolateSplineAreaPath(startD: string, endD: string, progress: number): string { + if (!startD || !endD) { + return endD || startD || ''; + } + + try { + // Parse the path data using the built-in function to avoid type errors + const startCommands: PathCommand[] = parsePathCommands(startD); + const endCommands: PathCommand[] = parsePathCommands(endD); + + // Convert to a more usable format with proper type safety + const convertCommand: (cmd: PathCommand) => CommandValues = (cmd: PathCommand): CommandValues => { + if (cmd.type === 'M' || cmd.type === 'L') { + return { + type: cmd.type, + x: cmd.params[0], + y: cmd.params[1] + }; + } else if (cmd.type === 'C') { + return { + type: cmd.type, + cx1: cmd.params[0], + cy1: cmd.params[1], + cx2: cmd.params[2], + cy2: cmd.params[3], + x: cmd.params[4], + y: cmd.params[5] + }; + } + return { type: cmd.type, x: 0, y: 0 }; + }; + + const startCmds: CommandValues[] = startCommands.map(convertCommand); + const endCmds: CommandValues[] = endCommands.map(convertCommand); + + // Find baseline and closing segments + const findBaselineSegments: (cmds: CommandValues[]) => { + baselineY: number; + dataPoints: CommandValues[]; + closingPoints: CommandValues[]; + } = (cmds: CommandValues[]) => { + // First command is the baseline starting point + const baselineStart: CommandValues = cmds[0]; + + // Find where the path returns to baseline + // Fix: Remove the unused closingIndex variable to fix the linter error + let lastDataPointIndex: number = cmds.length - 1; + + // Work backwards to find where the path drops back to baseline + for (let i: number = cmds.length - 2; i > 1; i--) { + const cmd: CommandValues = cmds[i as number]; + const nextCmd: CommandValues = cmds[i + 1]; + + // The drop to baseline is identified by a sharp vertical line + if (nextCmd.type === 'L' && cmd.y !== nextCmd.y && + Math.abs(nextCmd.y - baselineStart.y) < 2 && + Math.abs(cmd.x - nextCmd.x) < 2) { + lastDataPointIndex = i; + break; + } + } + + return { + baselineY: baselineStart.y, + dataPoints: cmds.slice(1, lastDataPointIndex + 1), + closingPoints: cmds.slice(lastDataPointIndex + 1) + }; + }; + + // Analyze both paths + const startPath: { + baselineY: number; dataPoints: CommandValues[]; closingPoints: CommandValues[]; + } = findBaselineSegments(startCmds); + const endPath: { + baselineY: number; dataPoints: CommandValues[]; closingPoints: CommandValues[]; + } = findBaselineSegments(endCmds); + + // Detect if points were added or removed + const pointAdded: boolean = endPath.dataPoints.length > startPath.dataPoints.length; + const pointRemoved: boolean = startPath.dataPoints.length > endPath.dataPoints.length; + + // Start building result path + let result: string = ''; + + // Baseline start (M command) + const startM: CommandValues = startCmds[0]; + const endM: CommandValues = endCmds[0]; + result += `M ${startM.x + (endM.x - startM.x) * progress} ${startM.y + (endM.y - startM.y) * progress} `; + + // First data point (L command) + if (startPath.dataPoints.length > 0 && endPath.dataPoints.length > 0) { + const startFirstPoint: CommandValues = startPath.dataPoints[0]; + const endFirstPoint: CommandValues = endPath.dataPoints[0]; + + result += `L ${startFirstPoint.x + (endFirstPoint.x - startFirstPoint.x) * progress} ${startFirstPoint.y + (endFirstPoint.y - startFirstPoint.y) * progress} `; + } + + // Special handling for adding or removing points + if (pointAdded) { + // Handle existing points + for (let i: number = 1; i < startPath.dataPoints.length; i++) { + const startPoint: CommandValues = startPath.dataPoints[i as number]; + const endPoint: CommandValues = endPath.dataPoints[i as number]; + + if (startPoint.type === 'C' && endPoint && endPoint.type === 'C') { + result += `C ${(startPoint.cx1 as number) + ((endPoint.cx1 as number) - (startPoint.cx1 as number)) * progress} + ${startPoint.cy1 as number + (endPoint.cy1 as number - (startPoint.cy1 as number)) * progress} + ${startPoint.cx2 as number + (endPoint.cx2 as number - (startPoint.cx2 as number)) * progress} + ${startPoint.cy2 as number + (endPoint.cy2 as number - (startPoint.cy2 as number)) * progress} + ${startPoint.x + (endPoint.x - startPoint.x) * progress} + ${startPoint.y + (endPoint.y - startPoint.y) * progress} `; + } + } + + // Animate new points growing from their nearest neighbor + const lastExistingPoint: CommandValues = startPath.dataPoints[startPath.dataPoints.length - 1]; + + for (let i: number = startPath.dataPoints.length; i < endPath.dataPoints.length; i++) { + const newPoint: CommandValues = endPath.dataPoints[i as number]; + + if (newPoint.type === 'C') { + // Animate from the last existing point + const sourceX: number = lastExistingPoint.x; + const sourceY: number = lastExistingPoint.y; + + // Grow control points from source + result += `C ${sourceX + ((newPoint.cx1 as number) - sourceX) * progress} + ${sourceY + (newPoint.cy1 as number - sourceY) * progress} + ${sourceX + (newPoint.cx2 as number - sourceX) * progress} + ${sourceY + (newPoint.cy2 as number - sourceY) * progress} + ${sourceX + (newPoint.x - sourceX) * progress} + ${sourceY + (newPoint.y - sourceY) * progress} `; + } + } + } + else if (pointRemoved) { + // Handle common points + const minPoints: number = Math.min(startPath.dataPoints.length, endPath.dataPoints.length); + + for (let i: number = 1; i < minPoints; i++) { + const startPoint: CommandValues = startPath.dataPoints[i as number]; + const endPoint: CommandValues = endPath.dataPoints[i as number]; + + if (startPoint.type === 'C' && endPoint.type === 'C') { + result += `C ${startPoint.cx1 as number + (endPoint.cx1 as number - (startPoint.cx1 as number)) * progress} + ${startPoint.cy1 as number + (endPoint.cy1 as number - (startPoint.cy1 as number)) * progress} + ${startPoint.cx2 as number + (endPoint.cx2 as number - (startPoint.cx2 as number)) * progress} + ${startPoint.cy2 as number + (endPoint.cy2 as number - (startPoint.cy2 as number)) * progress} + ${startPoint.x + (endPoint.x - startPoint.x) * progress} + ${startPoint.y + (endPoint.y - startPoint.y) * progress} `; + } + } + + // Fix: Improve point removal animation by changing the fade calculation + // Only add disappearing points when progress is not complete + if (progress < 0.99) { // Use 0.99 instead of 1 to avoid visual glitches at the end + // Animate disappearing points shrinking toward a neighbor + for (let i: number = minPoints; i < startPath.dataPoints.length; i++) { + const removedPoint: CommandValues = startPath.dataPoints[i as number]; + + // Target is the last common point + const targetPoint: CommandValues = endPath.dataPoints[endPath.dataPoints.length - 1]; + + if (removedPoint.type === 'C') { + // Calculate inverse progress for the shrinking effect + const fadeOutProgress: number = 1 - progress; + + // Shrink control points toward target + result += `C ${targetPoint.x + (removedPoint.cx1 as number - targetPoint.x) * fadeOutProgress} + ${targetPoint.y + (removedPoint.cy1 as number - targetPoint.y) * fadeOutProgress} + ${targetPoint.x + (removedPoint.cx2 as number - targetPoint.x) * fadeOutProgress} + ${targetPoint.y + (removedPoint.cy2 as number - targetPoint.y) * fadeOutProgress} + ${targetPoint.x + (removedPoint.x - targetPoint.x) * fadeOutProgress} + ${targetPoint.y + (removedPoint.y - targetPoint.y) * fadeOutProgress} `; + } + } + } + } + else { + // Normal path interpolation - handle all data points + for (let i: number = 1; i < Math.min(startPath.dataPoints.length, endPath.dataPoints.length); i++) { + const startPoint: CommandValues = startPath.dataPoints[i as number]; + const endPoint: CommandValues = endPath.dataPoints[i as number]; + + if (startPoint.type === 'C' && endPoint.type === 'C') { + result += `C ${startPoint.cx1 as number + (endPoint.cx1 as number - (startPoint.cx1 as number)) * progress} + ${(startPoint.cy1 as number) + (endPoint.cy1 as number - (startPoint.cy1 as number)) * progress} + ${(startPoint.cx2 as number) + (endPoint.cx2 as number - (startPoint.cx2 as number)) * progress} + ${(startPoint.cy2 as number) + (endPoint.cy2 as number - (startPoint.cy2 as number)) * progress} + ${startPoint.x + (endPoint.x - startPoint.x) * progress} + ${startPoint.y + (endPoint.y - startPoint.y) * progress} `; + } + } + } + + // Get the final data point and calculate the transition to baseline + let lastPoint: { x: number, y: number }; + + if (pointAdded) { + // For point addition, animate from the previous last point to the new last point + const oldLastPoint: CommandValues = startPath.dataPoints[startPath.dataPoints.length - 1]; + const newLastPoint: CommandValues = endPath.dataPoints[endPath.dataPoints.length - 1]; + + // Interpolate between the old last point and new last point + lastPoint = { + x: oldLastPoint.x + (newLastPoint.x - oldLastPoint.x) * progress, + y: oldLastPoint.y + (newLastPoint.y - oldLastPoint.y) * progress + }; + } + else if (pointRemoved) { + // For point removal, animate from the old last point to the new last point + const oldLastPoint: CommandValues = startPath.dataPoints[startPath.dataPoints.length - 1]; + const newLastPoint: CommandValues = endPath.dataPoints[endPath.dataPoints.length - 1]; + + // Fix: Calculate last point position better for removal animation + const fadeOutProgress: number = 1 - progress; + lastPoint = { + x: newLastPoint.x + (oldLastPoint.x - newLastPoint.x) * fadeOutProgress, + y: newLastPoint.y + (oldLastPoint.y - newLastPoint.y) * fadeOutProgress + }; + } + else { + // Normal case - just interpolate the last points + const oldLastPoint: CommandValues = startPath.dataPoints[startPath.dataPoints.length - 1]; + const newLastPoint: CommandValues = endPath.dataPoints[endPath.dataPoints.length - 1]; + + lastPoint = { + x: oldLastPoint.x + (newLastPoint.x - oldLastPoint.x) * progress, + y: oldLastPoint.y + (newLastPoint.y - oldLastPoint.y) * progress + }; + } + + // Interpolate baseline Y value + const baselineY: number = startPath.baselineY + (endPath.baselineY - startPath.baselineY) * progress; + + // Add vertical line to baseline + result += `L ${lastPoint.x} ${baselineY} `; + + // Add horizontal line back to start and close + result += `L ${startM.x + (endM.x - startM.x) * progress} ${baselineY} Z`; + + return result; + } + catch (e) { + // Fallback to standard interpolation on error + return interpolatePathD(startD, endD, progress); + } +} + + +/** + * Interpolates between two SVG path `d` attribute strings based on the given progress. + * This function is typically used for animating transitions between two spline border paths. + * + * @param {string} startD - The starting SVG path `d` string. + * @param {string} endD - The ending SVG path `d` string. + * @param {number} progress - A number between 0 and 1 indicating the interpolation progress. + * @returns {string} - interploted path. + * @private + */ +export function interpolateSplineBorderPath(startD: string, endD: string, progress: number): string { + if (!startD || !endD) { + return endD || startD || ''; + } + + try { + // Parse the path data + const startCommands: PathCommand[] = parsePathCommands(startD); + const endCommands: PathCommand[] = parsePathCommands(endD); + + // Convert to a more usable format + const convertCommand: (cmd: PathCommand) => CommandValues = (cmd: PathCommand): CommandValues => { + if (cmd.type === 'M' || cmd.type === 'L') { + return { + type: cmd.type, + x: cmd.params[0], + y: cmd.params[1] + }; + } else if (cmd.type === 'C') { + return { + type: cmd.type, + cx1: cmd.params[0], + cy1: cmd.params[1], + cx2: cmd.params[2], + cy2: cmd.params[3], + x: cmd.params[4], + y: cmd.params[5] + }; + } + return { type: cmd.type, x: 0, y: 0 }; + }; + + const startCmds: CommandValues[] = startCommands.map(convertCommand); + const endCmds: CommandValues[] = endCommands.map(convertCommand); + + // For border path, just count all data points (borders don't have closing sections) + const startDataPoints: CommandValues[] = startCmds; + const endDataPoints: CommandValues[] = endCmds; + + // Detect point addition or removal + const pointAdded: boolean = endDataPoints.length > startDataPoints.length; + const pointRemoved: boolean = startDataPoints.length > endDataPoints.length; + + // Build the result path + let result: string = ''; + + // First point (M command) + if (startDataPoints.length > 0 && endDataPoints.length > 0) { + const startFirstPoint: CommandValues = startDataPoints[0]; + const endFirstPoint: CommandValues = endDataPoints[0]; + + result += `M ${startFirstPoint.x + (endFirstPoint.x - startFirstPoint.x) * progress} ${startFirstPoint.y + (endFirstPoint.y - startFirstPoint.y) * progress} `; + } + + // Handle point addition + if (pointAdded) { + // Handle common points + for (let i: number = 1; i < startDataPoints.length; i++) { + const startPoint: CommandValues = startDataPoints[i as number]; + const endPoint: CommandValues = endDataPoints[i as number]; + + if (startPoint.type === 'C' && endPoint && endPoint.type === 'C') { + result += `C ${(startPoint.cx1 as number) + ((endPoint.cx1 as number) - (startPoint.cx1 as number)) * progress} ${(startPoint.cy1 as number) + + ((endPoint.cy1 as number) - (startPoint.cy1 as number)) * progress} ${(startPoint.cx2 as number) + ((endPoint.cx2 as number) - (startPoint.cx2 as number)) * progress} + ${(startPoint.cy2 as number) + ((endPoint.cy2 as number) - (startPoint.cy2 as number)) * progress} ${startPoint.x + (endPoint.x - startPoint.x) * progress} ${startPoint.y + (endPoint.y - startPoint.y) * progress} `; + } + } + + // Animate new points growing from their nearest neighbor + const lastExistingPoint: CommandValues = startDataPoints[startDataPoints.length - 1]; + + for (let i: number = startDataPoints.length; i < endDataPoints.length; i++) { + const newPoint: CommandValues = endDataPoints[i as number]; + + if (newPoint.type === 'C') { + // Animate from the last existing point + const sourceX: number = lastExistingPoint.x; + const sourceY: number = lastExistingPoint.y; + + // Grow control points from source + result += `C ${sourceX + ((newPoint.cx1 as number) - sourceX) * progress} ${sourceY + + ((newPoint.cy1 as number) - sourceY) * progress} ${sourceX + ((newPoint.cx2 as number) - sourceX) * progress} ${sourceY + ((newPoint.cy2 as number) - sourceY) * progress} ${sourceX + (newPoint.x - sourceX) * progress} ${sourceY + (newPoint.y - sourceY) * progress} `; + } + } + } + else if (pointRemoved) { + // Handle common points + const minPoints: number = Math.min(startDataPoints.length, endDataPoints.length); + + for (let i: number = 1; i < minPoints; i++) { + const startPoint: CommandValues = startDataPoints[i as number]; + const endPoint: CommandValues = endDataPoints[i as number]; + + if (startPoint.type === 'C' && endPoint && endPoint.type === 'C') { + result += `C ${(startPoint.cx1 as number) + ((endPoint.cx1 as number) - (startPoint.cx1 as number)) * progress} ${(startPoint.cy1 as number) + + ((endPoint.cy1 as number) - (startPoint.cy1 as number)) * progress} ${(startPoint.cx2 as number) + + ((endPoint.cx2 as number) - (startPoint.cx2 as number)) * progress} ${(startPoint.cy2 as number) + + ((endPoint.cy2 as number) - (startPoint.cy2 as number)) * progress} ${startPoint.x + + (endPoint.x - startPoint.x) * progress} ${startPoint.y + (endPoint.y - startPoint.y) * progress} `; + } + } + + // Only add disappearing points when progress is not complete + if (progress < 0.99) { // Use 0.99 instead of 1 to avoid visual glitches + // Animate disappearing points shrinking toward a neighbor + const targetPoint: CommandValues = endDataPoints[endDataPoints.length - 1]; + + for (let i: number = minPoints; i < startDataPoints.length; i++) { + const removedPoint: CommandValues = startDataPoints[i as number]; + + if (removedPoint.type === 'C') { + // Calculate fade out effect + const fadeOutProgress: number = 1 - progress; + + // Shrink control points toward target point + result += `C ${targetPoint.x + ((removedPoint.cx1 as number) - targetPoint.x) * fadeOutProgress} ${targetPoint.y + + ((removedPoint.cy1 as number) - targetPoint.y) * fadeOutProgress} ${targetPoint.x + + ((removedPoint.cx2 as number) - targetPoint.x) * fadeOutProgress} ${targetPoint.y + + ((removedPoint.cy2 as number) - targetPoint.y) * fadeOutProgress} ${targetPoint.x + (removedPoint.x - targetPoint.x) * fadeOutProgress} ${targetPoint.y + (removedPoint.y - targetPoint.y) * fadeOutProgress} `; + } + } + } + } + else { + // Normal interpolation + for (let i: number = 1; i < Math.min(startDataPoints.length, endDataPoints.length); i++) { + const startPoint: CommandValues = startDataPoints[i as number]; + const endPoint: CommandValues = endDataPoints[i as number]; + + if (startPoint.type === 'C' && endPoint.type === 'C') { + result += `C ${(startPoint.cx1 as number) + ((endPoint.cx1 as number) - (startPoint.cx1 as number)) * progress} ${(startPoint.cy1 as number) + + ((endPoint.cy1 as number) - (startPoint.cy1 as number)) * progress} ${(startPoint.cx2 as number) + + ((endPoint.cx2 as number) - (startPoint.cx2 as number)) * progress} ${(startPoint.cy2 as number) + ((endPoint.cy2 as number) - (startPoint.cy2 as number)) * progress} + ${startPoint.x + (endPoint.x - startPoint.x) * progress} ${startPoint.y + (endPoint.y - startPoint.y) * progress} `; + } + } + } + + return result.trim(); + } + catch (e) { + return interpolatePathD(startD, endD, progress); + } +} diff --git a/components/charts/src/chart/renderer/SeriesRenderer/SplineSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/SplineSeriesRenderer.tsx new file mode 100644 index 0000000..8c94314 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/SplineSeriesRenderer.tsx @@ -0,0 +1,618 @@ +import { ChartMarkerProps, ChartLocationProps } from '../../base/interfaces'; +import { PathCommand } from '../../common/base'; +import { getPoint, withInRange } from '../../utils/helper'; +import { LineBase, LineBaseReturnType } from './LineBase'; +import { calculatePathAnimation, interpolate, parsePathCommands } from './SeriesAnimation'; +import MarkerRenderer from './MarkerRenderer'; +import { AxisModel, ControlPoints, Points, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { IntervalType } from '../../base/enum'; + +const lineBaseInstance: LineBaseReturnType = LineBase; + +/** + * SplineSeriesInterface defines the structure for rendering spline series charts. + * This interface includes methods and properties for calculating control points, + * rendering spline curves, and handling animations for spline series in charts. + * + * @interface SplineSeriesInterface + */ +interface SplineSeriesInterface { + /** Previous X coordinate used in calculations */ + previousX: number; + /** Previous Y coordinate used in calculations */ + previousY: number; + + /** + * Generates the SVG path direction string for a spline curve between two points + * + * @param {ChartLocationProps} controlPoint1 - First bezier control point + * @param {ChartLocationProps} controlPoint2 - Second bezier control point + * @param {Points} firstPoint - Starting point of the spline segment + * @param {Points} secondPoint - Ending point of the spline segment + * @param {SeriesProperties} series - Series configuration object + * @param {boolean} isInverted - Whether axes are inverted + * @param {Function} getPointLocation - Function to translate data points to screen coordinates + * @param {string} startPoint - Starting SVG path command + * @returns {string} - SVG path command string for the spline segment + */ + getSplineDirection: ( + controlPoint1: ChartLocationProps, + controlPoint2: ChartLocationProps, + firstPoint: Points, + secondPoint: Points, + series: SeriesProperties, + isInverted: boolean, + getPointLocation: ( + x: number, + y: number, + xAxis: AxisModel, + yAxis: AxisModel, + isInverted?: boolean, + series?: SeriesProperties + ) => ChartLocationProps, + startPoint: string + ) => string; + + /** + * Calculates spline coefficients based on the specified spline type + * + * @param {Points[]} points - Array of data points + * @param {SeriesProperties} [series] - Optional series configuration + * @returns {number[]} - Array of calculated spline coefficients + */ + findSplineCoefficients: ( + points: Points[], + series?: SeriesProperties + ) => number[]; + + /** + * Calculates natural spline coefficients for smooth curve rendering + * + * @param {Points[]} points - Array of data points + * @returns {number[]} - Array of calculated natural spline coefficients + */ + naturalSplineCoefficients: ( + points: Points[] + ) => number[]; + + /** + * Calculates cardinal spline coefficients with tension control + * + * @param {Points[]} points - Array of data points + * @param {SeriesProperties} series - Series configuration with tension values + * @returns {number[]} - Array of calculated cardinal spline coefficients + */ + cardinalSplineCofficients: ( + points: Points[], + series: SeriesProperties + ) => number[]; + + /** + * Calculates monotonic spline coefficients to ensure monotonic curves + * + * @param {Points[]} points - Array of data points + * @returns {number[]} - Array of calculated monotonic spline coefficients + */ + monotonicSplineCoefficients: ( + points: Points[] + ) => number[]; + + /** + * Calculates clamped spline coefficients with specific boundary conditions + * + * @param {Points[]} points - Array of data points + * @returns {number[]} - Array of calculated clamped spline coefficients + */ + clampedSplineCofficients: ( + points: Points[] + ) => number[]; + + /** + * Generates control points for bezier curves based on spline coefficients + * + * @param {Points} point1 - First data point + * @param {Points} point2 - Second data point + * @param {number} ySpline1 - Spline coefficient for first point + * @param {number} ySpline2 - Spline coefficient for second point + * @param {SeriesProperties} series - Series configuration object + * @returns {ControlPoints} - Control points for bezier curve rendering + */ + getControlPoints: ( + point1: Points, + point2: Points, + ySpline1: number, + ySpline2: number, + series: SeriesProperties + ) => ControlPoints; + + /** + * Renders the spline series + * + * @param {SeriesProperties} series - Series configuration object + * @param {boolean} isInverted - Whether axes are inverted + * @param {Object} chartProps - Chart properties including event handlers + * @returns {RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }} - Render options for path and markers + */ + render: ( + series: SeriesProperties, + isInverted: boolean + ) => RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }; + + /** + * Handles animation for spline series paths + * + * @param {RenderOptions} pathOptions - Path rendering options + * @param {number} index - Series index + * @param {Object} animationState - Animation state object with refs + * @param {boolean} enableAnimation - Whether animation is enabled + * @param {SeriesProperties} _currentSeries - Current series being animated + * @param {Points | undefined} _currentPoint - Current point being animated + * @param {number} _pointIndex - Index of the current point + * @param {SeriesProperties[]} [visibleSeries] - Array of visible series + * @returns {void} - This method doesn't return anything + */ + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + _currentSeries: SeriesProperties, + _currentPoint: Points | undefined, + _pointIndex: number, + visibleSeries?: SeriesProperties[] + ) => void + + /** + * Calculates the appropriate date-time interval in milliseconds based on axis settings + * + * @param {SeriesProperties} series - Series configuration object with axis information + * @returns {number} - Interval in milliseconds + */ + dateTimeInterval?: ( + series: SeriesProperties + ) => number; +} + +const SplineSeriesRenderer: SplineSeriesInterface = { + previousX: 0, + previousY: 0, + + getSplineDirection: ( + controlPoint1: ChartLocationProps, + controlPoint2: ChartLocationProps, + firstPoint: Points, + secondPoint: Points, + series: SeriesProperties, + isInverted: boolean, + getPointLocation: Function, + startPoint: string + ): string => { + let direction: string = ''; + if (firstPoint != null) { + const pt1: ChartLocationProps = getPointLocation( + firstPoint.xValue, firstPoint.yValue, series.xAxis, series.yAxis, isInverted, series + ); + const pt2: ChartLocationProps = getPointLocation( + secondPoint.xValue, secondPoint.yValue, series.xAxis, series.yAxis, isInverted, series + ); + + // Get bezier control points + const bpt1: ChartLocationProps = getPointLocation( + controlPoint1.x, controlPoint1.y, series.xAxis, series.yAxis, isInverted, series + ); + const bpt2: ChartLocationProps = getPointLocation( + controlPoint2.x, controlPoint2.y, series.xAxis, series.yAxis, isInverted, series + ); + + // Create the cubic bezier curve command + direction = startPoint + ' ' + (pt1.x) + ' ' + (pt1.y) + ' ' + + 'C' + ' ' + (bpt1.x) + ' ' + (bpt1.y) + ' ' + + (bpt2.x) + ' ' + (bpt2.y) + ' ' + + (pt2.x) + ' ' + (pt2.y) + ' '; + } + return direction; + }, + + dateTimeInterval: (series: SeriesProperties): number => { + if (!series?.xAxis || !series.xAxis?.actualIntervalType) { + return 30 * 24 * 60 * 60 * 1000; // Default to month + } + + const interval: IntervalType = series?.xAxis?.actualIntervalType as IntervalType; + let intervalInMilliseconds: number; + + if (interval === 'Years') { + intervalInMilliseconds = 365 * 24 * 60 * 60 * 1000; + } else if (interval === 'Months') { + intervalInMilliseconds = 30 * 24 * 60 * 60 * 1000; + } else if (interval === 'Days') { + intervalInMilliseconds = 24 * 60 * 60 * 1000; + } else if (interval === 'Hours') { + intervalInMilliseconds = 60 * 60 * 1000; + } else if (interval === 'Minutes') { + intervalInMilliseconds = 60 * 1000; + } else if (interval === 'Seconds') { + intervalInMilliseconds = 1000; + } else { + intervalInMilliseconds = 30 * 24 * 60 * 60 * 1000; + } + return intervalInMilliseconds; + }, + + findSplineCoefficients: (points: Points[], series?: SeriesProperties): number[] => { + + let ySpline: number[] = []; + const ySplineDuplicate: number[] = []; + let cardinalSplineTension: number = series?.cardinalSplineTension ? series.cardinalSplineTension : 0.5; + if (cardinalSplineTension < 0) { + cardinalSplineTension = 0; + } + else if (cardinalSplineTension > 1) { + cardinalSplineTension = 1; + } + switch (series?.splineType) { + case 'Monotonic': + ySpline = SplineSeriesRenderer.monotonicSplineCoefficients(points); + break; + case 'Cardinal': + ySpline = SplineSeriesRenderer.cardinalSplineCofficients(points, series); + break; + default: + if (series?.splineType === 'Clamped') { + ySpline = SplineSeriesRenderer.clampedSplineCofficients(points); + } else { + // assigning the first and last value as zero + ySpline[0] = ySplineDuplicate[0] = 0; + ySpline[points.length - 1] = 0; + } + ySpline = SplineSeriesRenderer.naturalSplineCoefficients(points); + break; + } + return ySpline; + }, + + naturalSplineCoefficients: (points: Points[]): number[] => { + const count: number = points.length; + + const ySpline: number[] = []; + const ySplineDuplicate: number[] = []; + + // Setting first and last values as 0 + ySpline[0] = ySplineDuplicate[0] = 0; + ySpline[points.length - 1] = 0; + + // Natural spline algorithm + for (let i: number = 1; i < count - 1; i++) { + // Use nullish coalescing operator to provide default values if xValue is null + const coefficient1: number = Number(points[i as number].xValue) - Number(points[i - 1].xValue); + const coefficient2: number = ((points[i + 1]?.xValue) as number) - (points[i - 1]?.xValue as Required) as number; + const coefficient3: number = ((points[i + 1]?.xValue) as number) - (points[i as number]?.xValue as Required) as number; + + const dy1: number = (points[i + 1]?.yValue) as number - (points[i as number]?.yValue as Required) as number; + const dy2: number = (points[i as number]?.yValue) as number - (points[i - 1]?.yValue as Required) as number; + if (coefficient1 === 0 || coefficient2 === 0 || coefficient3 === 0) { + ySpline[i as number] = 0; + ySplineDuplicate[i as number] = 0; + } else { + const p: number = 1 / Math.max(Number.EPSILON, Math.abs(coefficient1 * ySpline[i - 1] + 2 * coefficient2)); + ySpline[i as number] = -p * coefficient3; + ySplineDuplicate[i as number] = p * (6 * (dy1 / coefficient3 - dy2 / coefficient1) - + coefficient1 * ySplineDuplicate[i - 1]); + } + } + + // Back substitution for tridiagonal algorithm + for (let k: number = count - 2; k >= 0; k--) { + ySpline[k as number] = ySpline[k as number] * ySpline[k + 1] + ySplineDuplicate[k as number]; + } + + return ySpline; + }, + + cardinalSplineCofficients: (points: Points[], series: SeriesProperties): number[] => { + const count: number = points.length; + const ySpline: number[] = []; + let cardinalSplineTension: number = series.cardinalSplineTension ? series.cardinalSplineTension : 0.5; + cardinalSplineTension = cardinalSplineTension < 0 ? 0 : cardinalSplineTension > 1 ? 1 : cardinalSplineTension; + for (let i: number = 0; i < count; i++) { + if (i === 0) { + ySpline[i as number] = (count > 2) ? + (cardinalSplineTension * ((points[i + 2].xValue as number) - (points[i as number].xValue as number))) : 0; + } else if (i === (count - 1)) { + ySpline[i as number] = (count > 2) ? + (cardinalSplineTension * ((points[count - 1].xValue as number) - (points[count - 3].xValue as number))) : 0; + } else { + ySpline[i as number] = (cardinalSplineTension * ((points[i + 1].xValue as number) - (points[i - 1].xValue as number))); + } + } + return ySpline; + }, + + monotonicSplineCoefficients: (points: Points[]): number[] => { + const count: number = points.length; + const ySpline: number[] = []; + const dx: number[] = []; + // const dy: number[] = []; + const slope: number[] = []; + let interPoint: number; + //interpolant points + const slopeLength: number = slope.length; + // to find the first and last co-efficient value + ySpline[0] = slope[0]; + ySpline[count - 1] = slope[slopeLength - 1]; + //to find the other co-efficient values + for (let j: number = 0; j < dx.length; j++) { + if (slopeLength > j + 1) { + if (slope[j as number] * slope[j + 1] <= 0) { + ySpline[j + 1] = 0; + } else { + interPoint = dx[j as number] + dx[j + 1]; + ySpline[j + 1] = 3 * interPoint / ((interPoint + dx[j + 1]) / slope[j as number] + + (interPoint + dx[j as number]) / slope[j + 1]); + } + } + } + + return ySpline; + }, + + clampedSplineCofficients: (points: Points[]): number[] => { + const count: number = points.length; + const ySpline: number[] = []; + const ySplineDuplicate: number[] = []; + for (let i: number = 0; i < count - 1; i++) { + ySpline[0] = (3 * ((points[1].yValue as number) - (points[0].yValue as number))) / + ((points[1].xValue as number) - (points[0].xValue as number)) - 3; + ySplineDuplicate[0] = 0.5; + ySpline[points.length - 1] = (3 * ((points[points.length - 1].yValue as number) - (points[points.length - 2].yValue as number))) + / ((points[points.length - 1].xValue as number) - (points[points.length - 2].xValue as number)); + ySpline[0] = ySplineDuplicate[0] = Number.isFinite(Math.abs(ySpline[0])) ? Math.abs(ySpline[0]) : 0; + ySpline[points.length - 1] = ySplineDuplicate[points.length - 1] = Number.isFinite(Math.abs(ySpline[points.length - 1])) ? + ySpline[points.length - 1] : 0; + } + + return ySpline; + }, + + getControlPoints: (point1: Points, point2: Points, ySpline1: number, ySpline2: number, series: SeriesProperties): ControlPoints => { + let controlPoint1: ChartLocationProps; + let controlPoint2: ChartLocationProps; + let point: ControlPoints; + let ySplineDuplicate1: number = ySpline1; + let ySplineDuplicate2: number = ySpline2; + const xValue1: number = point1.xValue as number; + const yValue1: number = point1.yValue as number; + const xValue2: number = point2.xValue as number; + const yValue2: number = point2.yValue as number; + switch (series.splineType) { + case 'Cardinal': + if (series.xAxis.valueType === 'DateTime') { + ySplineDuplicate1 = ySpline1 / (SplineSeriesRenderer.dateTimeInterval as Function)(series); + ySplineDuplicate2 = ySpline2 / (SplineSeriesRenderer.dateTimeInterval as Function)(series); + } + controlPoint1 = { x: xValue1 + ySpline1 / 3, y: yValue1 + ySplineDuplicate1 / 3 }; + controlPoint2 = { x: xValue2 - ySpline2 / 3, y: yValue2 - ySplineDuplicate2 / 3 }; + point = { controlPoint1: controlPoint1, controlPoint2: controlPoint2 }; + break; + case 'Monotonic': { + const value: number = (xValue2 - xValue1) / 3; + controlPoint1 = { x: xValue1 + value, y: yValue1 + ySpline1 * value }; + controlPoint2 = { x: xValue2 - value, y: yValue2 - ySpline2 * value }; + point = { controlPoint1: controlPoint1, controlPoint2: controlPoint2 }; + break; + } + default: { + const one3: number = 1 / 3.0; + let deltaX2: number = (xValue2 - xValue1); + deltaX2 = deltaX2 * deltaX2; + const y1: number = one3 * (((2 * yValue1) + yValue2) - one3 * deltaX2 * (ySpline1 + 0.5 * ySpline2)); + const y2: number = one3 * ((yValue1 + (2 * yValue2)) - one3 * deltaX2 * (0.5 * ySpline1 + ySpline2)); + controlPoint1 = { x: (2 * xValue1 + xValue2) * one3, y: y1 }; + controlPoint2 = { x: (xValue1 + 2 * xValue2) * one3, y: y2 }; + point = { controlPoint1: controlPoint1, controlPoint2: controlPoint2 }; + break; + } + } + return point; + }, + + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + _currentSeries: SeriesProperties, + _currentPoint: Points | undefined, + _pointIndex: number, + visibleSeries? : SeriesProperties[] + ) => { + return calculatePathAnimation(pathOptions, index, animationState, enableAnimation, visibleSeries); + }, + + render: (series: SeriesProperties, isInverted: boolean ): + RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps } => { + if (!series || series === null || !series.points || series.points === null || series.points.length < 1) { + return []; + } + + const getCoordinate: + (x: number, y: number, xAxis: AxisModel, yAxis: AxisModel, isInverted?: boolean, series?: SeriesProperties) => + ChartLocationProps = getPoint; + const visiblePoints: Points[] = lineBaseInstance.enableComplexProperty(series); + const points: Points[] = []; + let pointIndex: number = 0; + + for (let i: number = 0; i < visiblePoints.length; i++) { + const point: Points = visiblePoints[i as number]; + if (point.xValue === null) {continue; } + point.index = pointIndex++; + points.push(point); + } + + series.visiblePoints = visiblePoints; + + const splineCoefficients: number[] = SplineSeriesRenderer.findSplineCoefficients(points, series); + if (!series.drawPoints) {series.drawPoints = []; } + + const pointsLength: number = points.length; + if (!series.drawPoints || series.drawPoints.length < pointsLength - 1) { + series.drawPoints = new Array(pointsLength - 1); + } + for (let i: number = 1; i < pointsLength; i++) { + const currentPoint: Points = points[i as number]; + const previousPoint: Points = points[i - 1]; + const currentCoefficient: number = splineCoefficients[i as number] || 0; + const previousCoefficient: number = splineCoefficients[i - 1] || 0; + + series.drawPoints[i - 1] = SplineSeriesRenderer.getControlPoints( + previousPoint, + currentPoint, + previousCoefficient, + currentCoefficient, + series + ); + } + + let direction: string = ''; + let firstPoint: Points | null = null; + // let prevIndex = 0; + const isDropMode: boolean = series.emptyPointSettings?.mode === 'Drop'; + + for (let i: number = 0; i < points.length; i++) { + const point: Points = points[i as number]; + point.symbolLocations = []; + point.regions = []; + + const previous: number = i > 0 ? i - 1 : 0; + const next: number = i < points.length - 1 ? i + 1 : i; + + if ( + point.visible && + (i === 0 || withInRange(points[previous as number], point, points[next as number], series)) + ) { + if (firstPoint === null) { + const pt: ChartLocationProps = + getCoordinate((point.xValue as number), (point.yValue as number), series.xAxis, series.yAxis, isInverted, series); + direction += `M ${pt.x} ${pt.y} `; + } else { + let controlPt1: ChartLocationProps; let controlPt2: ChartLocationProps; + if (isDropMode && previous < i && !points[previous as number].visible) { + controlPt1 = series.drawPoints[previous - 1]?.controlPoint1 ?? firstPoint; + controlPt2 = series.drawPoints[previous - 1]?.controlPoint2 ?? firstPoint; + } else { + controlPt1 = series.drawPoints[previous as number]?.controlPoint1; + controlPt2 = series.drawPoints[previous as number]?.controlPoint2; + } + + const pt2: ChartLocationProps = getCoordinate( + (point.xValue as number), (point.yValue as number), series.xAxis, series.yAxis, isInverted, series); + const bpt1: ChartLocationProps = getCoordinate( + controlPt1.x, controlPt1.y, series.xAxis, series.yAxis, isInverted, series); + const bpt2: ChartLocationProps = getCoordinate( + controlPt2.x, controlPt2.y, series.xAxis, series.yAxis, isInverted, series); + + direction += `C ${bpt1.x} ${bpt1.y} ${bpt2.x} ${bpt2.y} ${pt2.x} ${pt2.y} `; + } + + lineBaseInstance.storePointLocation(point, series, isInverted, getCoordinate); + firstPoint = point; + } else { + if (!isDropMode && firstPoint) { + firstPoint = null; + } + } + } + + const name: string = series.chart.element.id + '_Series_' + series.index; + const options: RenderOptions[] = [{ + id: name, + fill: 'none', + strokeWidth: series.width, + stroke: series.interior, + opacity: series.opacity, + dashArray: series.dashArray, + d: direction + }]; + + const marker: Object | null = series.marker?.visible ? MarkerRenderer.render(series) as Object : null; + return marker ? { options, marker } : options; + } +}; + +/** + * Interpolates spline path data strings (containing cubic bezier curves) + * + * @param {string} fromD - Starting path data + * @param {string} toD - Ending path data + * @param {number} progress - Animation progress (0-1) + * @returns {string} - Interpolated path data + * @private + */ +export function interpolateSplinePathD(fromD: string, toD: string, progress: number): string { + // Parse path commands into structured objects + const fromCommands: PathCommand[] = parsePathCommands(fromD); + const toCommands: PathCommand[] = parsePathCommands(toD); + + // If different number of commands, use simple interpolation between points + if (fromCommands.length !== toCommands.length) { + // Use simple path transitions for paths with different structure + return progress < 0.5 ? fromD : toD; + } + + // Interpolate between matching commands + let result: string = ''; + for (let i: number = 0; i < fromCommands.length; i++) { + const fromCmd: PathCommand = fromCommands[i as number]; + const toCmd: PathCommand = toCommands[i as number]; + + // Skip if commands don't match + if (fromCmd.type !== toCmd.type) { + return progress < 0.5 ? fromD : toD; + } + + result += fromCmd.type + ' '; + + // Interpolate parameters based on command type + switch (fromCmd.type) { + case 'M': // Move to + case 'L': // Line to + for (let j: number = 0; j < fromCmd.params.length; j += 2) { + const x: number = interpolate(fromCmd.params[j as number], toCmd.params[j as number], progress); + const y: number = interpolate(fromCmd.params[j + 1], toCmd.params[j + 1], progress); + result += `${x} ${y} `; + } + break; + + case 'C': // Cubic bezier + // Cubics have 6 params: [x1, y1, x2, y2, x, y] + for (let j: number = 0; j < fromCmd.params.length; j += 2) { + const x: number = interpolate(fromCmd.params[j as number], toCmd.params[j as number], progress); + const y: number = interpolate(fromCmd.params[j + 1], toCmd.params[j + 1], progress); + result += `${x} ${y} `; + } + break; + + default: + // Just copy parameters for unsupported commands + result += fromCmd.params.join(' ') + ' '; + } + } + + return result.trim(); +} + +export default SplineSeriesRenderer; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/StackingBarSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/StackingBarSeriesRenderer.tsx new file mode 100644 index 0000000..bc04f0f --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/StackingBarSeriesRenderer.tsx @@ -0,0 +1,201 @@ +import { ChartMarkerProps } from '../../base/interfaces'; +import { DoubleRangeType, PointRenderingEvent, Points, Rect, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { StackValuesType, useVisiblePoints } from '../../utils/helper'; +import { ColumnBase, ColumnBaseReturnType, StackingBarSeriesRendererType } from './ColumnBase'; +import MarkerRenderer from './MarkerRenderer'; +import { handleRectAnimation } from './SeriesAnimation'; + +const columnBaseInstance: ColumnBaseReturnType = ColumnBase(); + +/** + * Stacking Bar Series Renderer implementation for rendering stacked bar charts. + * Handles the rendering of stacking bar series with proper positioning, animation, and styling. + * + */ +const StackingBarSeriesRenderer: StackingBarSeriesRendererType = { + sideBySideInfo: [] as DoubleRangeType[], + + /** + * Renders the stacking bar series with all its data points. + * Calculates positioning, handles markers, and generates render options for each point. + * + * @param {SeriesProperties} series - The series data containing points, styling, and configuration + * @param {boolean} _isInverted - Flag indicating if the chart is inverted (currently unused) + * @returns {RenderOptions[]|Object} Array of render options or object containing options and marker data + * + */ + render: (series: SeriesProperties, _isInverted: boolean ): + RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps } => { + series.isRectSeries = true; + StackingBarSeriesRenderer.sideBySideInfo[series.index] = columnBaseInstance.getSideBySideInfo(series); + const stackedValue: StackValuesType = series.stackedValues; + const options: RenderOptions[] = []; + + // Optimized loop: Use for...of with index when needed, or use forEach for better performance + series.points.forEach((point: Points) => { + const renderOption: RenderOptions | undefined = StackingBarSeriesRenderer.renderPoint( + series, + point, + StackingBarSeriesRenderer.sideBySideInfo[series.index], + stackedValue + ); + if (renderOption) { + options.push(renderOption); + } + }); + + series.visiblePoints = useVisiblePoints(series); + const marker: ChartMarkerProps | null = series.marker?.visible + ? MarkerRenderer.render(series) as ChartMarkerProps : null; + return marker ? { options, marker } : options; + }, + + /** + * Renders an individual point in the stacking bar series. + * Handles positioning calculations, legend click scenarios, and rectangle drawing. + * + * @param {SeriesProperties} series - The series containing the point to render + * @param {Points} point - The individual data point to render + * @param {DoubleRangeType} sideBySideInfo - Positioning information for multiple series + * @param {StackValuesType} stackedValue - Stacked values containing start and end positions + * @returns {RenderOptions | undefined} Render options for the point or undefined if not visible + * + * @description + * This method handles complex scenarios including: + * - Normal stacking bar rendering + * - Legend click state where series is hidden but needs to show placeholder + * - Side-by-side positioning for multiple series + * - Column width adjustments for both normal and transposed charts + * + */ + renderPoint: (series: SeriesProperties, point: Points, sideBySideInfo: DoubleRangeType, stackedValue: StackValuesType) => { + point.symbolLocations = []; + point.regions = []; + + if (!point.visible) { + return undefined; + } + + let index: number | undefined; + let startvalue: number | undefined = 0; + + if (!series.visible && series.isLegendClicked) { + // Find the last visible series before the current one + for (let index: number = series.index; index >= 0; index--) { + const currentVisibleSeries: SeriesProperties = series.chart.visibleSeries[index as number]; + if (currentVisibleSeries?.visible) { + index = currentVisibleSeries.index; + break; + } + } + + // Replace the existing if-else block with this ternary operator + startvalue = series.index > 0 && index !== undefined + ? series.chart.visibleSeries[index as number]?.stackedValues?.endValues?.[point.index] + : series.stackedValues?.startValues?.[point.index]; + } + + // Calculate rectangle coordinates with null safety + const endValue: number | undefined = (!series.visible && series.isLegendClicked) + ? startvalue + : (stackedValue.endValues?.[point.index] ?? 0); + + const startValueForRect: number | undefined = (!series.visible && series.isLegendClicked) + ? startvalue + : (stackedValue.startValues?.[point.index] ?? 0); + + const rect: Rect = columnBaseInstance.getRectangle( + Number(point.xValue) + sideBySideInfo.start, + Number(endValue), + Number(point.xValue) + sideBySideInfo.end, + Number(startValueForRect), + series + ); + + // Apply column width adjustments based on chart orientation + if (series.chart.iSTransPosed && series.columnWidthInPixel) { + rect.width = series.columnWidthInPixel; + rect.x -= series.columnWidthInPixel / 2; + } else if (series.columnWidthInPixel) { + rect.height = series.columnWidthInPixel; + } + + if (series.columnWidthInPixel) { + rect.y = series.chart.iSTransPosed ? rect.y : rect.y - (series.columnWidthInPixel / 2); + } + + // Trigger point render event + const argsData: PointRenderingEvent = columnBaseInstance.triggerEvent( + series, + point, + series.interior, + { + width: series.border?.width ?? 0, + color: series.border?.color ?? 'transparent' + } + ); + // Update symbol location and draw rectangle + columnBaseInstance.updateSymbolLocation(point, { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height + }, series); + + const name: string = `${series.chart.element.id}_Series_${series.index}_Point_${point.index}`; + const option: RenderOptions = columnBaseInstance.drawRectangle(series, point, rect, argsData, name); + + return option; + }, + + /** + * Handles animation for the stacking bar series rendering. + * Manages rectangle-based animations including direction and transform properties. + * + * @param {RenderOptions} pathOptions - Current rendering options for the path/rectangle + * @param {number} index - Index of the series being animated + * @param {Object} animationState - Current state of the animation system + * @param {boolean} enableAnimation - Flag to enable or disable animation + * @param {SeriesProperties} currentSeries - Current series being animated + * @param {Points | undefined} currentPoint - Current point being animated + * @param {number} pointIndex - Index of the current point + * @returns {Object} Animation properties object containing stroke and transform values + * + */ + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + currentSeries: SeriesProperties, + currentPoint: Points | undefined, + pointIndex: number + ) => { + const animatedvalues: { animatedDirection?: string; animatedTransform?: string; } = handleRectAnimation( + pathOptions, + currentSeries, + index, + currentPoint, + pointIndex, + animationState, + enableAnimation + ); + + return { + strokeDasharray: 'none', + strokeDashoffset: 0, + interpolatedD: undefined, + animatedDirection: animatedvalues.animatedDirection, + animatedTransform: animatedvalues.animatedTransform + }; + } +}; + +export default StackingBarSeriesRenderer; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/StackingColumnSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/StackingColumnSeriesRenderer.tsx new file mode 100644 index 0000000..5ab1af5 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/StackingColumnSeriesRenderer.tsx @@ -0,0 +1,227 @@ +import { ChartMarkerProps } from '../../base/interfaces'; +import { DoubleRangeType, PointRenderingEvent, Points, Rect, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; +import { StackValuesType, useVisiblePoints } from '../../utils/helper'; +import { ColumnBase, ColumnBaseReturnType } from './ColumnBase'; +import MarkerRenderer from './MarkerRenderer'; +import { handleRectAnimation } from './SeriesAnimation'; + +const columnBaseInstance: ColumnBaseReturnType = ColumnBase(); + +interface StackingColumnSeriesRendererType { + sideBySideInfo: DoubleRangeType[]; + render( + series: SeriesProperties, + _isInverted: boolean + ): RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }; + + renderPoint( + series: SeriesProperties, + point: Points, + sideBySideInfo: DoubleRangeType, + stackedValue: StackValuesType + ): RenderOptions | undefined; + + doAnimation( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + currentSeries: SeriesProperties, + currentPoint: Points | undefined, + pointIndex: number + ): { + strokeDasharray: string; + strokeDashoffset: number; + interpolatedD: string | undefined; + animatedDirection: string | undefined; + animatedTransform: string | undefined; + }; +} + +/** + * Creates a StackingColumnSeriesRenderer instance for rendering stacked column series in chart visualization + * + * @description Handles rendering of stacking column series with support for animation, + * side-by-side positioning, and custom styling. Provides methods for rendering individual + * points, handling animations, and managing series-level operations. + * + * @returns {StackingColumnSeriesRendererType} A renderer object with methods for handling stacking column series + */ +function createStackingColumnSeriesRenderer(): StackingColumnSeriesRendererType { + const sideBySideInfo: DoubleRangeType[] = []; + + return { + sideBySideInfo, + + /** + * Renders the complete stacking column series + * + * @param {SeriesProperties} series - The series data and configuration + * @param {boolean} _isInverted - Whether the chart is inverted (currently unused) + * @returns {Object} Array of render options or object with options and marker data + * + */ + render(series: SeriesProperties, _isInverted: boolean ): + RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps } { + series.isRectSeries = true; + this.sideBySideInfo[series.index] = columnBaseInstance.getSideBySideInfo(series); + const stackedValue: StackValuesType = series.stackedValues; + const options: RenderOptions[] = []; + + for (const point of series.points) { + options.push(this.renderPoint(series, point, this.sideBySideInfo[series.index], stackedValue) as RenderOptions); + } + series.visiblePoints = useVisiblePoints(series); + const marker: ChartMarkerProps | null = series.marker?.visible ? MarkerRenderer.render(series) as Object : null; + return marker ? { options, marker } : options; + }, + + /** + * Renders an individual point in the stacking column series + * + * @param {SeriesProperties} series - The series containing the point + * @param {Points} point - The specific point data to render + * @param {DoubleRangeType} sideBySideInfo - Side-by-side positioning information + * @param {StackValuesType} stackedValue - Stacked values for proper positioning + * @returns {RenderOptions | undefined} Render options for the point or undefined if not rendered + * + */ + renderPoint(series: SeriesProperties, point: Points, sideBySideInfo: DoubleRangeType + , stackedValue: StackValuesType): RenderOptions | undefined { + // Add proper validation + if (!point || !point.visible || !series) { + return undefined; + } + + point.symbolLocations = []; + point.regions = []; + + let index: number | undefined; + let startvalue: number | undefined = 0; + + if (!series.visible && series.isLegendClicked) { + for (let i: number = series.index; i >= 0; i--) { + const currentSeries: SeriesProperties = series.chart.visibleSeries[i as number]; + if (currentSeries.visible && currentSeries.stackingGroup === series.stackingGroup) { + index = currentSeries.index; + break; + } + } + startvalue = series.index > 0 && index !== undefined ? + series.chart.visibleSeries[index as number]?.stackedValues?.endValues?.[point.index] : + series.stackedValues?.startValues?.[point.index]; + } + + const rect: Rect = columnBaseInstance.getRectangle( + Number(point.xValue) + sideBySideInfo.start, + (!series.visible && series.isLegendClicked) ? Number(startvalue) : Number(stackedValue.endValues?.[point.index]), + Number(point.xValue) + sideBySideInfo.end, + (!series.visible && series.isLegendClicked) ? Number(startvalue) : Number(stackedValue.startValues?.[point.index]), + series + ); + + if (series.chart.iSTransPosed && series.columnWidthInPixel) { + rect.height = series.columnWidthInPixel ? series.columnWidthInPixel : rect.width; + rect.y -= series.columnWidthInPixel / 2; + } else { + rect.width = series.columnWidthInPixel ? series.columnWidthInPixel : rect.width; + } + + rect.x = series.columnWidthInPixel ? + (series.chart.iSTransPosed ? rect.x : rect.x - (((series.columnWidthInPixel / 2) * Number(series.rectCount)) - + (series.columnWidthInPixel * (typeof series.position === 'number' ? series.position : 0)))) : rect.x; + + const argsData: PointRenderingEvent = columnBaseInstance.triggerEvent( + series, + point, + series.interior, + { + width: series.border?.width, + color: series.border?.color + } + ); + + columnBaseInstance.updateSymbolLocation(point, { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height + }, series); + + const name: string = series.chart.element.id + '_Series_' + series.index + '_Point_' + point.index; + const option: RenderOptions = columnBaseInstance.drawRectangle(series, point, rect, argsData, name); + return option; + }, + + /** + * Handles animation for stacking column series points + * + * @param {RenderOptions} pathOptions - The render options for the animated element + * @param {number} index - The series index + * @param {object} animationState - Animation state containing references and progress + * @param {boolean} enableAnimation - Whether animation is enabled + * @param {SeriesProperties} currentSeries - The current series being animated + * @param {Points | undefined} currentPoint - The current point being animated (optional) + * @param {number} pointIndex - Index of the current point + * @returns {object} Animation properties including stroke dash array/offset and animated transforms + * @returns {string} returns.strokeDasharray - CSS stroke dash array value + * @returns {number} returns.strokeDashoffset - CSS stroke dash offset value + * @returns {string | undefined} returns.interpolatedD - Interpolated path data (unused for rectangles) + * @returns {string | undefined} returns.animatedDirection - Animated direction transform + * @returns {string | undefined} returns.animatedTransform - Animated transform properties + * + */ + doAnimation( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + currentSeries: SeriesProperties, + currentPoint: Points | undefined, + pointIndex: number + ): { + strokeDasharray: string; + strokeDashoffset: number; + interpolatedD: string | undefined; + animatedDirection: string | undefined; + animatedTransform: string | undefined; + } { + const animatedvalues: { animatedDirection?: string; animatedTransform?: string; } = handleRectAnimation( + pathOptions, currentSeries, index, currentPoint, pointIndex, animationState, enableAnimation + ); + return { + strokeDasharray: 'none', + strokeDashoffset: 0, + interpolatedD: undefined, + animatedDirection: animatedvalues.animatedDirection, + animatedTransform: animatedvalues.animatedTransform + }; + } + }; +} + +/** + * StackingColumnSeriesRenderer - Renders stacked column series for chart visualization + * + * @description A singleton instance that handles rendering of stacking column series with support for animation, + * side-by-side positioning, and custom styling. Provides comprehensive functionality for rendering + * individual points, managing animations, and handling series-level operations. + * + */ +const StackingColumnSeriesRenderer: StackingColumnSeriesRendererType = createStackingColumnSeriesRenderer(); + +export default StackingColumnSeriesRenderer; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/StepLineSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/StepLineSeriesRenderer.tsx new file mode 100644 index 0000000..9fe14b5 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/StepLineSeriesRenderer.tsx @@ -0,0 +1,221 @@ +import { EmptyPointSettings, ChartMarkerProps, ChartLocationProps } from '../../base/interfaces'; +import { StepPosition } from '../../base/enum'; +import { getPoint } from '../../utils/helper'; +import { LineBase, LineBaseReturnType } from './LineBase'; +import MarkerRenderer from './MarkerRenderer'; +import { calculatePathAnimation } from './SeriesAnimation'; +import { Points, RenderOptions, SeriesProperties, StepLineSeriesType } from '../../chart-area/chart-interfaces'; + +const lineBaseInstance: LineBaseReturnType = LineBase; +const StepLineSeriesRenderer: StepLineSeriesType = { + previousX: 0, + previousY: 0, + + /** + * Animates the step line while rendering. + * + * @param {RenderOptions} pathOptions - The rendering options for the step line path. + * @param {number} index - The index of the current series in the chart. + * @param {Object} animationState - Represent the state of animation and its properties. + * @param {boolean} enableAnimation - Flag indicating whether animation should be performed. + * @param {SeriesProperties} _currentSeries - The current series being rendered. + * @param {Points | undefined} _currentPoint - The current point being rendered. + * @param {number} _pointIndex - The index of the current point. + * @param {SeriesProperties[]} [visibleSeries] - Optional array of all visible series in the chart. + * @returns {RenderOptions} The animated render options with interpolated path data + */ + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + _currentSeries: SeriesProperties, + _currentPoint: Points | undefined, + _pointIndex: number, + visibleSeries? : SeriesProperties[] + ) => { + return calculatePathAnimation(pathOptions, index, animationState, enableAnimation, visibleSeries); + }, + + /** + * Renders the step line series. + * + * @param {SeriesProperties} series - The series to be rendered. + * @param {boolean} isInverted - Specifies whether the chart is inverted. + * @returns {Object} Returns step line with markers if enabled. + */ + render: (series: SeriesProperties, isInverted: boolean): + RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps } => { + let direction: string = ''; + let prevPoint: Points | null = null; + let startPoint: string = 'M'; + const getCoordinate: Function = getPoint; + const isDrop: boolean = ((series.emptyPointSettings as EmptyPointSettings).mode === 'Drop') as boolean; + const visiblePoints: Points[] = lineBaseInstance.enableComplexProperty(series); + + for (const point of visiblePoints) { + point.regions = []; + point.symbolLocations = []; + + if (point.visible && point.yValue !== null && point.yValue !== undefined) { + if (prevPoint != null) { + const point1: ChartLocationProps = getCoordinate( + prevPoint.xValue, prevPoint.yValue, series.xAxis, series.yAxis, isInverted, series + ); + const point2: ChartLocationProps = getCoordinate( + point.xValue, point.yValue, series.xAxis, series.yAxis, isInverted, series + ); + const stepType: StepPosition = series.step as StepPosition; + direction += lineBaseInstance.getStepLineDirection( + point2, point1, stepType, startPoint, series, false + ); + } else { + // First point in a segment + const point1: ChartLocationProps = getCoordinate( + point.xValue, point.yValue, series.xAxis, series.yAxis, isInverted, series + ); + direction += `${startPoint} ${point1.x} ${point1.y} `; + } + startPoint = 'L'; + prevPoint = point; + lineBaseInstance.storePointLocation(point, series, isInverted, getCoordinate); + + if (direction === '') { + direction = 'M ' + point.symbolLocations[0].x + ' ' + point.symbolLocations[0].y; + } + } else { + // Handle empty points based on mode + prevPoint = isDrop ? prevPoint : null; + startPoint = isDrop ? startPoint : 'M'; + } + } + + series.visiblePoints = visiblePoints; + const name: string = series.chart.element.id + '_Series_' + series.index; + const options: RenderOptions[] = [{ + id: name, + fill: 'none', + strokeWidth: series.width, + stroke: series.interior, + opacity: series.opacity, + dashArray: series.dashArray, + d: direction + }]; + const marker: ChartMarkerProps | null = series.marker?.visible + ? MarkerRenderer.render(series) as ChartMarkerProps : null; + return marker ? { options, marker } : options; + } +}; + +/** + * Interpolating the path of the step line series. + * + * @param {string} sourcePath - The source path of the step line series. + * @param {string} targetPath - The target path of the step line series. + * @param {number} progress - The progress of the animation. + * @param {string} step - The step type of the step line series. + * @returns {string} - The interpolated path of the step line series. + * @private + */ +export function interpolateSteplinePathD( + sourcePath: string, + targetPath: string, + progress: number, + step: string = 'Center' +): string { + // Parse the path data + const sourcePoints: [number, number][] = parseStepPath(sourcePath); + const targetPoints: [number, number][] = parseStepPath(targetPath); + + // If we have different numbers of points, handle specially + if (sourcePoints.length !== targetPoints.length) { + // For removing points, we'll animate the disappearance + if (sourcePoints.length > targetPoints.length) { + const interpolatedPoints: [number, number][] = []; + // Keep the points that are in the destination + for (let i: number = 0; i < targetPoints.length; i++) { + const x: number = sourcePoints[i as number][0] + (targetPoints[i as number][0] - sourcePoints[i as number][0]) * progress; + const y: number = sourcePoints[i as number][1] + (targetPoints[i as number][1] - sourcePoints[i as number][1]) * progress; + interpolatedPoints.push([x, y]); + } + return generateStepLinePath(interpolatedPoints, step); + } + } + + // For equal length paths, interpolate each coordinate + const interpolatedPoints: [number, number][] = sourcePoints.map((sourcePoint: [number, number], i: number) => { + const targetPoint: [number, number] = targetPoints[Math.min(i, targetPoints.length - 1)]; + return [ + sourcePoint[0] + (targetPoint[0] - sourcePoint[0]) * progress, + sourcePoint[1] + (targetPoint[1] - sourcePoint[1]) * progress + ] as [number, number]; + }); + + return generateStepLinePath(interpolatedPoints, step); +} + +/** + * Parses a step line path data string into an array of points. + * + * @param {string} pathData - The step line path data string to parse. + * @returns {[number, number][]} - The array of points parsed from the path data. + * @private + */ +function parseStepPath(pathData: string): [number, number][] { + const points: [number, number][] = []; + const segments: string[] = pathData.split(/(?=[ML])/); + + segments.forEach((segment: string) => { + const match: RegExpMatchArray = (segment as string).match(/([ML]) ([\d.-]+) ([\d.-]+)/) as RegExpMatchArray; + if (match) { + const [, command, xStr, yStr] = match; + if (command === 'M' || command === 'L') { + points.push([parseFloat(xStr), parseFloat(yStr)]); + } + } + }); + + return points; +} + +/** + * Generates a step line path data string from an array of points. + * + * @param {[number, number][]} points - The array of points to generate the step line path from. + * @param {string} stepPosition - The position of the step. + * @returns {string} - The generated step line path data string. + * @private + */ +function generateStepLinePath(points: [number, number][], stepPosition: string = 'Center'): string { + if (points.length === 0) { return ''; } + + let pathData: string = `M ${points[0][0]} ${points[0][1]}`; + + for (let i: number = 1 as number; i < points.length; i++) { + const [x1, y1]: number[] = points[i - 1] as number[]; + const [x2, y2]: number[] = points[i as number]; + + if (stepPosition === 'Center') { + const midX: number = (x1 + x2) / 2; + pathData += ` L ${midX} ${y1} L ${midX} ${y2}`; + } else if (stepPosition === 'Left') { + pathData += ` L ${x1} ${y2}`; + } else if (stepPosition === 'Right') { + pathData += ` L ${x2} ${y1}`; + } + + pathData += ` L ${x2} ${y2}`; + } + + return pathData; +} + +export default StepLineSeriesRenderer; + diff --git a/components/charts/src/chart/renderer/SeriesRenderer/lineSeriesRenderer.tsx b/components/charts/src/chart/renderer/SeriesRenderer/lineSeriesRenderer.tsx new file mode 100644 index 0000000..2c1adc3 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/lineSeriesRenderer.tsx @@ -0,0 +1,203 @@ + +import { ChartMarkerProps, ChartLocationProps } from '../../base/interfaces'; +import { getPoint } from '../../utils/helper'; +import { LineBase, LineBaseReturnType } from './LineBase'; +import { calculatePathAnimation } from './SeriesAnimation'; +import MarkerRenderer from './MarkerRenderer'; +import { Points, RenderOptions, SeriesProperties } from '../../chart-area/chart-interfaces'; + +// Access LineBase directly as it's now a constant object, not a function +const lineBaseInstance: LineBaseReturnType = LineBase; + +/** + * Gets the line direction path between two points + * + * @param {Points} firstPoint - The starting point coordinates and data + * @param {Points} secondPoint - The ending point coordinates and data + * @param {SeriesProperties} series - Series configuration properties including axes information + * @param {boolean} isInverted - Flag indicating if the chart orientation is inverted + * @param {Function} getPointLocation - Function to convert data coordinates to chart pixel coordinates + * @param {string} startPoint - SVG path command for the starting point (typically 'M' for move or 'L' for line) + * @returns {string} SVG path string representing the line direction between the two points + */ +const getLineDirection: ( + firstPoint: Points, + secondPoint: Points, + series: SeriesProperties, + isInverted: boolean, + getPointLocation: Function, + startPoint: string +) => string = ( + firstPoint: Points, + secondPoint: Points, + series: SeriesProperties, + isInverted: boolean, + getPointLocation: Function, + startPoint: string +): string => { + let direction: string = ''; + if (firstPoint != null) { + const point1: ChartLocationProps = getPointLocation( + firstPoint.xValue, firstPoint.yValue, series.xAxis, series.yAxis, isInverted, series + ); + const point2: ChartLocationProps = getPointLocation( + secondPoint.xValue, secondPoint.yValue, series.xAxis, series.yAxis, isInverted, series + ); + + direction = startPoint + ' ' + (point1.x) + ' ' + (point1.y) + ' ' + + 'L' + ' ' + (point2.x) + ' ' + (point2.y) + ' '; + } + return direction; +}; + +/** + * Handles animation for line series. + * + * @param {RenderOptions} pathOptions - The path rendering options and properties + * @param {number} index - The index of the series being animated + * @param {Object} animationState - Animation state object containing refs and progress + * @param {Object} animationState.previousPathLengthRef - Reference to store previous path lengths for animation + * @param {Object} animationState.isInitialRenderRef - Reference to track if this is the initial render for each series + * @param {Object} animationState.renderedPathDRef - Reference to store previously rendered path data + * @param {number} animationState.animationProgress - Current animation progress value (0 to 1) + * @param {Object} animationState.isFirstRenderRef - Reference to track if this is the very first render + * @param {Object} animationState.previousSeriesOptionsRef - Reference to store previous series options for comparison + * @param {boolean} enableAnimation - Flag indicating if animation is enabled + * @param {SeriesProperties} _currentSeries - The current series being processed (unused) + * @param {Points} _currentPoint - The current point being processed (unused) + * @param {number} _pointIndex - The index of the current point (unused) + * @param {SeriesProperties[]} visibleSeries - Array of visible series for animation calculation + * @returns {Object} Animation properties including dash patterns and transforms + */ +const doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + _currentSeries: SeriesProperties, + _currentPoint: Points | undefined, + _pointIndex: number, + visibleSeries: SeriesProperties[] +) => { strokeDasharray: string | number; strokeDashoffset: number; interpolatedD?: string | undefined } = ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + _currentSeries: SeriesProperties, + _currentPoint: Points | undefined, + _pointIndex: number, + visibleSeries: SeriesProperties[] +) => { + return calculatePathAnimation(pathOptions, index, animationState, enableAnimation, visibleSeries); +}; + + +/** + * Renders a line series by processing visible points and generating SVG path data for chart visualization. + * + * @param {SeriesProperties} series - The series configuration object containing data points, styling properties, and chart settings + * @param {boolean} isInverted - Flag indicating whether the chart axes are inverted (swaps x-axis and y-axis positions) + * @returns {RenderOptions[]|Object} Either an array of RenderOptions for the line path, or an object containing both options and marker configuration when markers are enabled + */ +const render: ( + series: SeriesProperties, + isInverted: boolean +) => RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps } = ( + series: SeriesProperties, + isInverted: boolean +): RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps } => { + let direction: string = ''; + let prevPoint: Points | null = null; + let startPoint: string = 'M'; + const getCoordinate: typeof getPoint = getPoint; + const isDrop: boolean = Boolean(series.emptyPointSettings && series.emptyPointSettings.mode === 'Drop'); + const visiblePoints: Points[] = lineBaseInstance.enableComplexProperty(series) || []; + + for (const point of visiblePoints) { + point.regions = []; + point.symbolLocations = []; + + if (point.visible) { + direction += getLineDirection(prevPoint as Points, point, series, isInverted, getCoordinate, startPoint); + startPoint = prevPoint ? 'L' : startPoint; + prevPoint = point; + lineBaseInstance.storePointLocation(point, series, isInverted, getCoordinate); + + if (direction === '' && visiblePoints.length === 1) { + direction = 'M ' + point.symbolLocations[0].x + ' ' + point.symbolLocations[0].y; + } + } else { + prevPoint = isDrop ? prevPoint : null; + startPoint = isDrop ? startPoint : 'M'; + } + } + series.visiblePoints = visiblePoints; + const name: string = + series.chart.element.id + '_Series_' + series.index; + const options: RenderOptions[] = [{ + id: name, + fill: 'none', + strokeWidth: series.width, + stroke: series.interior, + opacity: series.opacity, + dashArray: series.dashArray, + d: direction + }]; + const marker: Object | null = series.marker?.visible ? MarkerRenderer.render(series) as Object : null; + return marker ? { options, marker } : options; +}; + +/** + * Line Series Renderer - Pure functional approach + */ +const LineSeriesRenderer: { + getLineDirection: ( + firstPoint: Points, + secondPoint: Points, + series: SeriesProperties, + isInverted: boolean, + getPointLocation: Function, + startPoint: string + ) => string; + doAnimation: ( + pathOptions: RenderOptions, + index: number, + animationState: { + previousPathLengthRef: React.RefObject; + isInitialRenderRef: React.RefObject; + renderedPathDRef: React.RefObject; + animationProgress: number; + isFirstRenderRef: React.RefObject; + previousSeriesOptionsRef: React.RefObject; + }, + enableAnimation: boolean, + _currentSeries: SeriesProperties, + _currentPoint: Points | undefined, + _pointIndex: number, + visibleSeries: SeriesProperties[] + ) => { strokeDasharray: string | number; strokeDashoffset: number; interpolatedD?: string | undefined }; + render: ( + series: SeriesProperties, + isInverted: boolean + ) => RenderOptions[] | { options: RenderOptions[]; marker: ChartMarkerProps }; +} = { + getLineDirection, + doAnimation, + render +}; + +export default LineSeriesRenderer; diff --git a/components/charts/src/chart/renderer/SeriesRenderer/updatePoint.tsx b/components/charts/src/chart/renderer/SeriesRenderer/updatePoint.tsx new file mode 100644 index 0000000..8c055f1 --- /dev/null +++ b/components/charts/src/chart/renderer/SeriesRenderer/updatePoint.tsx @@ -0,0 +1,239 @@ +import { pushCategoryPoint, pushDateTimePoint } from './ProcessData'; +import { extend, getValue } from '@syncfusion/react-base'; +import { createPoint, getObjectValue, pushData, pushDoublePoint, setEmptyPoint } from './ProcessData'; +import { useRegisterAxisRender } from '../../hooks/useClipRect'; +import { DataPoint, Points, SeriesProperties } from '../../chart-area/chart-interfaces'; + +/** + * Updates a specific point within a series based on the axis value type. + * + * @param {number} index - The index of the point to update + * @param {SeriesProperties} series - The series containing the point + * @returns {void} + * @private + */ +export const updatePoint: (index: number, series: SeriesProperties) => void = (index: number, series: SeriesProperties): void => { + const point: Points = createPoint(); + const xField: string | undefined = series.xField as string; + const textMappingName: string = series.marker?.dataLabel?.labelField ? + series.marker.dataLabel.labelField : ''; + if (series.xAxis.valueType === 'Category') { + pushCategoryPoint(series, point, index, xField, textMappingName); + } + else if (series.xAxis.valueType === 'DateTime') { + pushDateTimePoint(series, point, index, xField, textMappingName); + } + else { + pushDoublePoint(series, point, index, xField); + } +}; + +/** + * Adjusts the series points after a point has been removed, ensuring consistent series data. + * + * @param {number} index - The index of the removed point + * @param {SeriesProperties} series - The series to update + * @returns {void} + * @private + */ +const updatePointsAfterRemoval: (index: number, series: SeriesProperties) => void = (index: number, series: SeriesProperties): void => { + void (series.points?.[index as number] + && (pushData(series.points[index as number], index, series), + setEmptyPoint(series.points[index as number], series, index)) + ); +}; + +/** + * Animates the path of the series by controlling the animation progress. + * + * @param {number} timestamp - The current timestamp from requestAnimationFrame + * @param {number} duration - The duration of the animation in milliseconds + * @param {(progress: number) => void} setAnimationProgress - Function to update animation progress state (0-1) + * @param {React.MutableRefObject} animationFrameRef - Ref object to store animation frame ID + * @param {number} startTime - The start time of the animation in milliseconds + * @returns {void} + * @private + */ + +export const animatePath: (timestamp: number, duration: number, setAnimationProgress: (progress: number) => void, + animationFrameRef: React.MutableRefObject, startTime: number) => void = (timestamp: number, duration: number, + setAnimationProgress: (progress: number) => void, + animationFrameRef: React.MutableRefObject, + startTime: number): void => { + const elapsed: number = timestamp - startTime; + const progress: number = duration === 0 ? 1 : Math.min((elapsed / duration), 1); + setAnimationProgress(progress); + if (progress < 1) { + animationFrameRef.current = requestAnimationFrame((time: DOMHighResTimeStamp) => + animatePath(time, duration, setAnimationProgress, animationFrameRef, startTime) + ); + } else { + cancelAnimationFrame(animationFrameRef.current); + } +}; + +/** + * Adds a point to the series. + * + * @param {DataPoint} pointData - The data point to add. + * @param {number} pointData.x - The x-coordinate of the point. + * @param {number} duration - Animation duration. + * @param {number} pointData.y - The y-coordinate of the point. + * @param {Series} series - The series to add the point to. + * @param {MutableRefObject} internalDataUpdateRef - Reference to track internal data updates. + * @param {React.Dispatch>} setAnimationProgress - Function to update animation progress state. + * @param {MutableRefObject} animationFrameRef - Reference to the animation frame. + * @param {number | null} animationFrameRef.current - The current animation frame ID. + * @param {Function} updateSeries - Function to update the series data after animation. + * @returns {void} This function does not return a value + * @private + */ +export const addPoint: (pointData: DataPoint, duration: number, series: SeriesProperties, + internalDataUpdateRef: React.MutableRefObject, + setAnimationProgress: (progress: number) => void, animationFrameRef: React.MutableRefObject, + updateSeries: (xAxis: boolean, yAxis: boolean, series: SeriesProperties) => void) => void = ( + pointData: DataPoint, + duration: number, + series: SeriesProperties, + internalDataUpdateRef: React.MutableRefObject, + setAnimationProgress: (progress: number) => void, + animationFrameRef: React.MutableRefObject, + updateSeries: (xAxis: boolean, yAxis: boolean, series: SeriesProperties) => void +): void => { + if (!Array.isArray(series.dataSource)) { + return; + } + (series.dataSource as Object[]).push(pointData); + series.currentViewData = series.dataSource as DataPoint[]; + const lastIndex: number = series.points?.[series.points.length - 1]?.index as number; + const pointIndex: number = lastIndex + 1; + updatePoint(pointIndex, series); + + void(internalDataUpdateRef && (internalDataUpdateRef.current = true)); + + series.chart.axisRender(); + requestAnimationFrame((startTime: number) => { + updateSeries(true, false, series); + animatePath(startTime, duration, setAnimationProgress, animationFrameRef, startTime); + }); +}; + +/** + * Removes a point from the series. + * + * @param {number} index - The index of the point to remove. + * @param {number} duration - Animation duration. + * @param {Series} series - The series to remove the point from. + * @param {MutableRefObject} internalDataUpdateRef - Reference to track internal data updates. + * @param {Function} setAnimationProgress - Function to set animation progress. + * @param {MutableRefObject} animationFrameRef - Reference to the animation frame. + * @param {number|null} animationFrameRef.current - The current animation frame ID. + * @param {Function} updateSeries - Function to update the series. + * @returns {void} - This function doesn't return a value. + * @private + */ +export const removePoint: (index: number, duration: number, series: SeriesProperties, + internalDataUpdateRef: React.MutableRefObject, + setAnimationProgress: (progress: number) => void, + animationFrameRef: React.MutableRefObject, + updateSeries: (xAxis: boolean, yAxis: boolean, series: SeriesProperties) => void) => void = ( + index: number, + duration: number, + series: SeriesProperties, + internalDataUpdateRef: React.MutableRefObject, + setAnimationProgress: (progress: number) => void, + animationFrameRef: React.MutableRefObject, + updateSeries: (xAxis: boolean, yAxis: boolean, series: SeriesProperties) => void +): void => { + const dataSource: Object[] = extend([], series.dataSource as Object, void 0, true) as Object[]; + void (dataSource.length > 0 && index >= 0 && index < dataSource.length && (() => { + dataSource.splice(index, 1); + (series.dataSource as object[]).splice(index, 1); + series.removedPointIndex = index; + series?.points?.splice(index, 1); + series?.visiblePoints?.splice(index, 1); + series.yData = []; + series.xData = []; + + void(internalDataUpdateRef && (internalDataUpdateRef.current = true)); + + series.yMin = Infinity; + series.xMin = Infinity; + series.yMax = -Infinity; + series.xMax = -Infinity; + for (let i: number = 0; i < (series.points).length; i++) { + updatePointsAfterRemoval(i, series); + } + })()); + + series.chart.axisRender(); + requestAnimationFrame((startTime: number) => { + updateSeries(true, false, series); + animatePath(startTime, duration, setAnimationProgress, animationFrameRef, startTime); + }); +}; + + +/** + * Sets new data for the series with animation support. + * + * @param {Object[]} data - The new data array to set for the series. + * @param {number} duration - Animation duration. + * @param {Series} series - The series to update. + * @param {MutableRefObject} internalDataUpdateRef - Reference to track internal data updates. + * @param {Function} setAnimationProgress - Function to set animation progress. + * @param {MutableRefObject} animationFrameRef - Reference to the animation frame. + * @param {number|null} animationFrameRef.current - The current animation frame ID. + * @param {Function} updateSeries - Function to update the series. + * @returns {void} - This function doesn't return a value. + * @private + */ +export const setData: (data: Object[], duration: number, + series: SeriesProperties, internalDataUpdateRef: React.MutableRefObject, + setAnimationProgress: (progress: number) => void, + animationFrameRef: React.MutableRefObject, + updateSeries: (xAxis: boolean, yAxis: boolean, series: SeriesProperties) => void) => void = ( + data: Object[], + duration: number, + series: SeriesProperties, + internalDataUpdateRef: React.MutableRefObject, + setAnimationProgress: (progress: number) => void, + animationFrameRef: React.MutableRefObject, + updateSeries: (xAxis: boolean, yAxis: boolean, series: SeriesProperties) => void +): void => { + void(internalDataUpdateRef && (internalDataUpdateRef.current = true)); + + series.yMin = Infinity; + series.yMax = -Infinity; + const points: number[] = []; + let samePoints: boolean = false; + + void ((series.dataSource as Object[]).length === data.length && (() => { + samePoints = true; + series.yData = []; + for (let i: number = 0; i < data.length; i++) { + const point: Points = series.points?.[i as number]; + const getObjectValueByMappingString: Function = series.enableComplexProperty ? getValue : getObjectValue; + const newPoint: Object = data[i as number]; + point.y = getObjectValueByMappingString(series.yField, newPoint); + points.push(i); + + point.yValue = (typeof point.y === 'number' && point.y !== null) ? point.y : 0; + point.x = getObjectValueByMappingString(series.xField, newPoint); + setEmptyPoint(point, series, series.index); + (series.dataSource as Object[])[i as number] = data[i as number]; + } + })()); + + const triggerRender: (chartId?: string) => void = useRegisterAxisRender(); + triggerRender(series?.chart?.element?.id); + void ( + !samePoints + ? (series.dataSource = data) + : requestAnimationFrame((startTime: number) => { + updateSeries(false, true, series); + animatePath(startTime, duration, setAnimationProgress, animationFrameRef, startTime); + }) + ); +}; + diff --git a/components/charts/src/chart/renderer/TooltipRenderer.tsx b/components/charts/src/chart/renderer/TooltipRenderer.tsx new file mode 100644 index 0000000..e16777d --- /dev/null +++ b/components/charts/src/chart/renderer/TooltipRenderer.tsx @@ -0,0 +1,933 @@ +import { useState, useEffect, useRef } from 'react'; +import { useLayout } from '../layout/LayoutContext'; +import { ChartFontProps, ChartTooltipProps, TooltipContentFunction } from '../base/interfaces'; +import { Tooltip, TooltipRefHandle } from '@syncfusion/react-svg-tooltip'; +import { registerChartEventHandler } from '../hooks/useClipRect'; +import { isNullOrUndefined } from '@syncfusion/react-base'; +import { AxisModel, BaseZoom, Chart, Points, Rect, SeriesProperties, VisibleRangeProps } from '../chart-area/chart-interfaces'; +import { ChartMarkerShape } from '../base/enum'; + +/** + * Represents data for a specific point in a chart series. + * Contains information about the point, its associated series, and positioning details. + * + * @private + */ +export interface PointData { + /** Defines the data point object containing values and visualization properties. */ + point: Points; + + /** Defines the series that contains this data point. */ + series: SeriesProperties; + + /** Optional index when multiple points overlap or are in outlier positions. */ + lierIndex?: number; + + /** Optional coordinates specifying the position of the point on the chart. */ + location?: { x: number, y: number }; +} + +/** + * Checks if the provided coordinates are within the bounds of the rectangle. + * + * @param {number} x - The x-coordinate to check. + * @param {number} y - The y-coordinate to check. + * @param {Rect} bounds - The bounding rectangle. + * @param {number} width - The width of the area to include in the bounds check. + * @param {number} height - The height of the area to include in the bounds check. + * @returns {boolean} - Returns true if the coordinates are within the bounds; otherwise, false. + */ +function withInBounds(x: number, y: number, bounds: Rect, width: number = 0, height: number = 0): boolean { + return ( + x >= bounds.x - width && + x <= bounds.x + bounds.width + width && + y >= bounds.y - height && + y <= bounds.y + bounds.height + height + ); +} + + +export const TooltipRenderer: React.FC = (props: ChartTooltipProps) => { + const { layoutRef, phase } = useLayout(); + const [tooltipVisible, setTooltipVisible] = useState(false); + const [tooltipLocation, setTooltipLocation] = useState({ x: 0, y: 0 }); + const [tooltipData, setTooltipData] = useState<{ + header: string; + content: string[]; + pointData: Points | undefined; + textStyle: object; + }>({ + header: '', + content: [], + pointData: undefined, + textStyle: props.textStyle || {} + }); + const [shapes, setShapes] = useState([]); + const [palette, setPalette] = useState([]); + const [currentPoints, setCurrentPoints] = useState([]); + const [lierIndex, setLierIndex] = useState(0); + + // Use refs for values that shouldn't trigger re-renders + const previousPointsRef: React.RefObject = useRef([]); + const tooltipRef: React.RefObject = useRef(null); + const hideTooltipTimeoutRef: React.RefObject = useRef(null); + const lastUpdateTimeRef: React.RefObject = useRef(0); + const touchTimeoutRef: React.RefObject = useRef(null); + + useEffect(() => { + if (phase === 'rendering' && layoutRef.current.chart) { + // Register tooltip mouse handlers + const unregisterMouseMove: () => void = registerChartEventHandler( + 'mouseMove', + (_e: Event, chart: Chart) => handleMouseMove(chart), (layoutRef.current?.chart as Chart)?.element.id + ); + + const unregisterMouseDown: () => void = registerChartEventHandler( + 'mouseDown', + (e: Event, chart: Chart) => { + if (layoutRef.current?.chartZoom && (layoutRef.current?.chartZoom as BaseZoom).isPanning) { + return; + } + handleMouseDown(e, chart); + }, (layoutRef.current?.chart as Chart)?.element.id + ); + + const unregisterMouseUp: () => void = registerChartEventHandler( + 'mouseUp', + (e: Event) => handleMouseUp(e), (layoutRef.current?.chart as Chart)?.element.id + ); + + const unregisterMouseLeave: () => void = registerChartEventHandler( + 'mouseLeave', + () => handleMouseOut(), (layoutRef.current?.chart as Chart)?.element.id + ); + const chart: Chart = (layoutRef.current.chart as Chart); + chart.tooltipRef = tooltipRef; + // Return cleanup function + return () => { + unregisterMouseMove(); + unregisterMouseDown(); + unregisterMouseUp(); + unregisterMouseLeave(); + }; + } + return; + }, [phase, layoutRef.current.chart, props.shared]); + + useEffect(() => { + return () => { + if (hideTooltipTimeoutRef.current) { + clearTimeout(hideTooltipTimeoutRef.current); + } + if (touchTimeoutRef.current) { + clearTimeout(touchTimeoutRef.current); + } + }; + }, []); + + /** + * Handle touch start events for tooltip updates - synchronized with trackball + * + * @param {Event} event - The touch event that initiates the tooltip interaction + * @param {Chart} chart - The chart object + * @returns {void} + */ + function handleMouseDown(event: Event, chart: Chart): void { + if (event.type === 'touchstart') { + if (touchTimeoutRef.current) { + clearTimeout(touchTimeoutRef.current); + touchTimeoutRef.current = null; + } + if (hideTooltipTimeoutRef.current) { + clearTimeout(hideTooltipTimeoutRef.current); + hideTooltipTimeoutRef.current = null; + } + + if (withInBounds(chart.mouseX, chart.mouseY, chart.chartAxislayout.seriesClipRect) && + !chart.zoomRedraw && !chart.startPanning) { + if (props.shared) { + renderGroupedTooltip(chart); + } else { + renderSeriesTooltip(); + } + } + } + } + + /** + * Handles mouse move events on the chart, updating tooltip visibility and position. + * + * @param {Chart} chart - The chart object containing layout and data details. + * @returns {void} + */ + function handleMouseMove(chart: Chart): void { + // Check if mouse is within chart area + if (chart.chartAxislayout?.seriesClipRect && + withInBounds(chart.mouseX, chart.mouseY, chart.chartAxislayout.seriesClipRect) && !chart.zoomRedraw && !chart.startPanning) { + // Clear any pending hide timeout + if (hideTooltipTimeoutRef.current) { + clearTimeout(hideTooltipTimeoutRef.current); + hideTooltipTimeoutRef.current = null; + } + // Handle tooltip display based on shared setting + if (!props.shared) { + renderSeriesTooltip(); + } else { + renderGroupedTooltip(chart); + } + } else { + // Hide tooltip if mouse is outside chart area + if (tooltipRef.current) { + if (hideTooltipTimeoutRef.current) { + clearTimeout(hideTooltipTimeoutRef.current); + } + hideTooltipTimeoutRef.current = setTimeout(() => { + tooltipRef.current?.fadeOut(); + setTooltipVisible(false); + hideTooltipTimeoutRef.current = null; + }, props.fadeOutDuration); + } + } + } + + /** + * Handles mouse out events on the chart, hiding the tooltip. + * + * @returns {void} + */ + function handleMouseOut(): void { + if (tooltipRef.current) { + setTimeout(() => { + tooltipRef.current?.fadeOut(); + }, props.fadeOutDuration); + } + setTooltipVisible(false); + } + + /** + * Handles mouse up events on the chart, hiding the tooltip with timeout for touch events. + * Synchronized with TrackballRenderer timeout behavior. + * + * @param {Event} [event] - The mouse or touch event (optional). + * @returns {void} + */ + function handleMouseUp(event?: Event): void { + if (event && event.type === 'touchend') { + if (touchTimeoutRef.current) { + clearTimeout(touchTimeoutRef.current); + touchTimeoutRef.current = null; + } + + touchTimeoutRef.current = setTimeout(() => { + tooltipRef.current?.fadeOut(); + setTooltipVisible(false); + touchTimeoutRef.current = null; + }, 2000); + } + const data: PointData = getData() as PointData; + if (lierIndex) { + data.lierIndex = lierIndex; + } + // If no data point found, hide tooltip and return + if ((!data || !data.point) && props.fadeOutMode === 'Click') { + if (tooltipRef.current) { + // Schedule hiding the tooltip after delay + if (hideTooltipTimeoutRef.current) { + clearTimeout(hideTooltipTimeoutRef.current); + } + hideTooltipTimeoutRef.current = setTimeout(() => { + tooltipRef.current?.fadeOut(); + setTooltipVisible(false); + hideTooltipTimeoutRef.current = null; + }, props.fadeOutDuration); + } + return; + } + } + + /** + * Retrieves data needed for displaying the tooltip based on the current mouse position. + * + * @returns {PointData | null} The PointData object containing information for the tooltip, or null if no data is found. + */ + function getData(): PointData | null { + const chart: Chart = layoutRef.current.chart as Chart; + let point: Points | null = null; + let series: SeriesProperties | null = null; + for (let len: number = chart.visibleSeries.length, i: number = len - 1; i >= 0; i--) { + series = chart.visibleSeries[i as number]; + const width: number = (series.type === 'Scatter' || series.drawType === 'Scatter' || (series.marker?.visible)) + ? (series.marker?.height as number + 5) / 2 : 0; + const height: number = (series.type === 'Scatter' || series.drawType === 'Scatter' || (series.marker?.visible)) + ? (series.marker?.width as number + 5) / 2 : 0; + const mouseX: number = chart.mouseX; + const mouseY: number = chart.mouseY; + if (series.visible && series.clipRect && withInBounds(mouseX, mouseY, series.clipRect, width, height)) { + point = getRectPoint(series, series.clipRect, mouseX, mouseY); + } + if (point) { + return { point, series, lierIndex }; + } + } + return null; + } + + /** + * Determines which point, if any, is located within a specified rectangle. + * + * @param {SeriesProperties} series - The chart series to evaluate points on. + * @param {Rect} rect - The rectangle to check points against. + * @param {number} x - The x-coordinate to check. + * @param {number} y - The y-coordinate to check. + * @returns {Points | null} The found point or null if no points are within the rectangle. + */ + function getRectPoint(series: SeriesProperties, rect: Rect, x: number, y: number): Points | null { + const insideRegion: boolean = false; + for (const point of series.visiblePoints as Points[]) { + if (!point.regionData) { + if (!point.regions || !point.regions.length) { + continue; + } + } + + // Check if point is in region + if (!insideRegion && point.regions && checkRegionContainsPoint(point.regions, rect, x, y)) { + return point; + } else if (insideRegion && point.regions && checkRegionContainsPoint(point.regions, rect, x, y)) { + return point; + } + } + return null; + } + + /** + * Checks whether a region rectangle contains a specific point. + * + * @param {Rect[]} regionRect - An array of regions describing rectangles. + * @param {Rect} rect - The parent rectangle area. + * @param {number} x - The x-coordinate of the point to check. + * @param {number} y - The y-coordinate of the point to check. + * @returns {boolean} Returns true if the region contains the point, false otherwise. + */ + function checkRegionContainsPoint(regionRect: Rect[], rect: Rect, x: number, y: number): boolean { + return regionRect.some((region: Rect, index: number) => { + setLierIndex(index); + return withInBounds( + x, y, + { + x: (rect.x) + region.x, + y: (rect.y) + region.y, + width: region.width, + height: region.height + } + ); + }); + } + + /** + * Generates a tooltip header based on the PointData object. + * + * @param {PointData} data - The data object containing point and series information. + * @returns {string} The formatted header string for the tooltip. + */ + function findHeader(data: PointData): string { + const headerTemplate: string = props.headerText === null || props.headerText === undefined ? + (props.shared ? '${point.x}' : '${series.name}') : + props.headerText!; + // Parse template + let header: string = headerTemplate; + // Replace point values + Object.entries(data.point).forEach(([key, val]: [string, object]) => { + const placeholder: string = `\${point.${key}}`; + const xAxis: AxisModel = data.series.xAxis; + const yAxis: AxisModel = data.series.yAxis; + const value: string = formatPointValue(val, placeholder === '${point.x}' ? xAxis : yAxis, placeholder === '${point.x}', placeholder === '${point.y}'); + header = header.split(placeholder).join(value); + }); + // Replace series values using Object.entries + Object.entries(data.series).forEach(([key, val]: [string, string]) => { + const placeholder: string = `\${series.${key}}`; + const value: string = val?.toString() || ''; + header = header.split(placeholder).join(value); + }); + if (header.replace(//g, '').replace(/<\/b>/g, '').trim() !== '') { + return header; + } + return ''; + } + + /** + * Formats the text content for the tooltip using data from PointData. + * + * @param {PointData} data - The data object containing point and series information. + * @returns {string} The formatted text for the tooltip. + */ + function getTooltipText(data: PointData): string { + // Get format + let format: string | undefined = props.format || data.series.tooltipFormat; + if (!format) { + const textX: string = '${point.x}'; + format = !props.shared ? textX : '${series.name}'; + format += ': ' + ('${point.y}'); + } + // Parse template + let text: string = format; + // Replace point values + // Replace point values using Object.entries + Object.entries(data.point).forEach(([key, val]: [string, object]) => { + const placeholder: string = `\${point.${key}}`; + const xAxis: AxisModel = data.series.xAxis; + const yAxis: AxisModel = data.series.yAxis; + const value: string = formatPointValue(val, placeholder === '${point.x}' ? xAxis : yAxis, placeholder === '${point.x}', placeholder === '${point.y}'); + text = text.split(placeholder).join(value); + }); + + // Replace series values using Object.entries + Object.entries(data.series).forEach(([key, val]: [string, string]) => { + const placeholder: string = `\${series.${key}}`; + const value: string = val?.toString() || ''; + text = text.split(placeholder).join(value); + }); + return text; + } + + /** + * Formats the point value based on axis settings and point type. + * + * @param {object} pointValue - The value of the point to format. + * @param {AxisModel} axis - The axis model containing format information. + * @param {boolean} isXPoint - Whether this is an X-axis point. + * @param {boolean} isYPoint - Whether this is a Y-axis point. + * @returns {string} The formatted point value as a string. + */ + function formatPointValue(pointValue: object, axis: AxisModel, isXPoint: boolean, isYPoint: boolean): string { + let textValue: string; + let customLabelFormat: boolean; + let value: string; + const axisLabelFormat: string = axis.labelStyle?.format || ''; + if (axis.valueType !== 'Category' && isXPoint) { + customLabelFormat = axisLabelFormat !== '' && axisLabelFormat.match('{value}') !== null; + const formattedValue: string | number | Object = axis.valueType === 'Double' ? +pointValue : pointValue; + textValue = customLabelFormat ? axisLabelFormat.replace('{value}', axis.format(formattedValue)) : + axis.format(formattedValue); + } else if (isYPoint && !isNullOrUndefined(pointValue)) { + customLabelFormat = axisLabelFormat !== '' && axisLabelFormat.match('{value}') !== null; + value = axis.format(+pointValue); + textValue = customLabelFormat ? axisLabelFormat.replace('{value}', value) : value; + } else { + textValue = pointValue?.toString() || ''; + } + return textValue; + } + + /** + * Renders a series tooltip with the relevant data and styles. + * + * @returns {void} + */ + function renderSeriesTooltip(): void { + let data: PointData = getData() as PointData; + let closestPointData: PointData | null = null; + let smallestDistance: number = Infinity; + + if (lierIndex) { + data.lierIndex = lierIndex; + } + // If no data point found directly under cursor, check for nearest point if enabled + if (!data || !data.point) { + const chart: Chart = layoutRef.current.chart as Chart; + + if (props.showNearestTooltip && withInBounds(chart.mouseX, chart.mouseY, chart.chartAxislayout.seriesClipRect)) { + const commonXvalues: number[] = getCommonXValues(chart.visibleSeries); + + // Find nearest point in any visible series + for (let i: number = chart.visibleSeries.length - 1; i >= 0; i--) { + const series: SeriesProperties = chart.visibleSeries[i as number]; + if (!series.visible || !series.enableTooltip || series.isRectSeries || series.type === 'Scatter' || series.type === 'Bubble') { + continue; + } + const pointData: PointData | null = getClosestX(chart, series, commonXvalues); + + if (pointData && pointData.point && series.clipRect && pointData.point.symbolLocations && + pointData.point.symbolLocations[0]) { + const symbolX: number = series.clipRect.x + pointData.point.symbolLocations[0].x; + const symbolY: number = series.clipRect.y + pointData.point.symbolLocations[0].y; + if (!withInBounds(symbolX, symbolY, chart.chartAxislayout.seriesClipRect)) { + continue; + } + // Calculate distance to current point + const currentDistance: number = Math.sqrt( + Math.pow((chart.mouseX - series.clipRect.x) - pointData.point.symbolLocations[0].x, 2) + + Math.pow((chart.mouseY - series.clipRect.y) - pointData.point.symbolLocations[0].y, 2) + ); + + if (currentDistance < smallestDistance) { + smallestDistance = currentDistance; + closestPointData = pointData; + } + } + } + + if (closestPointData) { + data = closestPointData; + } + } + } + + // If still no data found or point found but tooltip should hide + if ((!data || !data.point) || !data.series?.enableTooltip) { + if (tooltipRef.current && props.fadeOutMode === 'Move') { + // Schedule hiding the tooltip after delay + if (hideTooltipTimeoutRef.current) { + clearTimeout(hideTooltipTimeoutRef.current); + } + hideTooltipTimeoutRef.current = setTimeout(() => { + tooltipRef.current?.fadeOut(); + setTooltipVisible(false); + hideTooltipTimeoutRef.current = null; + }, props.fadeOutDuration); + } + return; + } + // Check if it's the same point as before + const isSamePoint: boolean = previousPointsRef.current.length > 0 && + previousPointsRef.current[0]?.point?.index === data.point.index && + previousPointsRef.current[0]?.series?.index === data.series.index; + if (isSamePoint) { + // If it's the same point and tooltip is not visible, make it visible + void (!tooltipVisible && tooltipRef.current && ( + tooltipRef.current.fadeIn(), + setTooltipVisible(true) + )); + return; + } + // Update previous points BEFORE updating tooltip data to prevent blinking + previousPointsRef.current = [data]; + + const header: string = findHeader(data); + let content: string = getTooltipText(data); + + const customText: string | string[] | boolean = applyTooltipContentCallback(content, props); + if (typeof customText === 'boolean' && !customText) { + return; // Don't show tooltip if event is canceled + } else if (typeof customText !== 'boolean') { + content = customText as string; + } + + // Get tooltip location + const location: { x: number, y: number } | null = getSymbolLocation(data); + location!.x = props.location?.x !== undefined ? props.location!.x : location!.x; + location!.y = props.location?.y !== undefined ? props.location!.y : location!.y; + + // Update tooltip data + setTooltipData({ + header, + content: [content], + pointData: data.point, + textStyle: props.textStyle as ChartFontProps + }); + setTooltipLocation(location!); + setShapes([getShapeForSeries(data)]); + setPalette([findColor(data)]); + setTooltipVisible(true); + + // Show tooltip + tooltipRef.current?.fadeIn(); + // Update current points + setCurrentPoints([data]); + } + + + /** + * Determines the color for a data point in a series. + * + * @param {PointData} data - The data object containing point and series information. + * @returns {string} The determined color for the data point. + */ + function findColor(data: PointData): string { + return data.point.color && data.point.color !== '#ffffff' ? data.point.color : + data.point.interior || data.series.marker?.fill || data.series.interior; + } + + /** + * Finds the symbol location for a given data point. + * + * @param {PointData} data - The data object containing point and series information. + * @returns {{x: number, y: number} | null} The calculated symbol location or null if not available. + */ + function getSymbolLocation(data: PointData): { x: number, y: number } | null { + if (!data.point.symbolLocations || !data.point.symbolLocations[0]) { + return null; + } + return { + x: data.series.clipRect!.x + data.point.symbolLocations[0].x, + y: data.series.clipRect!.y + data.point.symbolLocations[0].y + }; + } + + /** + * Determines the shape of the tooltip marker based on the series type. + * + * @param {PointData} data - The data object containing point and series information. + * @returns {ChartMarkerShape} The shape of the tooltip marker. + */ + function getShapeForSeries(data: PointData): ChartMarkerShape { + if (!props.showMarker) { + return 'None'; + } + const markerShape: ChartMarkerShape = data.series.marker?.shape === 'None' ? 'None' : ((data).point.marker.shape || data.series.marker?.shape || 'Circle'); + return markerShape; + } + + /** + * Renders a grouped tooltip for shared tooltip mode, handling multiple data points. + * + * @param {Chart} chart - The chart object containing layout and data details. + * @returns {void} This function does not return a value.[/SUFFIX] + */ + function renderGroupedTooltip(chart: Chart): void { + // Clear current points and prepare for new data + setCurrentPoints([]); + const dataCollection: PointData[] = []; + let lastData: PointData | null = null; + let tempData: PointData | null = null; + let closestXValue: number = Number.MAX_VALUE; + let closetYValue: number = Number.MAX_VALUE; + let pointXValue: number; + let pointYValue: number; + // Get common X values across all visible series + const commonXValues: number[] = getCommonXValues(chart.visibleSeries); + // Process each visible series to find points at the same X position + for (const series of chart.visibleSeries) { + if (!series.visible || !series.enableTooltip) { + continue; + } + // Get closest point in this series + const data: PointData | null = getClosestX(chart, series, commonXValues); + if (data && data.point) { + // Calculate distance to determine the closest point overall + pointXValue = (!chart.requireInvertedAxis) ? + chart.mouseX - (data.series.clipRect!.x) : + chart.mouseY - (data.series.clipRect!.y); + pointYValue = chart.mouseY - (data.series.clipRect!.y); + if (data.point.symbolLocations && data.point.symbolLocations.length && + Math.abs(pointXValue - data.point.symbolLocations[0].x) <= closestXValue && + Math.abs(data.point.symbolLocations[0].y - pointYValue) < Math.abs(closetYValue - pointYValue)) { + closestXValue = Math.abs(pointXValue - data.point.symbolLocations[0].x); + closetYValue = data.point.symbolLocations[0].y; + tempData = data; + } + lastData = tempData || data; + dataCollection.push(data); + } + } + + // If no data points found, hide tooltip + if (dataCollection.length === 0) { + tooltipRef.current?.fadeOut(); + setTooltipVisible(false); + return; + } + + // Filter points to only include those at the same X position as the closest point + const collection: PointData[] = []; + for (const data of dataCollection) { + if (data.point.symbolLocations?.[0].x === lastData?.point?.symbolLocations?.[0].x || ((data.series.type?.indexOf('Column') !== -1 || + lastData?.series.type?.indexOf('Column') !== -1) && (data.point.xValue === lastData?.point.xValue))) { + collection.push(data); + } + } + // Prepare tooltip data + const header: string = findHeader(collection[0]); + let contentArray: string[] = []; + const newShapes: ChartMarkerShape[] = []; + const newPalette: string[] = []; + // Generate content for each point + for (const data of collection) { + contentArray.push(getTooltipText(data)); + newShapes.push(getShapeForSeries(data)); + newPalette.push(findColor(data)); + } + + const customText: string | string[] | boolean = applyTooltipContentCallback(contentArray, props); + if (typeof customText === 'boolean' && !customText) { + return; // Don't show tooltip if event is canceled + } + else if (typeof customText !== 'boolean') { + contentArray = customText as string[]; + } + + // Find tooltip location + let location: { x: number, y: number } | null = null; + void (lastData && (location = findSharedLocation(collection, lastData))); + if (!location) { + location = { x: chart.mouseX, y: chart.mouseY }; + } + + // Update tooltip data + setTooltipData({ + header, + content: contentArray, + pointData: collection.length === 1 ? collection[0].point : lastData?.point, + textStyle: props.textStyle as ChartFontProps + }); + + const now: number = Date.now(); + const timeSinceLastUpdate: number = now - lastUpdateTimeRef.current; + const shouldUpdatePosition: boolean = timeSinceLastUpdate > 50; // Only update position every 50ms + + if (shouldUpdatePosition) { + setTooltipLocation(location); + lastUpdateTimeRef.current = now; + } + setShapes(newShapes); + setPalette(newPalette); + setCurrentPoints(collection); + setTooltipVisible(true); + // Show tooltip + tooltipRef.current?.fadeIn(); + } + + /** + * Helper function to get common X values across all visible series. + * + * @param {SeriesProperties[]} visibleSeries - The array of visible series in the chart. + * @returns {number[]} An array of common X values across the series. + */ + function getCommonXValues(visibleSeries: SeriesProperties[]): number[] { + const commonXValues: number[] = []; + for (let j: number = 0; j < visibleSeries.length; j++) { + for (let i: number = 0; i < (visibleSeries[j as number].points && visibleSeries[j as number].points.length); i++) { + const point: Points = visibleSeries[j as number].points[i as number]; + if (point && (point.index === 0 || point.index === visibleSeries[j as number].points.length - 1 || + (point.symbolLocations && point.symbolLocations.length > 0))) { + void (point.xValue != null && commonXValues.push(point.xValue)); + } + } + } + return commonXValues; + } + + /** + * Finds the point closest to the current X position in the series. + * + * @param {Chart} chart - The chart object containing layout and data details. + * @param {SeriesProperties} series - The series within the chart to find the point in. + * @param {number[]} [xvalues] - Optional array of X values to consider. + * @returns {PointData | null} The closest PointData object or null if not found. + */ + function getClosestX(chart: Chart, series: SeriesProperties, xvalues?: number[]): PointData | null { + let value: number = 0; + const rect: Rect = series.clipRect as Rect; + + // Determine value based on axis inversion and mouse position + void (chart.mouseX <= rect.x + rect.width && chart.mouseX >= rect.x && + (value = chart.requireInvertedAxis ? + getValueYByPoint(chart.mouseY - rect.y, rect.height, series.xAxis) : + getValueXByPoint(chart.mouseX - rect.x, rect.width, series.xAxis))); + // Get closest x value + const closest: number | null = getClosest(series, value, xvalues); + + // Find the point with this X value using the closest result + const point: Points | undefined = closest !== null ? series.visiblePoints?.find((p: Points) => p.xValue === closest && p.visible) + : undefined; + // Return the point and series only if a point is found; otherwise, return null + return point ? { point, series, lierIndex } : null; + } + + /** + * Finds the closest numeric value to a target within an array of `xData` values in the series. + * + * @param {SeriesProperties} series - The series within which to find the closest value. + * @param {number} value - The target value to find closest to. + * @param {number[]} [xvalues] - An optional array of X values to use for reference. + * @returns {number | null} The closest value or null if not found. + */ + function getClosest(series: SeriesProperties, value: number, xvalues?: number[]): number | null { + let closest: number = 0; let data: number; + const xData: number[] = xvalues ? xvalues : series.xData; + const xLength: number = xData.length; + const leftSideNearest: number = 0.5; + const rightSideNearest: number = 0.5; + if (value >= series.xAxis.visibleRange.minimum - leftSideNearest && value <= series.xAxis.visibleRange.maximum + rightSideNearest) { + for (let i: number = 0; i < xLength; i++) { + data = xData[i as number]; + if (closest == null || Math.abs(data - value) < Math.abs(closest - value)) { + closest = data; + } + } + } + const isDataExist: boolean = series.xData.indexOf(closest) !== -1; + if (isDataExist) { + return closest; + } else { + return null; + } + } + + /** + * Converts an X coordinate to a value using the axis configuration. + * + * @param {number} value - The position value to convert. + * @param {number} size - The size of the plotting area. + * @param {AxisModel} axis - The axis containing the range and scaling information. + * @returns {number} The equivalent value on the axis. + */ + function getValueXByPoint(value: number, size: number, axis: AxisModel): number { + const actualValue: number = !axis.isAxisInverse ? value / size : (1 - (value / size)); + return actualValue * (axis.visibleRange.delta) + axis.visibleRange.minimum; + } + + /** + * Converts a Y coordinate to a value using the axis configuration. + * + * @param {number} value - The position value to convert. + * @param {number} size - The size of the plotting area. + * @param {AxisModel} axis - The axis containing the range and scaling information. + * @returns {number} The equivalent value on the axis. + */ + function getValueYByPoint(value: number, size: number, axis: AxisModel): number { + const actualValue: number = axis.isAxisInverse ? value / size : (1 - (value / size)); + return actualValue * (axis.visibleRange.delta) + axis.visibleRange.minimum; + } + + /** + * Finds the shared location of the tooltip when multiple points are grouped. + * + * @param {PointData[]} points - An array of PointData representing the grouped points. + * @param {PointData} lastData - The last data point used for positioning. + * @returns {{x: number, y: number} | null} The shared tooltip location or null if not available. + */ + function findSharedLocation(points: PointData[], lastData: PointData): { x: number, y: number } | null { + const chart: Chart = layoutRef.current.chart as Chart; + if (points.length > 1) { + // Calculate proper position using mouse values + const mouseValues: { valueX: number, valueY: number } = findMouseValues(lastData, chart); + + return { + x: mouseValues.valueX, + y: mouseValues.valueY + }; + } else { + return getSymbolLocation(points[0]); + } + } + + + /** + * Converts a value to a coefficient based on the axis configuration. + * + * @param {number} value - The value to convert to a coefficient. + * @param {AxisModel} axis - The axis containing the range and scaling information. + * @returns {number} The coefficient based on the axis configuration. + */ + function valueToCoefficient(value: number | null, axis: AxisModel): number { + const range: VisibleRangeProps = axis.visibleRange; + const result: number = (value! - range.minimum) / (range.delta); + const isInverse: boolean = axis.isAxisInverse; + return isInverse ? (1 - result) : result; + } + + /** + * Calculates mouse values used for positioning the tooltip. + * + * @param {PointData} data - The data point containing X and Y values. + * @param {Chart} chart - The chart containing layout and axis information. + * @returns {{valueX: number, valueY: number}} The calculated mouse X and Y values. + */ + function findMouseValues(data: PointData, chart: Chart): { valueX: number, valueY: number } { + let valueX: number = 0; + let valueY: number = 0; + + if (!chart.requireInvertedAxis) { + valueX = valueToCoefficient(data.point.xValue, data.series.xAxis) * data.series.xAxis.rect.width + + data.series.xAxis.rect.x; + valueY = chart.mouseY; + } else { + valueY = (1 - valueToCoefficient(data.point.xValue, data.series.xAxis)) * data.series.xAxis.rect.height + + data.series.xAxis.rect.y; + valueX = chart.mouseX; + } + + return { valueX, valueY }; + } + + /** + * Calculates the marker height for tooltip offset + * + * @param {PointData} pointData - The point data to calculate marker height for + * @returns {number} The calculated marker height + */ + function findMarkerHeight(pointData: PointData): number { + let markerHeight: number = 0; + const series: SeriesProperties = pointData.series; + markerHeight = (((series.marker?.visible) || ((props.shared || props.showNearestTooltip) && + (!series.isRectSeries || series.marker?.visible)) || series.type === 'Scatter') && series.marker?.shape !== 'Image') ? + ((series.marker?.height as number) + 2) / 2 + (2 * (series.marker?.border?.width || 0)) : 0; + return markerHeight; + } + + if (phase === 'measuring') { + return null; + } + const chart: Chart = layoutRef.current.chart as Chart; + chart.tooltipRef = tooltipRef; + const areaBounds: Rect = chart?.rect; + return ( + 1 || + (props.location && (props.location.x !== undefined || props.location.y !== undefined)) ? 0 : 7} + offset={currentPoints[0] && findMarkerHeight(currentPoints[0])} + areaBounds={areaBounds} + isFixed={(props.location && (props.location.x !== undefined || props.location.y !== undefined))} + controlName="Chart" + enableAnimation={props.enableAnimation} + textStyle={tooltipData.textStyle} + // isTextWrap={true} + duration={props.duration} + opacity={props.opacity} + fill={props.fill} + border={props.border} + inverted={chart.requireInvertedAxis && currentPoints?.[0]?.series?.isRectSeries} + theme={chart?.theme} + enableRTL={chart?.enableRtl} + markerSize={7} + /> + ); +}; + +/** + * Applies a custom tooltip content callback to modify the tooltip's appearance or content. + * + * @param {string | string[] | boolean} text - The original tooltip content, which can be a string or an array of strings. + * @param {ChartTooltipProps} tooltipProps - The tooltip configuration object that may contain a custom content callback. + * @returns {string} The modified tooltip content as a string, array of strings, or boolean. + * + * @private + */ +export function applyTooltipContentCallback( + text: string | string[], + tooltipProps: ChartTooltipProps +): string | string[] | boolean { + const contentCallback: TooltipContentFunction = tooltipProps.formatter as TooltipContentFunction; + if (contentCallback && typeof contentCallback === 'function') { + try { + const customProps: string | string[] | boolean = contentCallback(text); + return customProps; + } catch (error) { + return text; + } + } + return text; +} diff --git a/components/charts/src/chart/renderer/TrackballRenderer.tsx b/components/charts/src/chart/renderer/TrackballRenderer.tsx new file mode 100644 index 0000000..a8307b1 --- /dev/null +++ b/components/charts/src/chart/renderer/TrackballRenderer.tsx @@ -0,0 +1,962 @@ +import { forwardRef, JSX, useEffect, useRef, useState } from 'react'; +import { useLayout } from '../layout/LayoutContext'; +import { ChartTooltipProps, ChartMarkerProps, ChartBorderProps, ChartLocationProps } from '../base/interfaces'; +import { drawSymbol, withInBounds } from '../utils/helper'; +import { registerChartEventHandler } from '../hooks/useClipRect'; +import { PointData } from './TooltipRenderer'; +import { colorNameToHex, convertHexToColor } from './SeriesRenderer/DataLabelRender'; +import { AxisModel, Chart, ColorValue, PathOptions, Points, Rect, SeriesProperties, ChartSizeProps, BaseZoom } from '../chart-area/chart-interfaces'; +import { ChartMarkerShape } from '../base/enum'; +/** + * Interface representing a trackball marker displayed on the chart. + */ +interface TrackballMarker { + /** Index of the series this marker belongs to */ + seriesIndex: number; + /** X coordinate of the marker */ + x: number; + /** Y coordinate of the marker */ + y: number; + /** Fill color of the marker */ + fill: string; + /** Border properties of the marker */ + border: { + /** Width of the marker border */ + width: number; + /** Color of the marker border */ + color: string; + }; + /** Size properties of the marker */ + size: { + /** Width of the marker */ + width: number; + /** Height of the marker */ + height: number; + }; + /** Visibility state of the marker */ + visible: boolean; + /** Shape of the marker */ + shape: ChartMarkerShape; + /** Optional image URL for image markers */ + imageUrl?: string; + /** Index of the current point being tracked */ + currentPointIndex: number; + /** Data point associated with this marker */ + point?: PointData; + /** Stroke color for the marker */ + stroke: string; + /** Shadow effect for the marker */ + markerShadow: string; + /** Animation state management. */ + animationState: 'appearing' | 'visible' | 'disappearing'; + /** Current radius for smooth animation. */ + currentRadius: number; + /** Target radius to animate towards. */ + targetRadius: number; +} + +/** + * Renders trackball markers for chart series data points. + * + * @component TrackballRenderer. + * @param {ChartTooltipProps} props - The tooltip model properties. + * @returns {JSX.Element} A trackball renderer component. + */ +export const TrackballRenderer: React.ForwardRefExoticComponent> = + forwardRef((props: ChartTooltipProps, ref: React.ForwardedRef) => { + const { layoutRef, phase } = useLayout(); + const [trackballMarkers, setTrackballMarkers] = useState([]); + const animationFrameRef: React.RefObject = useRef(null); + const markerTimeoutRef: React.RefObject = useRef(null); + const touchTimeoutRef: React.RefObject = useRef(null); + const isTouchModeRef: React.RefObject = useRef(false); + const isProcessingTouchRef: React.RefObject = useRef(false); + const previousPointRef: React.RefObject<{ + seriesIndex: number; + pointIndex: number; + } | null> + = useRef<{ + seriesIndex: number; + pointIndex: number; + } | null>(null); + const commonXValueRef: React.RefObject = useRef(null); + const markersCreatedRef: React.RefObject = useRef(false); + /** + * Initialize trackball markers for each series + */ + useEffect(() => { + if (phase !== 'rendering' || !layoutRef.current.chart) { + return; + } + markersCreatedRef.current = false; + const chart: Chart = layoutRef.current.chart as Chart; + // Initialize one marker per series with visible: false + const initialMarkers: TrackballMarker[] = []; + // Don't render markers if tooltip is disabled + if (!props.enable) { + markersCreatedRef.current = true; + return; + } + // Create one marker for each visible series + for (const series of chart.visibleSeries) { + if (!series.visible || !series.enableTooltip || !series.points || series.points.length === 0) { continue; } + // Check if marker should be shown for this series + // For shared tooltip or showNearestTooltip, show markers regardless of series marker visibility + const shouldShowMarker: boolean = ((props.shared || props.showNearestTooltip) && !series.isRectSeries) + || (series.marker?.visible !== false); + if (!shouldShowMarker) { continue; } + // Get marker and series marker settings + const seriesMarker: ChartMarkerProps | undefined = series.marker; + // Create a single marker for this series + if (seriesMarker?.highlightable) { + const baseRadius: number = (seriesMarker?.width || 8) + 3; + initialMarkers.push({ + seriesIndex: series.index || 0, + x: 0, // Will be updated when showing + y: 0, // Will be updated when showing + fill: seriesMarker?.fill || series.interior, + border: { + width: seriesMarker?.border?.width || 1, + color: seriesMarker?.border?.color || series.interior + }, + size: { + width: (seriesMarker?.width || 8) + 3, + height: (seriesMarker?.height as number) + 3 + }, + visible: false, // Initially hidden + shape: seriesMarker?.shape || 'Circle', + imageUrl: seriesMarker?.imageUrl, + currentPointIndex: -1, + stroke: '', + markerShadow: '', + animationState: 'visible', + currentRadius: 0, + targetRadius: baseRadius + }); + } + } + setTrackballMarkers(initialMarkers); + markersCreatedRef.current = true; + }, [phase, props.enable, props.shared, props.showNearestTooltip, (layoutRef.current.chart as Chart)?.visibleSeries.map((s: SeriesProperties) => s.visible).join('')]); + /** + * Set up mouse event listeners for trackball tracking + */ + useEffect(() => { + if (phase !== 'rendering' || !layoutRef.current.chart) { + return; + } + /** + * Handle mouse movement for trackball updates + * + * @param {Chart} chart - The chart object + * @returns {void} + */ + function handleMouseMove(chart: Chart): void { + if (withInBounds(chart.mouseX, chart.mouseY, chart.chartAxislayout.seriesClipRect) && + !chart.zoomRedraw && !chart.startPanning) { + if (props.shared) { + updateSharedTrackballVisibility(); + } else { + updateTrackballVisibility(); + } + } else { + hideAllMarkers(); + } + } + + /** + * Handle mouse leave events to hide trackball. + * + * @returns {void} + */ + function handleMouseLeave(): void { + isTouchModeRef.current = false; + hideAllMarkers(); + } + + /** + * Handle touch movement events for trackball updates + * @param {Event} event - The touch event that initiates the trackball interaction + * @param {Chart} chart - The chart object + * @returns {void} + */ + function handleMouseDown(event: Event, chart: Chart): void { + if (event.type === 'touchstart') { + if (isProcessingTouchRef.current) { + return; + } + isProcessingTouchRef.current = true; + isTouchModeRef.current = true; + if (touchTimeoutRef.current) { + clearTimeout(touchTimeoutRef.current); + touchTimeoutRef.current = null; + } + + setTimeout(() => { + if (withInBounds(chart.mouseX, chart.mouseY, chart.chartAxislayout.seriesClipRect) && + !chart.zoomRedraw && !chart.startPanning) { + if (props.shared) { + updateSharedTrackballVisibility(); + } else { + updateTrackballVisibility(); + } + } + + setTimeout(() => { + isProcessingTouchRef.current = false; + }, 50); + }, 16); + } + } + + /** + * Handle touch end events specifically for trackball with separate timeout + * + * @param {Event} event - The touch end event + * @returns {void} + */ + function handleTrackballTouchEnd(event: Event): void { + if (event.type === 'touchend') { + isTouchModeRef.current = false; + if (touchTimeoutRef.current) { + clearTimeout(touchTimeoutRef.current); + touchTimeoutRef.current = null; + } + + touchTimeoutRef.current = setTimeout(() => { + setTrackballMarkers((currentMarkers: TrackballMarker[]) => + currentMarkers.map((marker: TrackballMarker) => ({ ...marker, visible: false })) + ); + previousPointRef.current = null; + commonXValueRef.current = null; + touchTimeoutRef.current = null; + }, 2000); + } + } + + const unregisterMouseDown: () => void = registerChartEventHandler( + 'mouseDown', + (e: Event, chart: Chart) => { + if (layoutRef.current?.chartZoom && (layoutRef.current?.chartZoom as BaseZoom).isPanning) { + return; + } + handleMouseDown(e, chart); + }, (layoutRef.current?.chart as Chart)?.element.id + ); + + const unregisterMouseMove: () => void = registerChartEventHandler( + 'mouseMove', + (_e: Event, chart: Chart) => handleMouseMove(chart), (layoutRef.current?.chart as Chart)?.element.id + ); + + const unregisterMouseLeave: () => void = registerChartEventHandler( + 'mouseLeave', + () => handleMouseLeave(), (layoutRef.current?.chart as Chart)?.element.id + ); + const unregisterTouchEnd: () => void = registerChartEventHandler( + 'mouseUp', // This handles touchend events as well + (e: Event) => handleTrackballTouchEnd(e), + (layoutRef.current?.chart as Chart)?.element.id + ); + + // Return cleanup function + return () => { + unregisterMouseMove(); + unregisterMouseLeave(); + unregisterMouseDown(); + unregisterTouchEnd(); + }; + }, [phase, trackballMarkers.length]); + + /** + * Hides all trackball markers with a timeout + * + * @returns {void} + */ + function hideAllMarkers(): void { + if (markerTimeoutRef.current) { + clearTimeout(markerTimeoutRef.current); + } + markerTimeoutRef.current = setTimeout(() => { + setTrackballMarkers((currentMarkers: TrackballMarker[]) => + currentMarkers.map((marker: TrackballMarker) => ({ ...marker, visible: false })) + ); + previousPointRef.current = null; + commonXValueRef.current = null; + }, (layoutRef.current.chart as Chart).zoomRedraw ? 0 : 1000); + } + + const animateMarkers: () => void = () => { + setTrackballMarkers((currentMarkers: TrackballMarker[]) => { + let hasActiveAnimation: boolean = false; + + const updatedMarkers: TrackballMarker[] = currentMarkers.map((marker: TrackballMarker) => { + if (marker.animationState === 'appearing' || marker.animationState === 'disappearing') { + const animationSpeed: number = 0.15; + const difference: number = marker.targetRadius - marker.currentRadius; + const series: SeriesProperties = (layoutRef.current.chart as Chart)?.visibleSeries[marker.seriesIndex]; + const markerVisibility: boolean = series && (series.marker?.visible || series.type === 'Bubble' || series.type === 'Scatter'); + if (Math.abs(difference) > 0.5 && markerVisibility) { + hasActiveAnimation = true; + const newRadius: number = marker.currentRadius + (difference * animationSpeed); + + return { + ...marker, + currentRadius: newRadius + }; + } else { + // Animation complete + if (marker.animationState === 'appearing') { + return { + ...marker, + currentRadius: marker.targetRadius, + animationState: 'visible' as const + }; + } else { + return { + ...marker, + currentRadius: 0, + visible: false, + animationState: 'visible' as const + }; + } + } + } + return marker; + }); + + if (hasActiveAnimation) { + animationFrameRef.current = requestAnimationFrame(animateMarkers); + } + + return updatedMarkers; + }); + }; + const startMarkerAnimation: (seriesIndex: number, appearing: boolean) => void = (seriesIndex: number, appearing: boolean) => { + setTrackballMarkers((currentMarkers: TrackballMarker[]) => + currentMarkers.map((marker: TrackballMarker) => { + if (marker.seriesIndex === seriesIndex) { + const chart: Chart = layoutRef.current.chart as Chart; + const series: SeriesProperties = chart?.visibleSeries[marker.seriesIndex]; + const isBubbleSeries: boolean = series?.type === 'Bubble'; + const targetRadius: number = appearing ? (marker.size.width + marker.size.height) / 4 : 0; + return { + ...marker, + animationState: appearing ? 'appearing' : 'disappearing', + targetRadius, + currentRadius: appearing ? (isBubbleSeries ? marker.currentRadius : 0) : marker.currentRadius + }; + } + return marker; + }) + ); + + // Start animation loop + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + + animationFrameRef.current = requestAnimationFrame(animateMarkers); + + }; + + useEffect(() => { + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + if (markerTimeoutRef.current) { + clearTimeout(markerTimeoutRef.current); + } + if (touchTimeoutRef.current) { + clearTimeout(touchTimeoutRef.current); + } + }; + }, []); + /** + * Finds the data point at the current cursor position. + * + * @param {Chart} chart - The chart object + * + * @returns {Object} Object containing the point and series at cursor position + */ + function findPointAtCursor(chart: Chart): { point: Points | null, series: SeriesProperties | null } { + for (let i: number = chart.visibleSeries.length - 1; i >= 0; i--) { + const series: SeriesProperties = chart.visibleSeries[i as number]; + if (series.visible && series.clipRect && + withInBounds(chart.mouseX, chart.mouseY, series.clipRect)) { + // For showNearestTooltip and shared tooltip, check points regardless of marker visibility + const shouldCheckPoints: boolean = props.shared || props.showNearestTooltip || + (series.marker?.visible !== false); + if (!shouldCheckPoints) { continue; } + const point: Points | null = findPointInSeries(series, chart.mouseX, chart.mouseY); + if (point) { + return { point, series }; + } + } + } + return { point: null, series: null }; + } + + /** + * Gets the common X values across all visible series + * + * @param {SeriesProperties[]} visibleSeries - Array of visible series + * @returns {number[]} Array of common X values + */ + function getCommonXValues(visibleSeries: SeriesProperties[]): number[] { + const commonXValues: number[] = []; + for (let j: number = 0; j < visibleSeries.length; j++) { + const series: SeriesProperties = visibleSeries[j as number]; + if (!series.points) { continue; } + for (let i: number = 0; i < series.points.length; i++) { + const point: Points = series.points[i as number]; + if (point && (point.index === 0 || point.index === series.points.length - 1 || + (point.symbolLocations && point.symbolLocations.length > 0))) { + void (point.xValue != null && commonXValues.push(point.xValue)); + } + } + } + return commonXValues; + } + + /** + * Finds the closest point in a series to the current mouse position + * + * @param {Chart} chart - The chart object + * @param {SeriesProperties} series - The series to check + * @param {number[]} [xvalues] - Optional array of X values + * @returns {PointData | null} The closest point data or null + */ + function getClosestPoint(chart: Chart, series: SeriesProperties, xvalues?: number[]): + { point: Points, series: SeriesProperties } | null { + + const rect: Rect = series.clipRect!; + let value: number = 0; + + // Determine value based on axis inversion and mouse position + if (chart.mouseX <= rect.x + rect.width && chart.mouseX >= rect.x) { + value = chart.requireInvertedAxis ? + getValueYByPoint(chart.mouseY - rect.y, rect.height, series.xAxis) : + getValueXByPoint(chart.mouseX - rect.x, rect.width, series.xAxis); + } + + // Find closest X value + const closest: number | null = getClosestXValue(series, value, xvalues); + + // Find the point with this X value + const point: Points | undefined = closest !== null ? + series.visiblePoints?.find((p: Points) => p.xValue === closest && p.visible) : undefined; + + // Return the point and series only if a point is found + return point ? { point, series } : null; + } + + /** + * Finds the closest X value in a series to a target value + * + * @param {SeriesProperties} series - The series to search in + * @param {number} value - The target value + * @param {number[]} [xvalues] - Optional array of X values to use + * @returns {number | null} The closest X value or null if not found + */ + function getClosestXValue(series: SeriesProperties, value: number, xvalues?: number[]): number | null { + let closest: number = 0; + const xData: number[] = xvalues ? xvalues : series.xData; + const xLength: number = xData.length; + const leftSideNearest: number = 0.5; + const rightSideNearest: number = 0.5; + + if (value >= series.xAxis.visibleRange.minimum - leftSideNearest && + value <= series.xAxis.visibleRange.maximum + rightSideNearest) { + for (let i: number = 0; i < xLength; i++) { + const data: number = xData[i as number]; + if (closest == null || Math.abs(data - value) < Math.abs(closest - value)) { + closest = data; + } + } + } + + const isDataExist: boolean = series.xData.indexOf(closest) !== -1; + return isDataExist ? closest : null; + } + + /** + * Converts an X position to a value on the axis + * + * @param {number} value - The X position + * @param {number} size - The width of the plotting area + * @param {AxisModel} axis - The axis model + * @returns {number} The converted value + */ + function getValueXByPoint(value: number, size: number, axis: AxisModel): number { + const actualValue: number = !axis.isAxisInverse ? value / size : (1 - (value / size)); + return actualValue * (axis.visibleRange.delta) + axis.visibleRange.minimum; + } + + /** + * Converts a Y position to a value on the axis + * + * @param {number} value - The Y position + * @param {number} size - The height of the plotting area + * @param {AxisModel} axis - The axis model + * @returns {number} The converted value + */ + function getValueYByPoint(value: number, size: number, axis: AxisModel): number { + const actualValue: number = axis.isAxisInverse ? value / size : (1 - (value / size)); + return actualValue * (axis.visibleRange.delta) + axis.visibleRange.minimum; + } + + /** + * Finds a point in a series at the given mouse coordinates + * + * @param {SeriesProperties} series - The series to search in + * @param {number} mouseX - Mouse X coordinate + * @param {number} mouseY - Mouse Y coordinate + * @returns {Points | null} The found point or null if not found + */ + function findPointInSeries(series: SeriesProperties, mouseX: number, mouseY: number): Points | null { + for (const point of series.visiblePoints as Points[]) { + if (point.regions && checkRegionContainsPoint(point, series, mouseX, mouseY)) { + return point; + } + } + return null; + } + /** + * Checks if a given point contains the mouse position + * + * @param {Points} point - The point to check + * @param {SeriesProperties} series - The series the point belongs to + * @param {number} mouseX - Mouse X coordinate + * @param {number} mouseY - Mouse Y coordinate + * @returns {boolean} True if the point contains the mouse position + */ + function checkRegionContainsPoint(point: Points, series: SeriesProperties, mouseX: number, mouseY: number): boolean { + if (!point.regions || point.regions.length === 0) { return false; } + for (const region of point.regions) { + if (withInBounds( + mouseX, mouseY, + { + x: (series.clipRect?.x || 0) + region.x, + y: (series.clipRect?.y || 0) + region.y, + width: region.width, + height: region.height + } + )) { + return true; + } + } + return false; + } + /** + * Finds the closest X value to the current mouse position + * + * @param {Chart} chart - The chart object + * @returns {number | null} The closest X value or null if not found + */ + function findClosestXValue(chart: Chart): number | null { + if (!chart.visibleSeries || chart.visibleSeries.length === 0) { return null; } + const mouseX: number = chart.mouseX; + let closestDistance: number = Number.MAX_VALUE; + let closestXValue: number | null = null; + for (const series of chart.visibleSeries) { + if (!series.visible || !series.clipRect || !series.points) { continue; } + for (const point of series.points) { + if (!point.symbolLocations || !point.symbolLocations[0]) { continue; } + const distance: number = Math.abs((series.clipRect.x + point.symbolLocations[0].x) - mouseX); + if (distance < closestDistance) { + closestDistance = distance; + closestXValue = point.xValue; + } + } + } + return closestXValue; + } + /** + * Updates trackball visibility for single point tracking + * + * @returns {void} + */ + function updateTrackballVisibility(): void { + const chart: Chart = layoutRef.current.chart as Chart; + let result: { + point: Points | null; + series: SeriesProperties | null; + } = findPointAtCursor(chart); + + // Clear existing timeout + if (markerTimeoutRef.current) { + clearTimeout(markerTimeoutRef.current); + markerTimeoutRef.current = null; + } + + // If mouse is outside chart area, hide all markers + if (!withInBounds(chart.mouseX, chart.mouseY, chart.chartAxislayout.seriesClipRect)) { + hideAllMarkers(); + return; + } + + // If no point found directly under cursor, check for nearest point if enabled + if ((!result.point || !result.series) && props.showNearestTooltip) { + let closestPointData: PointData | null = null; + let smallestDistance: number = Infinity; + + const commonXValues: number[] = getCommonXValues(chart.visibleSeries); + + // Find nearest point in any visible series + for (let i: number = chart.visibleSeries.length - 1; i >= 0; i--) { + const series: SeriesProperties = chart.visibleSeries[i as number]; + // For showNearestTooltip, check all visible series regardless of marker visibility + if (!series.visible || !series.enableTooltip || series.isRectSeries || series.type === 'Scatter' || series.type === 'Bubble') { continue; } + + const pointData: { + point: Points; + series: SeriesProperties; + } | null = getClosestPoint(chart, series, commonXValues); + + if (pointData && series.clipRect && pointData.point.symbolLocations?.[0]) { + const symbolX: number = series.clipRect.x + pointData.point.symbolLocations[0].x; + const symbolY: number = series.clipRect.y + pointData.point.symbolLocations[0].y; + if (!withInBounds(symbolX, symbolY, chart.chartAxislayout.seriesClipRect)) { + continue; + } + + // Calculate distance to current point + const currentDistance: number = Math.sqrt( + Math.pow((chart.mouseX - series.clipRect.x) - pointData.point.symbolLocations[0].x, 2) + + Math.pow((chart.mouseY - series.clipRect.y) - pointData.point.symbolLocations[0].y, 2) + ); + + if (currentDistance < smallestDistance) { + smallestDistance = currentDistance; + closestPointData = pointData; + } + } + } + + if (closestPointData) { + result = { point: closestPointData.point, series: closestPointData.series }; + } + } + + // If still no point found, hide all markers + if (!result.point || !result.series || !result.series.enableTooltip) { + hideAllMarkers(); + return; + } + + if (result.point.symbolLocations && result.point.symbolLocations[0] && result.series.clipRect) { + const symbolX: number = result.series.clipRect.x + result.point.symbolLocations[0].x; + const symbolY: number = result.series.clipRect.y + result.point.symbolLocations[0].y; + if (!withInBounds(symbolX, symbolY, chart.chartAxislayout.seriesClipRect)) { + hideAllMarkers(); + return; + } + } + + // Check if it's the same point as before + const isSamePoint: boolean = previousPointRef.current?.seriesIndex === result.series.index && + previousPointRef.current?.pointIndex === result.point.index; + if (isSamePoint) { + return; + } + + // Update current point for reference + previousPointRef.current = { + seriesIndex: result.series.index, + pointIndex: result.point.index + }; + + // Update the position of the marker for this series + setTrackballMarkers((currentMarkers: TrackballMarker[]) => + currentMarkers.map((marker: TrackballMarker) => { + if (marker.seriesIndex === result.series?.index) { + const point: Points = result.point!; + const location: ChartLocationProps | null = point.symbolLocations && point.symbolLocations[0]; + if (!location) { return marker; } + const size: ChartSizeProps = { height: point.marker?.height as number, width: point.marker?.width as number }; + const series: SeriesProperties = result.series!; + const border: ChartBorderProps = (point.marker.border || series.border) as ChartBorderProps; + const explodeSeries: boolean = (series.type === 'Bubble' || series.type === 'Scatter'); + const borderColor: string = (border.color && border.color !== 'transparent') ? border.color : + point.marker.fill || point.interior || (explodeSeries ? point.color : series.interior); + const colorValue: ColorValue = convertHexToColor(colorNameToHex(borderColor)); + const markerShadow: string = series.chart.themeStyle.markerShadow || + 'rgba(' + colorValue.r + ',' + colorValue.g + ',' + colorValue.b + ',0.2)'; + const markerShape: ChartMarkerShape = series?.marker?.shape === 'None' ? 'None' : point.marker?.shape || marker.shape || 'Circle'; + const markerRadius: number = series.type === 'Bubble' ? ((point.marker?.width as number + (point.marker?.height as number)) / 4) - 5 : // 5px padding for bubble animation. + ((series.marker?.width as number) + (series.marker?.height as number)) / 4; + const updatedMarker: TrackballMarker = { + ...marker, + size: (result.series.type === 'Bubble' ? size : marker.size), + shape: markerShape, + x: location.x + (result.series?.clipRect?.x || 0), + y: location.y + (result.series?.clipRect?.y || 0), + visible: true, + currentPointIndex: point.index || 0, + fill: (point.marker.fill || point.color || (explodeSeries ? series.interior : '#ffffff')), + stroke: borderColor, + markerShadow: markerShadow, + currentRadius: markerRadius, + animationState: 'appearing' as const + }; + + setTimeout(() => startMarkerAnimation(marker.seriesIndex, true), 0); + + return updatedMarker; + } + if (marker.visible) { + setTimeout(() => startMarkerAnimation(marker.seriesIndex, false), 0); + } + return { ...marker, visible: false }; + }) + ); + + // Set timeout for auto hiding only if not in touch mode + if (!isTouchModeRef.current) { + markerTimeoutRef.current = setTimeout(() => { + setTrackballMarkers((currentMarkers: TrackballMarker[]) => + currentMarkers.map((marker: TrackballMarker) => ({ ...marker, visible: false })) + ); + }, 2000); + } + } + + /** + * Updates trackball visibility for shared tooltip mode + * + * @returns {void} + */ + function updateSharedTrackballVisibility(): void { + const chart: Chart = layoutRef.current.chart as Chart; + const xValue: number | null = findClosestXValue(chart); + // Clear timeout if any + if (markerTimeoutRef.current) { + clearTimeout(markerTimeoutRef.current); + markerTimeoutRef.current = null; + } + // If no X value found or mouse outside chart area, hide markers + if (xValue === null || !withInBounds(chart.mouseX, chart.mouseY, chart.chartAxislayout.seriesClipRect)) { + hideAllMarkers(); + return; + } + // Check if same X value as before + if (commonXValueRef.current === xValue) { + return; + } + commonXValueRef.current = xValue; + // Update markers for each series + setTrackballMarkers((currentMarkers: TrackballMarker[]) => + currentMarkers.map((marker: TrackballMarker) => { + const series: SeriesProperties = chart.visibleSeries[marker.seriesIndex]; + if (!series || !series.points || !series.visible || !series.enableTooltip) { + return { ...marker, visible: false }; + } + // Find the point in this series that matches the X value + const matchingPoint: Points | undefined = series.visiblePoints?.find((p: Points) => p.xValue === xValue); + if (matchingPoint && matchingPoint.symbolLocations && matchingPoint.symbolLocations[0]) { + const size: ChartSizeProps = { + height: matchingPoint.marker?.height as number, + width: matchingPoint.marker?.width as number + }; + const border: ChartBorderProps = (matchingPoint.marker.border || series.border) as ChartBorderProps; + const explodeSeries: boolean = (series.type === 'Bubble' || series.type === 'Scatter'); + const borderColor: string = (border.color && border.color !== 'transparent') ? border.color : + matchingPoint.marker.fill || matchingPoint.interior || (explodeSeries ? matchingPoint.color : series.interior); + const colorValue: ColorValue = convertHexToColor(colorNameToHex(borderColor)); + const markerShadow: string = series.chart.themeStyle.markerShadow || + 'rgba(' + colorValue.r + ',' + colorValue.g + ',' + colorValue.b + ',0.2)'; + const markerShape: ChartMarkerShape = series?.marker?.shape === 'None' ? 'None' : matchingPoint.marker?.shape || marker.shape || 'Circle'; + const markerRadius: number = series.type === 'Bubble' ? (matchingPoint.marker?.width as number + (matchingPoint.marker?.height as number)) / 4 - 5 : // 5px padding for bubble animation. + ((series.marker?.width as number) + (series.marker?.height as number)) / 4; + const updatedMarker: TrackballMarker = { + ...marker, + x: matchingPoint.symbolLocations[0].x + (series.clipRect?.x as number), + y: matchingPoint.symbolLocations[0].y + (series.clipRect?.y as number), + size: (series.type === 'Bubble' ? size : marker.size), + shape: markerShape, + visible: true, + currentPointIndex: matchingPoint.index || 0, + // Update fill and other properties from the specific point if needed + fill: (matchingPoint.marker.fill || matchingPoint.color || (explodeSeries ? series.interior : '#ffffff')), + stroke: borderColor, + markerShadow: markerShadow, + currentRadius: markerRadius, + animationState: 'appearing' as const + }; + setTimeout(() => startMarkerAnimation(marker.seriesIndex, true), 0); + + return updatedMarker; + } + if (marker.visible) { + setTimeout(() => startMarkerAnimation(marker.seriesIndex, false), 0); + } + return { ...marker, visible: false }; + }) + ); + // Set timeout for auto hiding only if not in touch mode + if (!isTouchModeRef.current) { + markerTimeoutRef.current = setTimeout(() => { + setTrackballMarkers((currentMarkers: TrackballMarker[]) => + currentMarkers.map((marker: TrackballMarker) => ({ ...marker, visible: false })) + ); + }, 2000); + } + } + // Only render if in rendering phase and tooltip is enabled + if (phase !== 'rendering' || !props.enable) { + return null; + } + + /** + * Renders an animated trackball marker as a JSX element. + * + * @param {TrackballMarker} marker - The `TrackballMarker` object containing visibility and animation state. + * @returns {JSX.Element} A JSX element representing the marker if it is visible or animating; otherwise, `null`. + */ + function renderAnimatedMarker(marker: TrackballMarker): JSX.Element | null { + if (!marker.visible && marker.currentRadius <= 0) { + return null; + } + + const radius: number = marker.currentRadius; + const targetRadius: number = (marker.size.width + marker.size.height) / 4; + const opacity: number = marker.visible ? + Math.min(1, radius / ((marker.size.width + marker.size.height) / 4)) : + Math.max(0, radius / ((marker.size.width + marker.size.height) / 4)); + const chart: Chart = layoutRef.current.chart as Chart; + const series: SeriesProperties = chart?.visibleSeries[marker.seriesIndex]; + const isBubbleSeries: boolean = series?.type === 'Bubble'; + if (marker.shape === 'Circle' || !marker.shape) { + return ( + + + + + + ); + } else { + const location: { x: number, y: number } = { x: marker.x, y: marker.y }; + const sizeRatio: number = radius / targetRadius; + const animatedWidth: number = marker.size.width * sizeRatio; + const animatedHeight: number = marker.size.height * sizeRatio; + // Create shape with animated size + const shapeOption: { + id: string; + fill: string; + strokeWidth: number; + stroke: string; + opacity: number; + strokeDasharray: string; + d: string; + } = { + id: `trackball-marker-${marker.seriesIndex}`, + fill: marker.fill, + strokeWidth: marker.border.width, + stroke: marker.stroke, + opacity: 1, + strokeDasharray: '', + d: '' + }; + + const markerOptions: Object | null = drawSymbol( + location, + marker.shape, + { width: animatedWidth, height: animatedHeight }, + marker.imageUrl || '', + shapeOption + ); + + // Create shadow with slightly larger animated size + const shadowShapeOption: { + id: string; + fill: string; + strokeWidth: number; + stroke: string; + opacity: number; + strokeDasharray: string; + d: string; + } = { + id: `trackball-shadow-${marker.seriesIndex}`, + fill: 'transparent', + strokeWidth: marker.border.width + 6, + stroke: marker.markerShadow, + opacity: 1, + strokeDasharray: '', + d: '' + }; + + const shadowOptions: Object | null = drawSymbol( + location, + marker.shape, + { + width: animatedWidth + 6, + height: animatedHeight + 6 + }, + marker.imageUrl || '', + shadowShapeOption + ); + const shadowPath: { + d: string; + fill: string; + } = shadowOptions as { + d: string; + fill: string; + }; + const options: PathOptions = markerOptions as PathOptions; + return ( + + + + {/* Main marker with animated size */} + + + ); + } + } + return ( + + {trackballMarkers.map((marker: TrackballMarker) => renderAnimatedMarker(marker))} + + ); + }); +export default TrackballRenderer; diff --git a/components/charts/src/chart/renderer/Zooming/zoom-toolbar.tsx b/components/charts/src/chart/renderer/Zooming/zoom-toolbar.tsx new file mode 100644 index 0000000..bf4db22 --- /dev/null +++ b/components/charts/src/chart/renderer/Zooming/zoom-toolbar.tsx @@ -0,0 +1,1098 @@ +import { JSX, useEffect, useState, useMemo } from 'react'; +import { ChartLocationProps, ChartZoomSettingsProps, ToolbarPosition } from '../../base/interfaces'; +import { useLayout } from '../../layout/LayoutContext'; +import { applyZoomToolkit, reset, togglePan, zoomIn, zoomOut } from './zooming'; +import { useZoomToolkitVisibility, useRegisterZoomToolkitVisibility } from '../../hooks/useClipRect'; +import { measureText } from '../../utils/helper'; +import * as React from 'react'; +import { Browser, IL10n, L10n } from '@syncfusion/react-base'; +import { BaseZoom, Chart, Rect } from '../../chart-area/chart-interfaces'; +import { ToolbarItems } from '../../base/enum'; + +/** + * Interface representing the state of the zoom tooltip + * + * @interface ZoomTooltipState + * @public + */ +interface ZoomTooltipState { + /** + * Indicates whether the tooltip is currently visible + * + * @type {boolean} + */ + visible: boolean; + + /** + * The text content to display in the tooltip + * + * @type {string} + */ + text: string; + + /** + * The y-coordinate position of the tooltip + * + * @type {number} + */ + top: number; + + /** + * The x-coordinate position of the tooltip + * + * @type {number} + */ + left: number; +} + +/** + * Type definition for toolbar item references + * + * @interface ToolbarRefs + */ +interface ToolbarRefs { + rect: React.RefObject; + mainIcon: React.RefObject; + secondaryIcon: React.RefObject; +} + +/** + * Configuration interface for icon rendering + * + * @interface IconRenderConfig + */ +interface IconRenderConfig { + /** The unique identifier suffix for this icon */ + idSuffix: string; + /** The path d attribute for the main icon */ + pathD: string; + /** The polygon points for the icon (if applicable) */ + polygonPoints?: string; + /** The fill color for the icon */ + fillColor: string; + /** The background fill color for the icon rectangle */ + rectFill: string; +} + +/** + * Global map storing zoom tooltip states for different charts identified by their IDs + * Each chart has its own tooltip state to handle multiple charts on same page + * + * @type {Object.} + * @private + */ +export const zoomTooltipStates: {[chartId: string]: ZoomTooltipState} = {}; +/** + * Global map storing listener functions for zoom tooltip state changes + * Enables push-based notification when tooltip state changes + * + * @type {Object.>} + * @private + */ +export const zoomTooltipListeners: {[chartId: string]: ((state: ZoomTooltipState) => void)[]} = {}; + +/** + * Creates and returns a function to update zoom tooltip state for a specific chart + * + * @returns {Function} A function that takes ZoomTooltipState and optional chartId to update the tooltip state + * @example + * ```typescript + * const updateTooltip = useRegisterZoomTooltipState(); + * updateTooltip({ visible: true, text: "Zoom In", top: 100, left: 200 }, "chart1"); + * ``` + * + * @private + */ +export const useRegisterZoomTooltipState: () => ((state: ZoomTooltipState, chartId?: string) => void) = (): ((state: ZoomTooltipState, + chartId?: string) => void) => { + return (state: ZoomTooltipState, chartId?: string) => { + const id: string = chartId as string; + + if (!zoomTooltipStates[id as string]) { + zoomTooltipStates[id as string] = { + visible: false, + text: '', + top: 0, + left: 0 + }; + } + + zoomTooltipStates[id as string] = state; + + if (zoomTooltipListeners[id as string]) { + zoomTooltipListeners[id as string].forEach((fn: (state: ZoomTooltipState) => void) => fn(zoomTooltipStates[id as string])); + } + }; +}; + +/** + * Hook to use zoomTooltip state for a specific chart + * + * @param {string} [chartId] - Optional chart ID to identify the specific chart + * @returns {ZoomTooltipState} The current zoomTooltip state for the chart + * @private + */ +export const useZoomTooltipState: (chartId?: string) => ZoomTooltipState = (chartId?: string): ZoomTooltipState => { + const id: string = chartId as string; + + if (!zoomTooltipStates[id as string]) { + zoomTooltipStates[id as string] = { + visible: false, + text: '', + top: 0, + left: 0 + }; + } + + if (!zoomTooltipListeners[id as string]) { + zoomTooltipListeners[id as string] = []; + } + + const [state, setState] = useState(zoomTooltipStates[id as string]); + + useEffect(() => { + zoomTooltipListeners[id as string].push(setState); + return () => { + void ((zoomTooltipListeners[id as string]) && ( + zoomTooltipListeners[id as string] = zoomTooltipListeners[id as string].filter( + (fn: (state: ZoomTooltipState) => void) => fn !== setState + )) + ); + }; + }, [id as string]); + + return state; +}; + +/** + * ZoomToolkit component for rendering zooming controls in a chart + * + * @param {ChartZoomSettingsProps} props - The zoom settings props + * @returns {JSX.Element | null} The rendered zoom toolkit or null if not visible + * @public + */ +export const ZoomToolkit: React.FC = (props: ChartZoomSettingsProps) => { + const { layoutRef, phase } = useLayout(); + const [toolkitVisible, setToolkitVisible] = useState(false); + const toolkitVersion: number = useZoomToolkitVisibility(); + let chart: Chart = layoutRef.current.chart as Chart; + const zoom: BaseZoom = layoutRef.current?.chartZoom as BaseZoom; + const chartId: string = chart?.element?.id; + const zoomTooltipState: ZoomTooltipState = useZoomTooltipState(chartId); + const setZoomTooltipState: ((state: ZoomTooltipState, chartId?: string) => void) = useRegisterZoomTooltipState(); + + /** + * Clear zoomTooltip when zooming state changes + */ + useEffect(() => { + if (!zoom?.isZoomed) { + setZoomTooltipState({ + visible: false, + text: '', + top: 0, + left: 0 + }, chartId); + } + }, [zoom?.isZoomed, chartId]); + + useEffect(() => { + if (phase === 'measuring' && layoutRef.current?.chart && zoom) { + chart = layoutRef.current.chart as Chart; + const shouldShowToolkit: boolean = applyZoomToolkit(chart, chart.axisCollection, zoom); + setToolkitVisible(shouldShowToolkit); + } + }, [phase, props.toolbar?.visible]); + + /** + * Memoized calculation of the zoom toolkit JSX element + * Recalculates only when relevant dependencies change to optimize performance + * + * @returns {JSX.Element | null} The rendered zoom toolkit or null if it shouldn't be displayed + */ + const zoomToolkit: JSX.Element | null = useMemo(() => { + // Cache the expensive calculations + const shouldShowToolkit: boolean = toolkitVisible || props.toolbar?.visible as boolean; + const canRender: boolean = (phase === 'rendering' && chart && zoom) as boolean; + + if (canRender && (shouldShowToolkit || applyZoomToolkit(chart, chart.axisCollection, zoom))) { + void ((layoutRef.current?.chart) && ((layoutRef.current?.chart as Chart).zoomSettings = props)); + return renderZoomingToolkit(chart, zoom, setZoomTooltipState); + } + return null; + }, [toolkitVersion, toolkitVisible, phase, props.toolbar?.position, chart, zoom]); + + /** + * Renders a custom SVG tooltip + * + * @returns {JSX.Element | null} The rendered tooltip or null if not visible + */ + const renderTooltip: () => JSX.Element | null = (): JSX.Element | null => { + if (!zoomTooltipState || !zoomTooltipState.visible || Browser.isDevice) { + return null; + } + const enableRTL: boolean = chart.enableRtl; + + const textWidth: number = measureText(zoomTooltipState.text, { + fontSize: '10px', + color: '', + fontStyle: '', + fontFamily: '', + fontWeight: '' + }, { + fontSize: '10px', + fontStyle: 'Normal', + fontWeight: '400', + fontFamily: 'Segoe UI', + color: '' + }).width; + + const paddedWidth: number = textWidth + 12; // Add padding + const height: number = 22; + const anchor: string = enableRTL ? 'end' : 'start'; + + return ( + + + + {zoomTooltipState.text} + + + ); + }; + + return (phase === 'rendering') && ( + <> + {zoomToolkit} + {renderTooltip()} + + ); +}; + +/** + * Creates and shows the zooming toolkit + * + * @param {Chart} chart - The chart instance + * @param {BaseZoom} zoom - The zoom controller + * @param {Function} setZoomTooltipState - Function to update zoomTooltip state + * @returns {JSX.Element | null} The rendered zoom toolkit or null + * @private + */ +function renderZoomingToolkit( + chart: Chart, + zoom: BaseZoom, + setZoomTooltipState: (state: ZoomTooltipState, chartId?: string) => void +): JSX.Element | null { + const areaBounds: Rect = chart.chartAxislayout.seriesClipRect; + if ( zoom.toolbar && chart.zoomSettings.toolbar && !zoom.toolbar.items) { + zoom.toolbar.items = chart.zoomSettings.toolbar.items = ['ZoomIn', 'ZoomOut', 'Pan', 'Reset']; + } + const position: ChartLocationProps = calculateToolbarPosition(chart, areaBounds); + let toolboxItems: ToolbarItems[] = chart.zoomSettings.toolbar?.items as ToolbarItems[]; + toolboxItems = Browser.isDevice && toolboxItems.length > 0 ? ['Reset'] : toolboxItems; + + if (toolboxItems.length === 0) { + return null; + } + const handleToolbarMouseEnter: () => void = () => { + if (chart.tooltipRef && chart.tooltipRef.current) { + chart.tooltipRef.current?.fadeOut(); + } + if (chart.trackballRef && chart.trackballRef.current) { + const childElements: HTMLCollection = chart.trackballRef.current.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.display = 'none'; + } + } + } + chart.zoomRedraw = true; + }; + + const handleToolbarMouseLeave: () => void = () => { + chart.zoomRedraw = false; + }; + + return ( + + {renderToolkitShadowEffects(chart.element.id)} + {renderToolkitBackground(chart)} + {toolboxItems.map((item: ToolbarItems, index: number) => + renderToolbarItemGroup(item, index, chart, zoom, setZoomTooltipState) + )} + + ); +} + +/** + * Calculates the position for the toolbar + * + * @param {Chart} chart - The chart instance + * @param {Rect} areaBounds - The area bounds + * @returns {ChartLocationProps} The x and y coordinates for the toolbar + * @private + */ +function calculateToolbarPosition(chart: Chart, areaBounds: Rect): ChartLocationProps { + const spacing: number = 10; + if (chart.zoomSettings.toolbar && !chart.zoomSettings.toolbar.items) { + chart.zoomSettings.toolbar.items = ['ZoomIn', 'ZoomOut', 'Pan', 'Reset']; + } + const toolboxItems: ToolbarItems[] = chart.zoomSettings.toolbar?.items as ToolbarItems[]; + const length: number = Browser.isDevice ? (toolboxItems.length === 0 ? 0 : 1) : toolboxItems.length; + const iconSize: number = Browser.isDevice ? + measureText('Reset Zoom', { fontSize: '12px', color: '#000000', fontStyle: 'Normal', fontFamily: 'Segoe UI', fontWeight: '400' }, + { fontSize: '12px', fontStyle: 'Normal', color: '', fontWeight: '400', fontFamily: 'Segoe UI' }).width : 16; + const height: number = Browser.isDevice ? + measureText('Reset Zoom', { fontSize: '12px', color: '#000000', fontStyle: 'Normal', fontFamily: 'Segoe UI', fontWeight: '400' }, + { fontSize: '12px', fontStyle: 'Normal', color: '', fontWeight: '400', fontFamily: 'Segoe UI' }).height : 22; + const width: number = (length * iconSize) + ((length + 1) * spacing) + ((length - 1) * spacing); + const position: ToolbarPosition = chart.zoomSettings.toolbar?.position || + { hAlign: 'Right', vAlign: 'Top', x: 0, y: 0 }; + + let transX: number = 0; + let transY: number = 0; + + // Calculate X position based on horizontal alignment + switch (position.hAlign) { + case 'Right': + transX = areaBounds.x + areaBounds.width - width - spacing; + break; + case 'Left': + transX = areaBounds.x + spacing; + break; + default: + transX = (areaBounds.width / 2) - (width / 2) + areaBounds.x; + break; + } + transX += position.x || 0; + + // Calculate Y position based on vertical alignment + switch (position.vAlign) { + case 'Bottom': + transY = areaBounds.height - (areaBounds.y + height + spacing); + break; + case 'Top': + transY = areaBounds.y + spacing; + break; + default: + transY = (areaBounds.height / 2) - (height / 2) + areaBounds.y; + break; + } + transY += position.y || 0; + + return { x: transX, y: transY }; +} + +/** + * Renders the shadow effects for the toolbar + * + * @param {string} chartId - The ID of the chart + * @returns {JSX.Element} The rendered shadow effects + * @private + */ +function renderToolkitShadowEffects(chartId: string): JSX.Element { + const filterId: string = `${chartId}_chart_shadow`; + return ( + + + + + + + + + + + + + + ); +} + +/** + * Renders the background of the toolkit + * + * @param {Chart} chart - The chart instance + * @returns {JSX.Element} The rendered background + */ +function renderToolkitBackground(chart: Chart): JSX.Element { + const zoomFillColor: string = '#FFFFFF'; + const spacing: number = 10; + const toolboxItems: ToolbarItems[] = chart.zoomSettings.toolbar?.items as ToolbarItems[]; + const length: number = Browser.isDevice ? (toolboxItems.length === 0 ? 0 : 1) : toolboxItems.length; + const iconSize: number = Browser.isDevice ? + measureText('Reset Zoom', { fontSize: '12px', color: '#000000', fontStyle: 'Normal', fontFamily: 'Segoe UI', fontWeight: '400' }, + { fontSize: '12px', fontStyle: 'Normal', color: '', fontWeight: '400', fontFamily: 'Segoe UI' }).width : 16; + const height: number = Browser.isDevice ? + measureText('Reset Zoom', { fontSize: '12px', color: '#000000', fontStyle: 'Normal', fontFamily: 'Segoe UI', fontWeight: '400' }, + { fontSize: '12px', fontStyle: 'Normal', color: '', fontWeight: '400', fontFamily: 'Segoe UI' }).height : 22; + const width: number = (length * iconSize) + ((length + 1) * spacing) + ((length - 1) * spacing); + const filterId: string = `${chart.element.id}_chart_shadow`; + + return ( + <> + + + + ); +} + +/** + * Renders a toolbar item group + * + * @param {ToolbarItems} item - The toolbar item to render + * @param {number} index - The index of the toolbar item + * @param {Chart} chart - The chart instance + * @param {BaseZoom} zoom - The zoom controller + * @param {Function} setZoomTooltipState - Function to update zoom tooltip state + * @returns {JSX.Element} The rendered toolbar item group + */ +function renderToolbarItemGroup( + item: ToolbarItems, + index: number, + chart: Chart, + zoom: BaseZoom, + setZoomTooltipState: (state: ZoomTooltipState, chartId?: string) => void +): JSX.Element { + // Each icon should be placed with proper spacing + const xPosition: number = 10 + (index * 36); // 36px between icons + const yPosition: number = 10 + 3; // Adjusted position + const chartId: string = chart.element.id; + + // Create refs for this toolbar item + const rectRef: React.RefObject = React.createRef(); + const mainIconRef: React.RefObject = React.createRef(); + const secondaryIconRef: React.RefObject = React.createRef(); + const refs: { + rect: React.RefObject; + mainIcon: React.RefObject; + secondaryIcon: React.RefObject; + } = { + rect: rectRef as React.RefObject, + mainIcon: mainIconRef as React.RefObject, + secondaryIcon: secondaryIconRef as React.RefObject + }; + + // Event handlers for this toolbar item + const handleMouseOver: (e: React.MouseEvent) => void = (e: React.MouseEvent) => + handleZoomTooltipShow(e, item, refs, setZoomTooltipState, chartId, chart); + const handleMouseOut: () => void = () => handleZoomTooltipHide(refs, zoom, chart); + const handleClick: (e: React.MouseEvent) => void = (e: React.MouseEvent) => { + // Then process the normal click event + handleToolbarClick(item, chart, zoom)(e); + }; + + return ( + + {renderToolbarItemIcon(item, chart, zoom, refs)} + + ); +} + +/** + * Handles showing the zoom tooltip + * + * @param {React.MouseEvent} e - The mouse event + * @param {ToolbarItems} item - The toolbar item + * @param {Object} refs - References to SVG elements + * @param {React.RefObject} refs.rect - Reference to the rectangle SVG element + * @param {React.RefObject} refs.mainIcon - Reference to the path SVG element + * @param {React.RefObject} refs.secondaryIcon - Reference to the polygon SVG element + * @param {Function} setZoomTooltipState - Function to update zoom tooltip state + * @param {string} chartId - The chart ID + * @param {Chart} chart - The chart instance + * @returns {void} This function doesn't return a value + * @private + */ +export function handleZoomTooltipShow( + e: React.MouseEvent, + item: ToolbarItems, + refs: ToolbarRefs, + setZoomTooltipState: (state: ZoomTooltipState, chartId?: string) => void, + chartId: string, + chart: Chart +): void { + const text: string = getZoomTooltipText(item, chart.locale); + const textWidth: number = measureText(text, { + fontSize: '10px', + color: '', + fontStyle: '', + fontFamily: '', + fontWeight: '' + }, { + fontSize: '10px', fontStyle: 'Normal', fontWeight: '400', fontFamily: 'Segoe UI', + color: '' + }).width; + + const chartRect: DOMRect = getChartRect(e.currentTarget as Element); + chart.zoomRedraw = true; + if (chart.tooltipRef && chart.tooltipRef.current) { + chart.tooltipRef.current?.fadeOut(); + } + if (chart.trackballRef && chart.trackballRef.current) { + const childElements: HTMLCollection = chart.trackballRef.current.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.display = 'none'; + } + } + } + const left: number = e.clientX - chartRect.left - (textWidth + 5); + const top: number = e.clientY - chartRect.top + 20; + + // Show zoomTooltip + setZoomTooltipState({ + visible: true, + text, + top, + left + }, chartId); + + // Apply hover styles using refs + void ((!Browser.isDevice) && (applyHoverStyles(refs))); +} + +/** + * Safely retrieves the bounding client rectangle of a chart element + * Handles null checks and provides fallback when elements can't be found + * + * @param {Element} element - The DOM element to get bounds from + * @returns {DOMRect} The bounding client rectangle or a default empty rectangle + */ +function getChartRect(element: Element): DOMRect { + try { + const svgElement: SVGSVGElement = element.closest('svg') as SVGSVGElement; + if (!svgElement || !svgElement.parentElement) { + return new DOMRect(0, 0, 0, 0); + } + return svgElement.parentElement.getBoundingClientRect(); + } catch (error) { + console.warn('Failed to get chart bounds:', error); + return new DOMRect(0, 0, 0, 0); + } +} + +/** + * Applies hover styles to toolbar item references + * + * @param {ToolbarRefs} refs - The references to SVG elements + * @returns {void} + */ +function applyHoverStyles(refs: ToolbarRefs): void { + // Apply hover styles using refs + void (refs.rect.current && refs.rect.current.setAttribute('fill', '#f5f5f5')); + + void (refs.mainIcon.current && refs.mainIcon.current.setAttribute('fill', '#0075ff')); + + void (refs.secondaryIcon.current && refs.secondaryIcon.current.setAttribute('fill', '#0075ff')); +} + +/** + * Handles hiding the zoom tooltip + * + * @param {Object} refs - Object containing references to SVG elements + * @param {React.RefObject} refs.rect - Reference to the rectangle SVG element + * @param {React.RefObject} refs.mainIcon - Reference to the path SVG element + * @param {React.RefObject} refs.secondaryIcon - Reference to the polygon SVG element + * @param {BaseZoom} zoom - The zoom controller + * @param {Chart} chart - The chart instance + * @returns {void} This function doesn't return a value + */ +function handleZoomTooltipHide( + refs: { + rect: React.RefObject; + mainIcon: React.RefObject; + secondaryIcon: React.RefObject; + }, + zoom: BaseZoom, + chart: Chart +): void { + // Get chart ID + const chartId: string = chart?.element?.id; + + // Directly call the tooltip state updater to hide the tooltip + // This is the key fix - make sure we use the proper function to update state + if (zoomTooltipListeners[chartId as string]) { + const updatedState: ZoomTooltipState = { + visible: false, + text: '', + top: 0, + left: 0 + }; + + // Update the stored state + zoomTooltipStates[chartId as string] = updatedState; + + // Notify all listeners for this specific chart + zoomTooltipListeners[chartId as string].forEach((fn: (state: ZoomTooltipState) => void) => { + fn(updatedState); + }); + } + + // Rest of the function for updating element styles... + if (refs.rect.current) { + const id: string = refs.rect.current.id; + const isPanning: boolean = zoom.isPanning as boolean; + const isPanButton: boolean = id.indexOf('_Pan_') > -1; + + let rectColor: string = 'transparent'; + if (isPanning && isPanButton) { + rectColor = chart.themeStyle?.toolkitIconRectSelectionFill; + } + + refs.rect.current.setAttribute('fill', rectColor); + } + + // Reset icon colors using refs + if (refs.mainIcon.current) { + const id: string = refs.mainIcon.current.id; + let iconColor: string = chart.themeStyle?.toolkitFill; + + if (zoom.isPanning && id.indexOf('_Pan_') > -1) { + iconColor = chart.themeStyle?.toolkitSelectionColor; + } + + refs.mainIcon.current.setAttribute('fill', iconColor); + } + const iconColor: string = chart.themeStyle?.toolkitFill; + void ((refs.secondaryIcon.current) && refs.secondaryIcon.current.setAttribute('fill', iconColor)); +} + +/** + * Returns a click handler for toolbar items + * + * @param {ToolbarItems} action - The toolbar item action + * @param {Chart} chart - The chart instance + * @param {BaseZoom} zoom - The zoom controller + * @returns {Function} The click handler + */ +function handleToolbarClick(action: ToolbarItems, chart: Chart, zoom: BaseZoom): (e: React.MouseEvent) => void { + return (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const setToolkitVisible: (visible: boolean) => void = useRegisterZoomToolkitVisibility(); + + switch (action) { + case 'Pan': + togglePan(chart, zoom); + break; + case 'ZoomIn': + zoomIn(chart); + break; + case 'ZoomOut': + zoomOut(chart); + break; + case 'Reset': + reset(chart, zoom); + break; + } + + const shouldShowToolkit: boolean = applyZoomToolkit(chart, chart.axisCollection, zoom); + setToolkitVisible(shouldShowToolkit); + }; +} + +/** + * Returns zoom tooltip text for chart toolbar items based on locale. + * + * @param {ToolbarItems} item - The toolbar item key (e.g., 'ZoomIn', 'ZoomOut', 'Reset', 'Pan'). + * @param {string} locale - The locale code (e.g., 'fr-CH', 'ar', 'zh-CN', 'de-DE'). + * @returns {string} - The localized tooltip text for the given toolbar item. + * + * If the provided locale is not supported, it defaults to English ('en-US'). + */ +function getZoomTooltipText(item: ToolbarItems, locale: string): string { + + const defaultChartLocale: { + zoom: string; + zoomIn: string; + zoomOut: string; + reset: string; + pan: string; + resetZoom: string; + } = { + zoom: 'Zoom', + zoomIn: 'Zoom In', + zoomOut: 'Zoom Out', + reset: 'Reset', + pan: 'Pan', + resetZoom: 'Reset Zoom' + }; + + const localeObj: IL10n = L10n('chart', defaultChartLocale, locale); + return localeObj.getConstant(item.charAt(0).toLowerCase() + item.slice(1)); +} + +/** + * Renders the icon for a toolbar item + * + * @param {ToolbarItems} item - The toolbar item to render + * @param {Chart} chart - The chart instance + * @param {BaseZoom} zoom - The zoom controller + * @param {Object} refs - Object containing refs for SVG elements + * @param {React.RefObject} refs.rect - Reference to the rectangle SVG element + * @param {React.RefObject} refs.mainIcon - Reference to the path SVG element + * @param {React.RefObject} refs.secondaryIcon - Reference to the polygon SVG element + * @returns {JSX.Element} The rendered toolbar item icon + */ +function renderToolbarItemIcon( + item: ToolbarItems, + chart: Chart, + zoom: BaseZoom, + refs: { + rect: React.RefObject, + mainIcon: React.RefObject, + secondaryIcon: React.RefObject + } +): JSX.Element { + const iconSize: number = 32; + + switch (item) { + case 'ZoomIn': + return renderZoomInIcon(chart, refs, iconSize); + case 'ZoomOut': + return renderZoomOutIcon(chart, refs, iconSize); + case 'Pan': + return renderPanIcon(chart, zoom, refs, iconSize); + case 'Reset': + default: + return renderResetIcon(chart, refs, iconSize, Browser.isDevice, zoom); + } +} + +/** + * Renders the ZoomIn icon + * + * @param {Chart} chart - The chart instance + * @param {Object} refs - Object containing refs for SVG elements + * @param {number} iconSize - The size of the icon + * @returns {JSX.Element} The rendered icon + */ +function renderZoomInIcon( + chart: Chart, + refs: { + rect: React.RefObject, + mainIcon: React.RefObject, + secondaryIcon: React.RefObject + }, + iconSize: number +): JSX.Element { + const fillColor: string = chart.themeStyle?.toolkitFill; + + const config: IconRenderConfig = { + idSuffix: 'ZoomIn', + pathD: 'M5.29297 3.28906H6.10156V5.29297H8.12305V6.10156H6.10156V8.12305H5.29297V6.10156H3.28906V5.29297H5.29297V3.28906ZM5.69727 1.67188L5.50391 1.68945H5.29297L4.88867 1.75977L4.50195 1.86523L4.13281 1.98828L3.78125 2.16406L3.44727 2.35742L3.14844 2.60352L2.84961 2.84961L2.60352 3.14844L2.35742 3.44727L2.16406 3.78125L1.98828 4.13281L1.86523 4.50195L1.75977 4.88867L1.68945 5.29297L1.67188 5.69727L1.68945 6.11914L1.75977 6.50586L1.86523 6.89258L1.98828 7.26172L2.16406 7.61328L2.35742 7.94727L2.60352 8.26367L2.84961 8.54492L3.14844 8.80859L3.44727 9.03711L3.78125 9.24805L4.13281 9.40625L4.50195 9.54688L4.88867 9.65234L5.29297 9.70508L5.69727 9.72266L6.11914 9.70508L6.50586 9.65234L6.89258 9.54688L7.26172 9.40625L7.61328 9.24805L7.94727 9.03711L8.26367 8.80859L8.54492 8.54492L8.80859 8.26367L9.03711 7.94727L9.24805 7.61328L9.40625 7.26172L9.54688 6.89258L9.65234 6.50586L9.70508 6.11914L9.72266 5.69727L9.70508 5.29297L9.65234 4.88867L9.54688 4.50195L9.40625 4.13281L9.24805 3.78125L9.03711 3.44727L8.80859 3.14844L8.54492 2.84961L8.26367 2.60352L7.94727 2.35742L7.61328 2.16406L7.26172 1.98828L6.89258 1.86523L6.50586 1.75977L6.11914 1.68945L5.69727 1.67188ZM5.69727 0.0722656H5.99609L6.27734 0.0898438L6.55859 0.125L6.83984 0.177734L7.10352 0.248047L7.38477 0.318359L7.63086 0.40625L7.89453 0.511719L8.14062 0.617188L8.38672 0.740234L8.61523 0.880859L8.86133 1.02148L9.07227 1.17969L9.2832 1.35547L9.49414 1.53125L9.6875 1.72461L9.88086 1.91797L10.0566 2.11133L10.2148 2.33984L10.373 2.55078L10.5312 2.7793L10.6543 3.02539L10.7773 3.25391L10.9004 3.51758L11.0059 3.76367L11.0938 4.02734L11.1641 4.29102L11.2168 4.57227L11.2695 4.83594L11.3047 5.13477L11.3398 5.41602V5.69727V5.96094L11.3223 6.22461L11.2871 6.48828L11.252 6.73438L11.1992 6.98047L11.1289 7.22656L11.0586 7.47266L10.9707 7.70117L10.8828 7.92969L10.7773 8.1582L10.6543 8.38672L10.5488 8.59766L10.4082 8.80859L10.2676 9.00195L10.127 9.19531L9.96875 9.38867L10.4258 9.8457L11.1113 9.96875L15.9453 14.8027L14.8027 15.9277L9.96875 11.1113L9.8457 10.4258L9.38867 9.96875L9.19531 10.127L9.00195 10.2676L8.80859 10.4082L8.59766 10.5488L8.38672 10.6543L8.1582 10.7773L7.92969 10.8828L7.70117 10.9707L7.47266 11.0586L7.22656 11.1289L6.98047 11.1992L6.73438 11.252L6.48828 11.2871L6.22461 11.3223L5.96094 11.3398H5.69727H5.41602L5.13477 11.3047L4.85352 11.2695L4.57227 11.2344L4.29102 11.1641L4.02734 11.0938L3.76367 11.0059L3.51758 10.9004L3.25391 10.7773L3.02539 10.6543L2.7793 10.5312L2.55078 10.373L2.32227 10.2148L2.11133 10.0566L1.91797 9.88086L1.72461 9.6875L1.53125 9.49414L1.35547 9.2832L1.17969 9.07227L1.02148 8.86133L0.880859 8.63281L0.740234 8.38672L0.617188 8.14062L0.511719 7.89453L0.40625 7.63086L0.318359 7.38477L0.248047 7.10352L0.177734 6.83984L0.125 6.55859L0.0898438 6.27734L0.0722656 5.99609L0.0546875 5.69727L0.0722656 5.41602L0.0898438 5.13477L0.125 4.85352L0.177734 4.57227L0.248047 4.29102L0.318359 4.02734L0.40625 3.76367L0.511719 3.51758L0.617188 3.25391L0.740234 3.02539L0.880859 2.7793L1.02148 2.55078L1.17969 2.33984L1.35547 2.11133L1.53125 1.91797L1.72461 1.72461L1.91797 1.53125L2.11133 1.35547L2.32227 1.17969L2.55078 1.02148L2.7793 0.880859L3.02539 0.740234L3.25391 0.617188L3.51758 0.511719L3.76367 0.40625L4.02734 0.318359L4.29102 0.248047L4.57227 0.177734L4.85352 0.125L5.13477 0.0898438L5.41602 0.0722656H5.69727Z', + fillColor, + rectFill: 'transparent' + }; + + return renderToolbarIcon(chart, refs, iconSize, config); +} + +/** + * Renders the ZoomOut icon + * + * @param {Chart} chart - The chart instance + * @param {Object} refs - Object containing refs for SVG elements + * @param {number} iconSize - The size of the icon + * @returns {JSX.Element} The rendered icon + */ +function renderZoomOutIcon( + chart: Chart, + refs: { + rect: React.RefObject, + mainIcon: React.RefObject, + secondaryIcon: React.RefObject + }, + iconSize: number +): JSX.Element { + const fillColor: string = chart.themeStyle?.toolkitFill; + + const config: IconRenderConfig = { + idSuffix: 'ZoomOut', + pathD: 'M3.28906 5.29297H8.12305V6.10156H3.28906V5.29297ZM5.69727 1.67188L5.50391 1.68945H5.29297L4.88867 1.75977L4.50195 1.86523L4.13281 1.98828L3.78125 2.16406L3.44727 2.35742L3.14844 2.60352L2.84961 2.84961L2.60352 3.14844L2.35742 3.44727L2.16406 3.78125L1.98828 4.13281L1.86523 4.50195L1.75977 4.88867L1.68945 5.29297L1.67188 5.69727L1.68945 6.11914L1.75977 6.50586L1.86523 6.89258L1.98828 7.26172L2.16406 7.61328L2.35742 7.94727L2.60352 8.26367L2.84961 8.54492L3.14844 8.80859L3.44727 9.03711L3.78125 9.24805L4.13281 9.40625L4.50195 9.54688L4.88867 9.65234L5.29297 9.70508L5.69727 9.72266L6.11914 9.70508L6.50586 9.65234L6.89258 9.54688L7.26172 9.40625L7.61328 9.24805L7.94727 9.03711L8.26367 8.80859L8.54492 8.54492L8.80859 8.26367L9.03711 7.94727L9.24805 7.61328L9.40625 7.26172L9.54688 6.89258L9.65234 6.50586L9.70508 6.11914L9.72266 5.69727L9.70508 5.29297L9.65234 4.88867L9.54688 4.50195L9.40625 4.13281L9.24805 3.78125L9.03711 3.44727L8.80859 3.14844L8.54492 2.84961L8.26367 2.60352L7.94727 2.35742L7.61328 2.16406L7.26172 1.98828L6.89258 1.86523L6.50586 1.75977L6.11914 1.68945L5.69727 1.67188ZM5.69727 0.0722656H5.99609L6.27734 0.0898438L6.55859 0.125L6.83984 0.177734L7.10352 0.248047L7.38477 0.318359L7.63086 0.40625L7.89453 0.511719L8.14062 0.617188L8.38672 0.740234L8.61523 0.880859L8.84375 1.02148L9.07227 1.17969L9.2832 1.35547L9.49414 1.53125L9.6875 1.72461L9.88086 1.91797L10.0566 2.11133L10.2148 2.33984L10.373 2.55078L10.5137 2.7793L10.6543 3.02539L10.7773 3.25391L10.9004 3.51758L11.0059 3.76367L11.0938 4.02734L11.1641 4.29102L11.2168 4.57227L11.2695 4.83594L11.3047 5.13477L11.3398 5.41602V5.69727V5.96094L11.3223 6.22461L11.2871 6.48828L11.252 6.73438L11.1992 6.98047L11.1289 7.22656L11.0586 7.47266L10.9707 7.70117L10.8828 7.92969L10.7773 8.1582L10.6543 8.38672L10.5488 8.59766L10.4082 8.80859L10.2676 9.00195L10.127 9.19531L9.96875 9.38867L10.4258 9.8457L11.1113 9.96875L15.9453 14.8027L14.8027 15.9277L9.96875 11.1113L9.8457 10.4258L9.38867 9.96875L9.00195 10.2676L8.80859 10.4082L8.59766 10.5488L8.38672 10.6543L8.1582 10.7773L7.92969 10.8828L7.70117 10.9707L7.47266 11.0586L7.22656 11.1289L6.98047 11.1992L6.73438 11.252L6.48828 11.2871L6.22461 11.3223L5.96094 11.3398H5.69727H5.41602L5.13477 11.3047L4.85352 11.2695L4.57227 11.2344L4.29102 11.1641L4.02734 11.0938L3.76367 11.0059L3.51758 10.9004L3.25391 10.7773L3.02539 10.6543L2.7793 10.5312L2.55078 10.373L2.32227 10.2148L2.11133 10.0566L1.91797 9.88086L1.72461 9.6875L1.53125 9.49414L1.35547 9.2832L1.17969 9.07227L1.02148 8.86133L0.880859 8.63281L0.740234 8.38672L0.617188 8.14062L0.511719 7.89453L0.40625 7.63086L0.318359 7.38477L0.248047 7.10352L0.177734 6.83984L0.125 6.55859L0.0898438 6.27734L0.0722656 5.99609L0.0546875 5.69727L0.0722656 5.41602L0.0898438 5.13477L0.125 4.85352L0.177734 4.57227L0.248047 4.29102L0.318359 4.02734L0.40625 3.76367L0.511719 3.51758L0.617188 3.25391L0.740234 3.02539L0.880859 2.7793L1.02148 2.55078L1.17969 2.33984L1.35547 2.11133L1.53125 1.91797L1.72461 1.72461L1.91797 1.53125L2.11133 1.35547L2.32227 1.17969L2.55078 1.02148L2.7793 0.880859L3.02539 0.740234L3.25391 0.617188L3.51758 0.511719L3.76367 0.40625L4.02734 0.318359L4.29102 0.248047L4.57227 0.177734L4.85352 0.125L5.13477 0.0898438L5.41602 0.0722656H5.69727Z', + fillColor, + rectFill: 'transparent' + }; + + return renderToolbarIcon(chart, refs, iconSize, config); +} + +/** + * Renders the Pan icon + * + * @param {Chart} chart - The chart instance + * @param {BaseZoom} zoom - The zoom controller + * @param {Object} refs - Object containing refs for SVG elements + * @param {React.RefObject} refs.rect - Reference to the rectangle SVG element + * @param {React.RefObject} refs.mainIcon - Reference to the path SVG element + * @param {React.RefObject} refs.secondaryIcon - Reference to the polygon SVG element + * @param {number} iconSize - The size of the icon + * @returns {JSX.Element} The rendered icon + */ +function renderPanIcon( + chart: Chart, + zoom: BaseZoom, + refs: { + rect: React.RefObject, + mainIcon: React.RefObject, + secondaryIcon: React.RefObject + }, + iconSize: number +): JSX.Element { + // Set fill color based on isPanning state + const fillColor: string = zoom.isPanning ? + chart.themeStyle?.toolkitSelectionColor : + chart.themeStyle?.toolkitFill; + + const rectFill: string = zoom.isPanning ? + chart.themeStyle?.toolkitIconRectSelectionFill : + 'transparent'; + + return ( + <> + + + + + ); +} + +/** + * Renders the Reset icon + * + * @param {Chart} chart - The chart instance + * @param {Object} refs - Object containing refs for SVG elements + * @param {React.RefObject} refs.rect - Reference to the rectangle SVG element + * @param {React.RefObject} refs.mainIcon - Reference to the path SVG element + * @param {React.RefObject} refs.secondaryIcon - Reference to the polygon SVG element + * @param {number} iconSize - The size of the icon + * @param {boolean} isDevice - to check the Browser is Device for mobile mode + * @param {BaseZoom} zoom - The zoom controller + * + * @returns {JSX.Element} The rendered icon + */ +function renderResetIcon( + chart: Chart, + refs: { + rect: React.RefObject, + mainIcon: React.RefObject, + secondaryIcon: React.RefObject + }, + iconSize: number, + isDevice: boolean, + zoom: BaseZoom +): JSX.Element { + const fillColor: string = chart.themeStyle?.toolkitFill; + const elementOpacity: string = !zoom.isZoomed && zoom.toolbar?.visible ? '0.2' : '1'; + if (isDevice) { + return ( + <> + + + + ); + } + return ( + <> + + + + + ); +} + +/** + * Renders a toolbar icon based on provided configuration + * + * @param {Chart} chart - The chart instance + * @param {Object} refs - Object containing refs for SVG elements + * @param {React.RefObject} refs.rect - Reference to the rectangle SVG element + * @param {React.RefObject} refs.mainIcon - Reference to the path SVG element + * @param {React.RefObject} refs.secondaryIcon - Reference to the polygon SVG element + * @param {number} iconSize - The size of the icon + * @param {IconRenderConfig} config - The icon configuration + * @returns {JSX.Element} The rendered icon + * + * @private + */ +export function renderToolbarIcon( + chart: Chart, + refs: { + rect: React.RefObject, + mainIcon: React.RefObject, + secondaryIcon: React.RefObject + }, + iconSize: number, + config: IconRenderConfig +): JSX.Element { + const { idSuffix, pathD, polygonPoints, fillColor, rectFill } = config; + + return ( + <> + + + {polygonPoints ? ( + + ) : ( + + )} + + ); +} diff --git a/components/charts/src/chart/renderer/Zooming/zooming.tsx b/components/charts/src/chart/renderer/Zooming/zooming.tsx new file mode 100644 index 0000000..45de52c --- /dev/null +++ b/components/charts/src/chart/renderer/Zooming/zooming.tsx @@ -0,0 +1,1457 @@ +import { JSX, useEffect, useLayoutEffect, useMemo, useState } from 'react'; +import { ChartLocationProps, ChartZoomSettingsProps, ZoomEndEvent, ZoomStartEvent } from '../../base/interfaces'; +import { getRectLocation, minMax, withInBounds } from '../../utils/helper'; +import { registerChartEventHandler, registerZoomRectSetter, useRegisterAxisRender, useRegisterSeriesRender, useRegisterZoomToolkitVisibility } from '../../hooks/useClipRect'; +import { useLayout } from '../../layout/LayoutContext'; +import { Browser, extend } from '@syncfusion/react-base'; +import { AxisModel, BaseZoom, Chart, AxisDataProps, ITouches, IZoomAxisRange, Rect, VisibleRangeProps } from '../../chart-area/chart-interfaces'; +import { ZoomMode } from '../../base/enum'; + +let zoom: BaseZoom; + +/** + * Options for performing zoom redraw operations + * + * @interface ZoomRedrawOptions + */ +interface ZoomRedrawOptions { + /** + * The zooming rectangl + */ + rect?: Rect; + + /** + * Whether the chart is currently zoomed + * + * @default false + */ + zoomed?: boolean; + + /** + * Whether panning is active + * + * @default false + */ + panning?: boolean; + + /** + * Whether UI interactions have been performed + * + * @default false + */ + performedUI?: boolean; + + /** + * Whether panning has started + * + * @default false + */ + startPanning?: boolean; +} + +/** + * ZoomContent component that manages the zooming functionality for charts + * + * This component handles chart zooming interactions including mouse/touch selection, + * mouse wheel zooming, pinch gestures, and panning. It renders the zoom selection + * rectangle and coordinates with the chart's axes for proper zoom behavior. + * + * @component + * @param {ChartZoomSettingsProps} props - The zoom settings configuration + * @param {boolean} [props.selectionZoom=false] - Enables zooming by selecting an area + * @param {boolean} [props.mouseWheelZoom=false] - Enables zooming using mouse wheel + * @param {boolean} [props.pinchZoom=false] - Enables zooming with touch pinch gestures + * @param {boolean} [props.pan=false] - Enables panning of the zoomed chart + * @param {ZoomMode} [props.mode='XY'] - Determines zoom direction ('X', 'Y', or 'XY') + * @param {ToolbarItems[]} [props.items=[]] - Toolbar items to display for zoom control + * @param {boolean} [props.toolbar=false] - Whether to always show the zoom toolbar + * @returns {JSX.Element | null} The zoom selection rectangle or null when not in selection mode + * + * @example + * ```tsx + * + * ``` + * + * @see {@link ZoomToolkit} - For the zoom controls toolbar component + * @see {@link applyZoomToolkit} - For applying zoom settings to chart + * @see {@link handleChartMouseMove} - For zoom mouse interactions + * @private + */ +export const ZoomContent: React.FC = (props: ChartZoomSettingsProps) => { + const { layoutRef, phase, setLayoutValue, reportMeasured } = useLayout(); + const initialZoomRect: null = useMemo(() => null, []); + const [zoomRect, setZoomRect] = useState(initialZoomRect); + if (layoutRef.current?.chartZoom) { + zoom = layoutRef.current.chartZoom as BaseZoom; + } + + useLayoutEffect(() => { + if (phase === 'measuring') { + if ((props.selectionZoom || props.mouseWheelZoom || props.pinchZoom) && layoutRef.current?.chart) { + zoom = layoutRef.current?.chartZoom as BaseZoom; + const chartZoom: BaseZoom = getZoomOptions(props, layoutRef.current?.chart as Chart); + setLayoutValue('chartZoom', chartZoom); + } + reportMeasured('ChartZoom'); + } + }, [phase]); + + useEffect(() => { + if (phase !== 'measuring' && layoutRef.current?.chart) { + zoom = layoutRef.current?.chartZoom as BaseZoom; + (layoutRef.current?.chart as Chart).zoomSettings = props; + } + }, [props.selectionZoom, props.accessibility, props.mouseWheelZoom, + props.pinchZoom, props.pan, + props.mode, props.toolbar?.items]); + + // In ZoomContent component, replace the existing useEffect with: + useEffect(() => { + if ((props.selectionZoom || props.mouseWheelZoom || props.pinchZoom) && layoutRef.current?.chart) { + registerZoomRectSetter(setZoomRect); + + // Register zoom mouse handlers + const unregisterMouseDown: () => void = registerChartEventHandler( + 'mouseDown', (_e: Event, chart: Chart) => { + zoom = layoutRef.current?.chartZoom as BaseZoom; + handleChartMouseDown(_e as MouseEvent, chart); + }, (layoutRef.current?.chart as Chart)?.element.id); + + const unregisterMouseMove: () => void = registerChartEventHandler( + 'mouseMove', (_e: Event, chart: Chart) => { + zoom = layoutRef.current?.chartZoom as BaseZoom; + handleChartMouseMove(_e as MouseEvent, chart, setZoomRect); + }, (layoutRef.current?.chart as Chart)?.element.id); + + const unregisterMouseUp: () => void = registerChartEventHandler( + 'mouseUp', (_e: Event, chart: Chart) => { + zoom = layoutRef.current?.chartZoom as BaseZoom; + handleChartMouseUp(_e as MouseEvent, chart, setZoomRect); + }, (layoutRef.current?.chart as Chart)?.element.id); + + const unregisterMouseWheel: () => void = registerChartEventHandler( + 'mouseWheel', (_e: Event, chart: Chart) => { + zoom = layoutRef.current?.chartZoom as BaseZoom; + handleChartMouseWheel(_e as WheelEvent, chart); + }, (layoutRef.current?.chart as Chart)?.element.id); + + const unregisterMouseLeave: () => void = registerChartEventHandler( + 'mouseLeave', (_e: Event, chart: Chart) => { + zoom = layoutRef.current?.chartZoom as BaseZoom; + handleChartMouseCancel(chart, setZoomRect); + }, (layoutRef.current?.chart as Chart)?.element.id); + + // Return cleanup function + return () => { + unregisterMouseDown(); + unregisterMouseMove(); + unregisterMouseUp(); + unregisterMouseWheel(); + unregisterMouseLeave(); + }; + } + return () => { /* empty cleanup function */ }; + + }, []); + + const shouldRenderZoomRect: boolean | undefined = useMemo(() => phase === 'rendering' && + props.selectionZoom, [phase, props.selectionZoom]); + + return shouldRenderZoomRect ? <>{zoomRect} : null; +}; + +/** + * Gets zoom options from the provided settings + * + * @param {ChartZoomSettingsProps} chartZoom - The chart zoom settings + * @param {Chart} chart - The chart instance + * @returns {BaseZoom} The configured zoom controller + * @private + */ +function getZoomOptions(chartZoom: ChartZoomSettingsProps, chart: Chart): BaseZoom { + const Zoom: BaseZoom = extend({}, chartZoom) as BaseZoom; + + Zoom.zoomingRect = { x: 0, y: 0, width: 0, height: 0 }; + Zoom.isZoomed = isAxisZoomed(chart.axisCollection); + Zoom.isPanning = Zoom.isZoomed && Zoom.pan; + Zoom.performedUI = false; + Zoom.startPanning = false; + Zoom.zoomAxes = []; + Zoom.touchStartList = []; + Zoom.touchMoveList = []; + Zoom.offset = { x: 0, y: 0, width: 0, height: 0 }; + + return Zoom; +} + + +/** + * Handles mouse or touch move events for chart zooming/panning + * + * @param {MouseEvent | TouchEvent | WheelEvent} e - The mouse/touch/wheel event + * @param {Chart} chart - The chart instance + * @param {Function} setZoomRect - Function to update the zoom rectangle + * @returns {void} + * @private + */ +export function handleChartMouseMove( + e: MouseEvent | TouchEvent | WheelEvent, + chart: Chart, + setZoomRect?: (rect: JSX.Element | null) => void +): void { // Zooming for chart + if (chart.isChartDrag) { + const touches: TouchList | null = (e as TouchEvent).touches || null; + if (e.type === 'touchmove') { + if (zoom.isPanning) { + chart.startPanning = true; + } + zoom.touchMoveList = addTouchPointer(zoom.touchMoveList as ITouches[] | TouchList, + e as PointerEvent, touches) as Required; + if (chart.zoomSettings.pinchZoom && (zoom.touchMoveList as Required).length > 1 && + (zoom.touchStartList?.length as Required) > 1) { + performPinchZooming(chart); + } + } + renderZooming(e, chart, setZoomRect, chart.isTouch); + } +} + +/** + * Handles mouse or touch down events for chart zooming/panning + * + * @param {MouseEvent | TouchEvent | WheelEvent} e - The mouse/touch/wheel event + * @param {Chart} chart - The chart instance + * @returns {void} + * @private + */ +export function handleChartMouseDown(e: MouseEvent | TouchEvent | WheelEvent, chart: Chart): void { + let target: Element; + let touches: TouchList | null = null; + + if (e.type === 'touchstart') { + touches = (e as TouchEvent & PointerEvent).touches; + target = (e as TouchEvent & PointerEvent).target as Element; + } else { + target = e.target as Element; + } + + if (target.id.indexOf(chart.element.id + '_Zooming_') === -1 && + (chart.zoomSettings.pinchZoom || chart.zoomSettings.selectionZoom || zoom.isPanning) && + withInBounds(chart.previousMouseMoveX, chart.previousMouseMoveY, chart.chartAxislayout.seriesClipRect)) { + chart.isChartDrag = true; + if (chart.element) { + if (zoom.isPanning) { + chart.element.style.cursor = 'grab'; + if (chart.tooltipRef && chart.tooltipRef.current) { + chart.tooltipRef.current?.fadeOut(); + } + if (chart.trackballRef && chart.trackballRef.current) { + const childElements: HTMLCollection = chart.trackballRef.current.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.display = 'none'; + } + } + } + } else if (chart.zoomSettings.selectionZoom) { + chart.element.style.cursor = 'crosshair'; + } + } + } + + if (chart.isTouch) { + const result: ITouches[] | TouchList = addTouchPointer(zoom.touchStartList as ITouches[] | TouchList, e as PointerEvent, touches); + zoom.touchStartList = result as Required; + } +} + +/** + * Handles mouse up events for chart zooming + * + * @param {MouseEvent} e - The mouse event + * @param {Chart} chart - The chart instance + * @param {Function} setZoomRect - Function to update the zoom rectangle + * @returns {void} + * @private + */ +export function handleChartMouseUp(e: MouseEvent, chart: Chart, setZoomRect?: (rect: JSX.Element | null) => void): void { + let performZoomRedraw: boolean = true; + void ((e.target instanceof Element) && (e.target as Element)?.id && ( + performZoomRedraw = (e.target as Element)?.id.indexOf(chart.element.id + '_ZoomOut_') === -1 || + (e.target as Element)?.id.indexOf(chart.element.id + '_ZoomIn_') === -1)); + + void ((chart.isChartDrag || performZoomRedraw) && (redrawOnZooming(chart, true, true))); + + setZoomRect?.(null); + + void (chart.element && (chart.element.style.cursor = '')); + + if (chart.isTouch) { + if (chart.isDoubleTap && + withInBounds(chart.mouseX, chart.mouseY, chart.chartAxislayout.seriesClipRect) && + (zoom.touchStartList as Required).length === 1 && zoom.isZoomed) { + // Reset zoom functionality + reset(chart, zoom); + } + zoom.touchStartList = []; + chart.isDoubleTap = false; + } + + // Reset chart drag state + chart.isChartDrag = false; +} + +/** + * Handles the mouse cancel event on the chart. + * + * @param {Chart} chart - The chart instance + * @param {Function} setZoomRect - Function to update the zoom rectangle + * @returns {void} + * @private + */ +export function handleChartMouseCancel( + chart: Chart, + setZoomRect?: (rect: JSX.Element | null) => void +): void { + if (zoom.isZoomed) { + redrawOnZooming(chart, true, true); + } + void (chart.element && (chart.element.style.cursor = '')); + + setZoomRect?.(null); + + // Reset chart states + chart.isChartDrag = false; + + // Clear touch-related properties + zoom.pinchTarget = undefined; + zoom.touchStartList = []; + zoom.touchMoveList = []; +} + +/** + * Renders zooming rectangle or performs panning + * + * @param {MouseEvent | TouchEvent} e - The mouse/touch event + * @param {Chart} chart - The chart instance + * @param {Function} setZoomRect - Function to update the zoom rectangle + * @param {boolean} isTouch - Whether the event is a touch event + * @returns {void} + * @private + */ +export function renderZooming( + e: MouseEvent | TouchEvent, + chart: Chart, + setZoomRect?: (rect: JSX.Element | null) => void, + isTouch?: boolean +): void { + calculateZoomAxesRange(chart); + + if (chart.zoomSettings.selectionZoom && (!isTouch + || (chart.isDoubleTap && (zoom.touchStartList as Required).length === 1)) + && (!zoom.isPanning || chart.isDoubleTap)) { + zoom.isPanning = Browser.isDevice ? true : zoom.isPanning; + zoom.performedUI = true; + const zoomRectElement: JSX.Element = drawZoomingRectangle(chart) as JSX.Element; + setZoomRect?.(zoomRectElement); + } else if (zoom.isPanning && chart.isChartDrag) { + if (!isTouch || (isTouch && (zoom.touchStartList as Required).length === 1)) { + zoom.pinchTarget = isTouch ? (e.target as Element) : undefined; + doPan(chart, chart.axisCollection); + } + } +} + +/** + * Draws the zooming rectangle + * + * @param {Chart} chart - The chart instance + * @returns {JSX.Element | null} The rendered zoom rectangle or null + * @private + */ +export function drawZoomingRectangle(chart: Chart): JSX.Element | null { + const areaBounds: Rect = chart.chartAxislayout.seriesClipRect; + const startLocation: ChartLocationProps = { x: chart.previousMouseMoveX, y: chart.previousMouseMoveY }; + const endLocation: ChartLocationProps = { x: chart.mouseX, y: chart.mouseY }; + const rect: Rect = zoom.zoomingRect = getRectLocation(startLocation, endLocation, areaBounds); + + if (rect.width > 0 && rect.height > 0) { + zoom.isZoomed = true; + chart.disableTrackTooltip = true; + + if (chart.zoomSettings.mode === 'X') { + rect.height = areaBounds.height; + rect.y = areaBounds.y; + } else if (chart.zoomSettings.mode === 'Y') { + rect.width = areaBounds.width; + rect.x = areaBounds.x; + } + chart.zoomRedraw = true; + if (chart.tooltipRef && chart.tooltipRef.current) { + chart.tooltipRef.current?.fadeOut(); + } + return ( + + ); + } + + return null; +} + +/** + * Performs panning on the chart + * + * @param {Chart} chart - The chart instance + * @param {AxisModel[]} axes - The axes to pan + * @param {number} xDifference - Optional X difference for panning + * @param {number} yDifference - Optional Y difference for panning + * @returns {void} + * @private + */ +export function doPan(chart: Chart, axes: AxisModel[], xDifference: number = 0, yDifference: number = 0): void { + + let currentScale: number; + let offsetValue: number; + zoom.isZoomed = true; + chart.startPanning = true; + zoom.zoomCompleteEvtCollection = []; + chart.disableTrackTooltip = true; + zoom.offset = !chart.delayRedraw ? chart.chartAxislayout.seriesClipRect : zoom.offset; + + const zoomedAxisCollection: AxisDataProps[] = []; + + for (const axis of (axes as AxisModel[])) { + const argsData: ZoomEndEvent = { + axisName: axis.name, + previousZoomFactor: axis.zoomFactor, + previousZoomPosition: axis.zoomPosition, + currentZoomFactor: axis.zoomFactor, + currentZoomPosition: axis.zoomPosition, + previousVisibleRange: axis.visibleRange, + currentVisibleRange: undefined + }; + + currentScale = Math.max(1 / minMax(axis.zoomFactor as number, 0, 1), 1); + + if (axis.orientation === 'Horizontal') { + offsetValue = (xDifference !== 0 ? xDifference : (chart.previousMouseMoveX - chart.mouseX)) / axis.rect.width / currentScale; + argsData.currentZoomPosition = minMax((axis.zoomPosition as number) + offsetValue, 0, (1 - (axis.zoomFactor as number))); + } else { + offsetValue = (yDifference !== 0 ? yDifference : (chart.previousMouseMoveY - chart.mouseY)) / axis.rect.height / currentScale; + argsData.currentZoomPosition = minMax((axis.zoomPosition as number) - offsetValue, 0, (1 - (axis.zoomFactor as number))); + } + axis.zoomFactor = argsData.currentZoomFactor; + axis.zoomPosition = argsData.currentZoomPosition; + zoom.zoomCompleteEvtCollection.push(argsData); + zoomedAxisCollection.push({ + zoomFactor: axis.zoomFactor as number, + zoomPosition: axis.zoomPosition as number, + axisName: axis.name as string, + axisRange: axis.visibleRange + }); + } + + const zoomingEventArgs: ZoomStartEvent = { + cancel: false, + axisData: zoomedAxisCollection + }; + chart.chartProps?.onZoomStart?.(zoomingEventArgs); + + if (zoomingEventArgs.cancel) { + zoomCancel(axes, zoom.zoomCompleteEvtCollection); + } else { + performDeferredZoom(chart); + redrawOnZooming(chart, false); + } +} + +/** + * Performs a deferred zoom operation on the chart + * + * @param {Chart} chart - The chart instance to perform the zoom on + * @returns {void} This function doesn't return a value + * @private + */ +function performDeferredZoom(chart: Chart): void { + let translateX: number; + let translateY: number; + + translateX = chart.mouseX - chart.mouseDownX; + translateY = chart.mouseY - chart.mouseDownY; + + switch (chart.zoomSettings.mode) { + case 'X': + translateY = 0; + break; + case 'Y': + translateX = 0; + break; + } + + setTransform(translateX, translateY, null, null, chart, false); + + chart.previousMouseMoveX = chart.mouseX; + chart.previousMouseMoveY = chart.mouseY; +} + +/** + * Sets transform for series elements during panning/zooming + * + * @param {number} transX - X translation + * @param {number} transY - Y translation + * @param {number} scaleX - X scale factor + * @param {number} scaleY - Y scale factor + * @param {Chart} chart - The chart instance + * @param {boolean} isPinch - Whether this is a pinch operation + * @returns {void} + * @private + */ +function setTransform(transX: number, transY: number, scaleX: number | null, scaleY: number | null, chart: Chart, isPinch: boolean): void { + + let translate: string; + let xAxisLoc: number; + let yAxisLoc: number; + + if (transX !== null && transY !== null) { + for (const series of chart.visibleSeries) { + if (!series.visible) { continue; } + + xAxisLoc = chart.requireInvertedAxis ? series.yAxis.rect.x : series.xAxis.rect.x; + yAxisLoc = chart.requireInvertedAxis ? series.xAxis.rect.y : series.yAxis.rect.y; + + translate = `translate(${transX + (isPinch ? ((scaleX ? scaleX : 0) * xAxisLoc) : xAxisLoc)},${transY + (isPinch ? ((scaleY ? scaleY : 0) * yAxisLoc) : yAxisLoc)})`; + translate = (scaleX || scaleY) ? `${translate} scale(${scaleX} ${scaleY})` : translate; + + if (series.seriesElement) { + series.seriesElement.setAttribute('transform', translate); + } + } + } +} + +/** + * Calculates the zoom axes range for the chart. + * + * @param {Chart} chart - The chart instance + * @returns {void} + * @private + */ +function calculateZoomAxesRange(chart: Chart): void { + let range: IZoomAxisRange; + let axisRange: VisibleRangeProps; + + for (let index: number = 0; index < chart.axisCollection.length; index++) { + const axis: AxisModel = chart.axisCollection[index as number]; + axisRange = axis.visibleRange; + + if ((zoom.zoomAxes as Required)[index as number]) { + if (!chart.delayRedraw) { + (zoom.zoomAxes as Required)[index as number].min = axisRange.minimum; + (zoom.zoomAxes as Required)[index as number].delta = axisRange.delta; + } + } else { + range = { + actualMin: axis.actualRange.minimum, + actualDelta: axis.actualRange.delta, + min: axisRange.minimum, + delta: axisRange.delta + }; + (zoom.zoomAxes as Required)[index as number] = range; + } + } +} + +/** + * Redraws the chart after zooming operations + * + * @param {Chart} chart - The chart instance + * @param {boolean} isRedraw - Whether to perform a full redraw + * @param {boolean} isMouseUp - Whether this is triggered by a mouse up event + * @returns {void} + * @private + */ +export function redrawOnZooming(chart: Chart, isRedraw: boolean = true, isMouseUp: boolean = false): void { + const zoomCompleteCollection: ZoomEndEvent[] = isMouseUp ? [] : zoom.zoomCompleteEvtCollection; + + if (isRedraw) { + performZoomRedraw(chart, { + rect: zoom.zoomingRect, + zoomed: zoom.isZoomed, + panning: zoom.isPanning, + performedUI: zoom.performedUI, + startPanning: chart.startPanning + }); + } + + for (let i: number = 0; i < zoomCompleteCollection.length; i++) { + const argsData: ZoomEndEvent = { + axisName: chart.axisCollection[i as number].name, + previousZoomFactor: zoomCompleteCollection[i as number].previousZoomFactor, + previousZoomPosition: zoomCompleteCollection[i as number].previousZoomPosition, + currentZoomFactor: chart.axisCollection[i as number].zoomFactor, + currentZoomPosition: chart.axisCollection[i as number].zoomPosition, + currentVisibleRange: chart.axisCollection[i as number].visibleRange, + previousVisibleRange: zoomCompleteCollection[i as number].previousVisibleRange + }; + chart.chartProps?.onZoomEnd?.(argsData); + } +} + +/** + * Performs the redraw operation after zooming/panning + * + * @param {Chart} chart - The chart instance + * @param {ZoomRedrawOptions} [options={}] - Configuration options for zoom redraw, including rectangle dimensions, zoom state flags + * @returns {void} + * @private + */ +export function performZoomRedraw( + chart: Chart, + options: ZoomRedrawOptions = {} +): void { + const { + rect = { x: 0, y: 0, width: 0, height: 0 }, + zoomed = false, + panning = false, + performedUI = false, + startPanning = false + } = options; + const zoomRect: Rect = rect || (zoom.zoomingRect as Required); + chart.animateSeries = false; + + if (zoomed !== undefined ? zoomed : zoom.isZoomed) { + if (zoomRect.width > 0 && zoomRect.height > 0) { + zoom.performedUI = performedUI !== undefined ? performedUI : true; + doZoom( + chart, + chart.axisCollection, + chart.chartAxislayout.seriesClipRect, + zoomRect, + panning !== undefined ? panning : zoom.isPanning + ); + chart.isDoubleTap = false; + } else if (chart.disableTrackTooltip) { + chart.disableTrackTooltip = false; + chart.delayRedraw = false; + + const chartDuration: number = chart.duration || 0; + if (!(panning && (chart.isChartDrag || startPanning))) { + chart.duration = 600; + } + chart.zoomRedraw = true; + const chartId: string = chart.element.id; + if (chart.tooltipRef && chart.tooltipRef.current) { + chart.tooltipRef.current?.fadeOut(); + } + if (chart.trackballRef && chart.trackballRef.current) { + const childElements: HTMLCollection = chart.trackballRef.current.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.display = 'none'; + } + } + } + // Trigger renders + const triggerAxisRender: (chartId?: string) => void = useRegisterAxisRender(); + triggerAxisRender(chartId); + + const triggerSeriesRender: (chartId?: string) => void = useRegisterSeriesRender(); + triggerSeriesRender(chartId); + + const setToolkitVisible: () => void = useRegisterZoomToolkitVisibility(); + setToolkitVisible(); + + chart.startPanning = false; + chart.redraw = false; + chart.duration = chartDuration; + } + } +} + +/** + * Performs zooming operations on the chart based on user interactions. + * + * @param {Chart} chart - The chart instance to perform zooming on + * @param {AxisModel[]} axes - The axes to apply zooming to + * @param {Rect} bounds - The bounds of the chart area + * @param {Rect} zoomingRectParam - The rectangle defining the zoom area + * @param {boolean} isPanningParam - Whether panning is active + * @returns {void} + * @private + */ +export function doZoom( + chart: Chart, + axes: AxisModel[], + bounds: Rect, + zoomingRectParam: Rect, + isPanningParam: boolean = false +): void { + const zoomRect: Rect = zoomingRectParam; + const mode: ZoomMode = chart.zoomSettings.mode as ZoomMode; + zoom.isPanning = chart.zoomSettings.pan || isPanningParam; + const zoomedAxisCollections: AxisDataProps[] = []; + zoom.zoomCompleteEvtCollection = []; + + for (const axis of (axes as AxisModel[])) { + const argsData: ZoomEndEvent = { + axisName: axis.name, + previousZoomFactor: axis.zoomFactor, + previousZoomPosition: axis.zoomPosition, + currentZoomFactor: axis.zoomFactor, + currentZoomPosition: axis.zoomPosition, + previousVisibleRange: axis.visibleRange, + currentVisibleRange: undefined + }; + + if (axis.orientation === 'Horizontal') { + if (mode !== 'Y') { + (argsData.currentZoomPosition as Required) += (Math.abs((zoomRect.x - bounds.x) / (bounds.width)) * + (axis.zoomFactor as number) as number); + (argsData.currentZoomFactor as Required) *= (zoomRect.width / bounds.width); + } + } else { + if (mode !== 'X') { + (argsData.currentZoomPosition as Required) += (1 - Math.abs((zoomRect.height + + (zoomRect.y - bounds.y)) / (bounds.height))) * (axis.zoomFactor as Required); + (argsData.currentZoomFactor as Required) *= (zoomRect.height / bounds.height); + } + } + + if (parseFloat((argsData.currentZoomFactor as Required).toFixed(3)) <= 0.001) { + argsData.currentZoomFactor = argsData.previousZoomFactor; + argsData.currentZoomPosition = argsData.previousZoomPosition; + } + + axis.zoomFactor = argsData.currentZoomFactor; + axis.zoomPosition = argsData.currentZoomPosition; + zoom.zoomCompleteEvtCollection.push(argsData); + + zoomedAxisCollections.push({ + zoomFactor: axis.zoomFactor as number, + zoomPosition: axis.zoomPosition as number, + axisName: axis.name as string, + axisRange: axis.visibleRange + }); + } + + const onZoomingEventArg: ZoomStartEvent = { + cancel: false, + axisData: zoomedAxisCollections + }; + + // Trigger onZooming event + chart.chartProps?.onZoomStart?.(onZoomingEventArg); + + if (onZoomingEventArg.cancel) { + zoomCancel(axes, zoom.zoomCompleteEvtCollection); + } else { + // Reset zooming rectangle + zoom.zoomingRect = { x: 0, y: 0, width: 0, height: 0 }; + redrawOnZooming(chart); + } +} + +/** + * Performs mouse wheel zooming on the chart + * + * @param {WheelEvent} e - The wheel event + * @param {number} mouseX - The x-coordinate of the mouse position + * @param {number} mouseY - The y-coordinate of the mouse position + * @param {Chart} chart - The chart instance + * @param {AxisModel[]} axes - The axes to zoom + * @returns {void} + * @private + */ +export function performMouseWheelZooming(e: WheelEvent, mouseX: number, mouseY: number, chart: Chart, axes: AxisModel[]): void { + + const browserName: string = (Browser.info.name && typeof Browser.info.name === 'string') + ? Browser.info.name : ''; + const isMozilla: boolean = browserName.toLowerCase().includes('mozilla') || + browserName.toLowerCase().includes('firefox'); + const isPointer: boolean = Browser?.isPointer || false; + const direction: number = (isMozilla && !isPointer) ? + -(e.detail) / 3 > 0 ? 1 : -1 : (e.deltaY > 0 ? -1 : 1); + const mode: ZoomMode = chart.zoomSettings.mode as ZoomMode; + + zoom.isZoomed = true; + chart.isGestureZooming = true; + calculateZoomAxesRange(chart); + chart.disableTrackTooltip = true; + zoom.performedUI = true; + zoom.isPanning = true; + zoom.zoomCompleteEvtCollection = []; + + const zoomedAxisCollection: AxisDataProps[] = calculateWheelZoomFactors( + direction, mouseX, mouseY, chart, axes, mode, zoom + ); + + const onZoomingEventArgs: ZoomStartEvent = { + cancel: false, + axisData: zoomedAxisCollection + }; + + // Trigger onZooming event + chart.chartProps?.onZoomStart?.(onZoomingEventArgs); + + if (!onZoomingEventArgs.cancel) { + redrawOnZooming(chart); + } else { + zoomCancel(axes, zoom.zoomCompleteEvtCollection); + } +} + +/** + * Calculates zoom factors for each axis based on mouse wheel zooming + * + * @private + * @param {number} direction - The zoom direction (1 for in, -1 for out) + * @param {number} mouseX - Mouse X position + * @param {number} mouseY - Mouse Y position + * @param {Chart} chart - The chart instance + * @param {AxisModel[]} axes - The axes to zoom + * @param {ZoomMode} mode - The zoom mode (X, Y, or XY) + * @param {BaseZoom} zoom - The zoom controller + * @returns {AxisDataProps[]} Collection of zoomed axis data + * @private + */ +function calculateWheelZoomFactors( + direction: number, + mouseX: number, + mouseY: number, + chart: Chart, + axes: AxisModel[], + mode: ZoomMode, + zoom: BaseZoom +): AxisDataProps[] { + const zoomedAxisCollection: AxisDataProps[] = []; + let anyAxisZoomed: boolean = false; + + for (const axis of axes) { + const argsData: ZoomEndEvent = { + axisName: axis.name, + previousZoomFactor: axis.zoomFactor, + previousZoomPosition: axis.zoomPosition, + currentZoomFactor: axis.zoomFactor, + currentZoomPosition: axis.zoomPosition, + currentVisibleRange: undefined, + previousVisibleRange: axis.visibleRange + }; + + if ((axis.orientation === 'Vertical' && mode !== 'X') || + (axis.orientation === 'Horizontal' && mode !== 'Y')) { + const ZOOM_FACTOR_INCREMENT: number = 0.25; + const MAX_ZOOM_CUMULATIVE: number = 50000000000; + + let cumulative: number = Math.max(Math.max(1 / minMax(axis.zoomFactor as number, 0, 1), 1) + + (ZOOM_FACTOR_INCREMENT * direction), 1); + cumulative = cumulative > MAX_ZOOM_CUMULATIVE ? MAX_ZOOM_CUMULATIVE : cumulative; + + if (cumulative >= 1) { + let origin: number = axis.orientation === 'Horizontal' ? mouseX / axis.rect.width : 1 - (mouseY / axis.rect.height); + origin = origin > 1 ? 1 : origin < 0 ? 0 : origin; + let zoomFactor: number = (cumulative === 1) ? 1 : minMax((direction > 0 ? 0.9 : 1.1) / cumulative, 0, 1); + const zoomPosition: number = (cumulative === 1) ? 0 : (axis.zoomPosition as number) + (((axis.zoomFactor as number) + - zoomFactor) * origin); + + if (axis.zoomPosition !== zoomPosition || axis.zoomFactor !== zoomFactor) { + zoomFactor = (zoomPosition + zoomFactor) > 1 ? (1 - zoomPosition) : zoomFactor; + } + + if (parseFloat((argsData.currentZoomFactor as Required).toFixed(3)) <= 0.001) { + argsData.currentZoomFactor = argsData.previousZoomFactor; + argsData.currentZoomPosition = argsData.previousZoomPosition; + } else { + argsData.currentZoomFactor = zoomFactor; + argsData.currentZoomPosition = zoomPosition; + } + } + + if (argsData.currentZoomFactor !== argsData.previousZoomFactor || + argsData.currentZoomPosition !== argsData.previousZoomPosition) { + anyAxisZoomed = true; + } + + axis.zoomFactor = argsData.currentZoomFactor; + axis.zoomPosition = argsData.currentZoomPosition; + zoom.zoomCompleteEvtCollection.push(argsData); + } + + zoomedAxisCollection.push({ + zoomFactor: axis.zoomFactor as number, + zoomPosition: axis.zoomPosition as number, + axisName: axis.name as string, + axisRange: axis.visibleRange + }); + } + if (!anyAxisZoomed) { + chart.disableTrackTooltip = false; + } + + return zoomedAxisCollection; +} + +/** + * Performs pinch zooming on the chart + * + * @param {Chart} chart - The chart instance to perform pinch zooming on + * @returns {boolean} Whether pinch zooming was successfully performed + * @private + */ +export function performPinchZooming(chart: Chart): boolean { + if (!zoom.touchStartList || !zoom.touchMoveList || + zoom.touchStartList.length < 2 || zoom.touchMoveList.length < 2) { + return false; + } + + if ((zoom.zoomingRect && zoom.zoomingRect.width > 0 && zoom.zoomingRect.height > 0)) { + return false; + } + + calculateZoomAxesRange(chart); + chart.isGestureZooming = true; + zoom.isZoomed = true; + zoom.isPanning = true; + zoom.performedUI = true; + zoom.offset = chart.delayRedraw ? chart.chartAxislayout.seriesClipRect : zoom.offset; + chart.delayRedraw = true; + chart.disableTrackTooltip = true; + + const elementOffset: DOMRect = chart.element.getBoundingClientRect(); + const touchDown: TouchList = zoom.touchStartList as TouchList; + const touchMove: TouchList = zoom.touchMoveList as TouchList; + + const touch0StartX: number = touchDown[0].pageX - elementOffset.left; + const touch0StartY: number = touchDown[0].pageY - elementOffset.top; + const touch0EndX: number = touchMove[0].pageX - elementOffset.left; + const touch0EndY: number = touchMove[0].pageY - elementOffset.top; + const touch1StartX: number = touchDown[1].pageX - elementOffset.left; + const touch1StartY: number = touchDown[1].pageY - elementOffset.top; + const touch1EndX: number = touchMove[1].pageX - elementOffset.left; + const touch1EndY: number = touchMove[1].pageY - elementOffset.top; + + const scaleX: number = Math.abs(touch0EndX - touch1EndX) / Math.abs(touch0StartX - touch1StartX); + const scaleY: number = Math.abs(touch0EndY - touch1EndY) / Math.abs(touch0StartY - touch1StartY); + const clipX: number = (((zoom.offset as Required).x - touch0EndX) / scaleX) + touch0StartX; + const clipY: number = (((zoom.offset as Required).y - touch0EndY) / scaleY) + touch0StartY; + const pinchRect: Rect = { + x: clipX, y: clipY, width: (zoom.offset as Required).width / scaleX, + height: (zoom.offset as Required).height / scaleY + }; + const translateXValue: number = (touch0EndX - (scaleX * touch0StartX)); + const translateYValue: number = (touch0EndY - (scaleY * touch0StartY)); + + if (!isNaN(scaleX - scaleX) && !isNaN(scaleY - scaleY)) { + switch (chart.zoomSettings.mode) { + case 'XY': + setTransform(translateXValue, translateYValue, scaleX, scaleY, chart, true); + break; + case 'X': + setTransform(translateXValue, 0, scaleX, 1, chart, true); + break; + case 'Y': + setTransform(0, translateYValue, 1, scaleY, chart, true); + break; + } + } + + if (!calculatePinchZoomFactor(chart, pinchRect)) { + chart.zoomRedraw = true; + if (chart.tooltipRef && chart.tooltipRef.current) { + chart.tooltipRef.current?.fadeOut(); + } + if (chart.trackballRef && chart.trackballRef.current) { + const childElements: HTMLCollection = chart.trackballRef.current.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.display = 'none'; + } + } + } + const chartId: string = chart.element.id; + const triggerAxisRender: (chartId?: string) => void = useRegisterAxisRender(); + triggerAxisRender(chartId); + + const triggerSeriesRender: (chartId?: string) => void = useRegisterSeriesRender(); + triggerSeriesRender(chartId); + + const setToolkitVisible: () => void = useRegisterZoomToolkitVisibility(); + setToolkitVisible(); + } + + return true; +} + +/** + * Calculates pinch zoom factor + * + * @param {Chart} chart - The chart instance + * @param {Rect} pinchRect - The pinch rectangle + * @returns {boolean} Whether zoom factor calculation was cancelled + * @private + */ +function calculatePinchZoomFactor(chart: Chart, pinchRect: Rect): boolean { + const mode: ZoomMode = chart.zoomSettings.mode as ZoomMode; + let selectionMin: number; + let selectionMax: number; + let rangeMin: number; + let rangeMax: number; + let value: number; + let axisTrans: number; + let argsData: ZoomEndEvent; + let currentZF: number; + let currentZP: number; + const zoomedAxisCollection: AxisDataProps[] = []; + zoom.zoomCompleteEvtCollection = []; + + for (let index: number = 0; index < chart.axisCollection.length; index++) { + const axis: AxisModel = chart.axisCollection[index as number]; + if ((axis.orientation === 'Horizontal' && mode !== 'Y') || + (axis.orientation === 'Vertical' && mode !== 'X')) { + currentZF = axis.zoomFactor as Required; + currentZP = axis.zoomPosition as Required; + argsData = { + axisName: axis.name, + previousZoomFactor: axis.zoomFactor, + previousZoomPosition: axis.zoomPosition, + currentZoomFactor: currentZF, + currentZoomPosition: currentZP, + previousVisibleRange: axis.visibleRange, + currentVisibleRange: undefined + }; + + if (axis.orientation === 'Horizontal') { + value = pinchRect.x - (zoom.offset as Required).x; + + axisTrans = + axis.rect.width / ((zoom.zoomAxes as Required)[index as number].delta as Required); + rangeMin = value / axisTrans + ((zoom.zoomAxes as Required)[index as number].min as Required); + value = pinchRect.x + pinchRect.width - (zoom.offset as Required).x; + rangeMax = value / axisTrans + ((zoom.zoomAxes as Required)[index as number].min as Required); + } else { + value = pinchRect.y - (zoom.offset as Required).y; + axisTrans = axis.rect.height / ((zoom.zoomAxes as Required)[index as number].delta as Required); + rangeMin = (value * -1 + axis.rect.height) / axisTrans + + ((zoom.zoomAxes as Required)[index as number].min as Required); + value = pinchRect.y + pinchRect.height - (zoom.offset as Required).y; + rangeMax = (value * -1 + axis.rect.height) / axisTrans + + ((zoom.zoomAxes as Required)[index as number].min as Required); + } + + selectionMin = Math.min(rangeMin, rangeMax); + selectionMax = Math.max(rangeMin, rangeMax); + currentZP = (selectionMin - + ((zoom.zoomAxes as Required)[index as number].actualMin as Required)) / + ((zoom.zoomAxes as Required)[index as number].actualDelta as Required); + currentZF = (selectionMax - + selectionMin) / ((zoom.zoomAxes as Required)[index as number].actualDelta as Required); + argsData.currentZoomPosition = currentZP < 0 ? 0 : currentZP; + argsData.currentZoomFactor = currentZF > 1 ? 1 : (currentZF < 0.003) ? 0.003 : currentZF; + + axis.zoomFactor = argsData.currentZoomFactor; + axis.zoomPosition = argsData.currentZoomPosition; + zoom.zoomCompleteEvtCollection.push(argsData); + + zoomedAxisCollection.push({ + zoomFactor: axis.zoomFactor as number, + zoomPosition: axis.zoomPosition as number, + axisName: axis.name as string, + axisRange: axis.visibleRange + }); + } + } + + const onZoomingEventArgs: ZoomStartEvent = { + cancel: false, + axisData: zoomedAxisCollection + }; + + if (!onZoomingEventArgs.cancel) { + chart.chartProps?.onZoomStart?.(onZoomingEventArgs); + } + else { + zoomCancel(chart.axisCollection, zoom.zoomCompleteEvtCollection); + return true; + } + + return false; +} + +/** + * Applies the zoom toolkit on the chart + * + * @param {Chart} chart - The chart instance + * @param {AxisModel[]} axes - The axes to check + * @returns {boolean} Whether the toolkit should be shown + * @param {BaseZoom} zoom - The zoom controller + * @private + */ +export function applyZoomToolkit(chart: Chart, axes: AxisModel[], zoom: BaseZoom): boolean { + const showToolkit: boolean = isAxisZoomed(axes); + + if (showToolkit) { + zoom.isZoomed = true; + return true; + } else if (chart.zoomSettings.toolbar?.visible) { + zoom.isZoomed = showToolkit; + return true; + } else { + zoom.isPanning = false; + zoom.isZoomed = false; + chart.isZoomed = false; + return false; + } +} + +/** + * Cancels the zoom action + * + * @param {AxisModel[]} axes - The axes to reset + * @param {ZoomEndEvent[]} zoomCompleteEventCollection - Collection of zoom complete events + * @returns {void} + * @private + */ +export function zoomCancel(axes: AxisModel[], zoomCompleteEventCollection: ZoomEndEvent[]): void { + for (const zoomCompleteEvent of zoomCompleteEventCollection) { + for (const axis of (axes as AxisModel[])) { + if (axis.name === zoomCompleteEvent.axisName) { + axis.zoomFactor = zoomCompleteEvent.previousZoomFactor; + axis.zoomPosition = zoomCompleteEvent.previousZoomPosition; + axis.visibleRange = zoomCompleteEvent.previousVisibleRange as VisibleRangeProps; + break; + } + } + } +} + +/** + * Checks if any of the axes is zoomed + * + * @param {AxisModel[]} axes - The axes to check + * @returns {boolean} Whether any axis is zoomed + * @private + */ +export function isAxisZoomed(axes: AxisModel[]): boolean { + let showToolkit: boolean = false; + for (const axis of (axes as AxisModel[])) { + showToolkit = (showToolkit || (axis.zoomFactor !== 1 || axis.zoomPosition !== 0)); + } + return showToolkit; +} + +/** + * Handles mouse wheel zooming + * + * @param {WheelEvent} e - The wheel event + * @param {Chart} chart - The chart instance + * @returns {boolean} Whether the wheel event was handled + * @private + */ +export function handleChartMouseWheel(e: WheelEvent, chart: Chart): boolean { + const offset: DOMRect = chart.element.getBoundingClientRect(); + const svgElement: Element = chart.element.querySelector('svg') as Element; + const svgRect: DOMRect | null = svgElement?.getBoundingClientRect(); + const mouseX: number = (e.clientX - offset.left) - Math.max((svgRect?.left || 0) - offset.left, 0); + const mouseY: number = (e.clientY - offset.top) - Math.max((svgRect?.top || 0) - offset.top, 0); + + if (chart.zoomSettings.mouseWheelZoom && + withInBounds(mouseX, mouseY, chart.chartAxislayout.seriesClipRect)) { + e.preventDefault(); + performMouseWheelZooming(e, mouseX, mouseY, chart, chart.axisCollection); + } + + return false; +} + +/** + * Adds touch pointer to the touch list + * + * @param {ITouches[] | TouchList} touchList - The touch list to modify + * @param {PointerEvent} e - The pointer event + * @param {TouchList | null} touches - Touch list from the event + * @returns {ITouches[] | TouchList} The updated touch list + * @private + */ +export function addTouchPointer(touchList: ITouches[] | TouchList, e: PointerEvent, touches: TouchList | null): ITouches[] | TouchList { + if (touches) { + touchList = []; + for (let i: number = 0, length: number = touches.length; i < length; i++) { + touchList.push({ + pageX: touches[i as number].clientX, + pageY: touches[i as number].clientY, + pointerId: undefined + }); + } + } else { + touchList = touchList ? touchList : []; + if (touchList.length === 0) { + (touchList as Required).push({ + pageX: e.clientX, + pageY: e.clientY, + pointerId: e.pointerId + }); + } else { + let found: boolean = false; + for (let i: number = 0, length: number = touchList.length; i < length; i++) { + if ((touchList[i as number] as Required).pointerId === e.pointerId) { + touchList[i as number] = { + pageX: e.clientX, + pageY: e.clientY, + pointerId: e.pointerId + }; + found = true; + break; + } + } + if (!found) { + (touchList as Required).push({ + pageX: e.clientX, + pageY: e.clientY, + pointerId: e.pointerId + }); + } + } + } + return touchList; +} + +/** + * Toggles pan mode for the chart + * + * @param {Chart} chart - The chart instance + * @param {BaseZoom} zoom - The zoom controller + * @returns {void} + * @private + */ +export function togglePan(chart: Chart, zoom: BaseZoom): void { + if (!zoom.isZoomed) { + return; + } + zoom.isPanning = true; + chart.isZoomed = true; + const setToolkitVisible: () => void = useRegisterZoomToolkitVisibility(); + setToolkitVisible(); +} + +/** + * Performs zoom in operation on the chart + * + * @param {Chart} chart - The chart instance + * @returns {void} + * @private + */ +export function zoomIn(chart: Chart): void { + zoomInOutCalculation(1, chart, chart.axisCollection, chart.zoomSettings.mode as ZoomMode); +} + +/** + * Performs zoom out operation on the chart + * + * @param {Chart} chart - The chart instance + * @returns {void} + * @private + */ +export function zoomOut(chart: Chart): void { + zoomInOutCalculation(-1, chart, chart.axisCollection, chart.zoomSettings.mode as ZoomMode); +} + +/** + * Calculates zoom factors and positions for zoom in/out actions + * + * @param {number} scale - Scale factor (positive for zoom in, negative for zoom out) + * @param {Chart} chart - The chart instance + * @param {AxisModel[]} axes - The axes to zoom + * @param {ZoomMode} mode - The zoom mode (X, Y, or XY) + * @returns {void} + * @private + */ +export function zoomInOutCalculation(scale: number, chart: Chart, axes: AxisModel[], mode: ZoomMode): void { + chart.delayRedraw = false; + zoom.isPanning = false; + + const zoomCompleteEvtCollection: ZoomEndEvent[] = []; + const zoomedAxisCollection: AxisDataProps[] = []; + + for (const axis of axes) { + // Save previous values for zoom complete event + const argsData: ZoomEndEvent = { + axisName: axis.name, + previousZoomFactor: axis.zoomFactor, + previousZoomPosition: axis.zoomPosition, + currentZoomFactor: axis.zoomFactor, + currentZoomPosition: axis.zoomPosition, + previousVisibleRange: axis.visibleRange, + currentVisibleRange: undefined + }; + + // Apply zoom calculations only to relevant axes based on mode + if ((axis.orientation === 'Horizontal' && mode !== 'Y') || + (axis.orientation === 'Vertical' && mode !== 'X')) { + + let currentZoomFactor: number; + let currentZoomPosition: number; + + // For zoom in (scale > 0) + if (scale > 0) { + currentZoomFactor = (axis.zoomFactor as number) * 0.8; + currentZoomPosition = (axis.zoomPosition as number) + + (((axis.zoomFactor as number) - currentZoomFactor) * 0.5); + } + // For zoom out (scale < 0) + else { + currentZoomFactor = Math.min((axis.zoomFactor as number) * 1.25, 1); + if (currentZoomFactor === 1) { + currentZoomPosition = 0; + } else { + currentZoomPosition = Math.max((axis.zoomPosition as number) - + ((currentZoomFactor - (axis.zoomFactor as number)) * 0.5), 0); + } + } + + argsData.currentZoomFactor = currentZoomFactor; + argsData.currentZoomPosition = currentZoomPosition; + + axis.zoomFactor = argsData.currentZoomFactor; + axis.zoomPosition = argsData.currentZoomPosition; + zoomCompleteEvtCollection.push(argsData); + + zoomedAxisCollection.push({ + zoomFactor: axis.zoomFactor as number, + zoomPosition: axis.zoomPosition as number, + axisName: axis.name as string, + axisRange: axis.visibleRange + }); + } + } + zoom.isZoomed = isAxisZoomed(axes); + const zoomingEventArgs: ZoomStartEvent = { + cancel: false, + axisData: zoomedAxisCollection + }; + + // Trigger onZooming event + chart.chartProps?.onZoomStart?.(zoomingEventArgs); + + if (zoomingEventArgs.cancel) { + zoomCancel(axes, zoomCompleteEvtCollection); + } else { + chart.zoomRedraw = true; + if (chart.tooltipRef && chart.tooltipRef.current) { + chart.tooltipRef.current?.fadeOut(); + } + if (chart.trackballRef && chart.trackballRef.current) { + const childElements: HTMLCollection = chart.trackballRef.current.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.display = 'none'; + } + } + } + const chartId: string = chart.element.id; + const triggerAxisRender: (chartId?: string) => void = useRegisterAxisRender(); + triggerAxisRender(chartId); + + const triggerSeriesRender: (chartId?: string) => void = useRegisterSeriesRender(); + triggerSeriesRender(chartId); + + const setToolkitVisible: () => void = useRegisterZoomToolkitVisibility(); + setToolkitVisible(); + } +} + +/** + * Resets zoom to default state + * + * @param {Chart} chart - The chart instance + * @param {BaseZoom} zoom - The zoom controller + * @returns {boolean} Whether the reset was successful + * @private + */ +export function reset(chart: Chart, zoom: BaseZoom): boolean { + + const zoomedAxisCollection: AxisDataProps[] = []; + zoom.zoomCompleteEvtCollection = []; + + // Reset each axis + for (const axis of chart.axisCollection) { + const argsData: ZoomEndEvent = { + axisName: axis.name, + previousZoomFactor: axis.zoomFactor, + previousZoomPosition: axis.zoomPosition, + currentZoomFactor: 1, + currentZoomPosition: 0, + previousVisibleRange: axis.visibleRange, + currentVisibleRange: undefined + }; + + // Reset axis zoom properties + axis.zoomFactor = 1; + axis.zoomPosition = 0; + axis.zoomFactor = argsData.currentZoomFactor; + axis.zoomPosition = argsData.currentZoomPosition; + zoom.zoomCompleteEvtCollection.push(argsData); + + zoomedAxisCollection.push({ + zoomFactor: axis.zoomFactor as number, + zoomPosition: axis.zoomPosition as number, + axisName: axis.name as string, + axisRange: axis.visibleRange + }); + + // Trigger zoom complete event for touch devices + chart.chartProps?.onZoomEnd?.(argsData); + } + + // Reset zoom state + zoom.isZoomed = false; + zoom.isPanning = false; + chart.isZoomed = false; + chart.delayRedraw = false; + const chartId: string = chart.element.id; + chart.zoomRedraw = true; + if (chart.tooltipRef && chart.tooltipRef.current) { + chart.tooltipRef.current?.fadeOut(); + } + if (chart.trackballRef && chart.trackballRef.current) { + const childElements: HTMLCollection = chart.trackballRef.current.children as HTMLCollection; + for (let i: number = 0; i < childElements.length; i++) { + const element: HTMLElement = childElements[i as number] as HTMLElement; + if (element) { + element.style.display = 'none'; + } + } + } + // Trigger renders + const triggerAxisRender: (chartId?: string) => void = useRegisterAxisRender(); + triggerAxisRender(chartId); + + const triggerSeriesRender: (chartId?: string) => void = useRegisterSeriesRender(); + triggerSeriesRender(chartId); + + const setToolkitVisible: () => void = useRegisterZoomToolkitVisibility(); + setToolkitVisible(); + return true; +} + diff --git a/components/charts/src/chart/series/DataLabel.tsx b/components/charts/src/chart/series/DataLabel.tsx new file mode 100644 index 0000000..9b9dfb9 --- /dev/null +++ b/components/charts/src/chart/series/DataLabel.tsx @@ -0,0 +1,14 @@ +import { ChartDataLabelProps } from '../base/interfaces'; +import * as React from 'react'; + +/** + * @description Component for configuring data labels in a chart series + * @param {ChartDataLabelProps} props - Properties for configuring the data label + * @returns {null} This component doesn't render any UI elements but configures data label settings + */ +export const ChartDataLabel: React.FC = () => { + return null; +}; + +ChartDataLabel.displayName = 'ChartDataLabel'; + diff --git a/components/charts/src/chart/series/Marker.tsx b/components/charts/src/chart/series/Marker.tsx new file mode 100644 index 0000000..27b5dca --- /dev/null +++ b/components/charts/src/chart/series/Marker.tsx @@ -0,0 +1,20 @@ +import { ChartMarkerProps } from '../base/interfaces'; + +/** + * @typedef ChartMarkerProps + * @extends ChartMarkerProps + * @property {React.ReactNode} [children] - Optional content to be rendered inside the marker + * @private + */ +type ChartMarkerProperty = ChartMarkerProps & { + children?: React.ReactNode +}; + +/** + * @description Represents a marker in a chart series + * @param {ChartMarkerProps} props - Properties for configuring the chart marker + * @returns {JSX.Element} A React element that renders the marker with its children + */ +export const ChartMarker: React.FC = (props: ChartMarkerProperty) => { + return <>{props.children}; +}; diff --git a/components/charts/src/chart/series/Series.tsx b/components/charts/src/chart/series/Series.tsx new file mode 100644 index 0000000..80b35f2 --- /dev/null +++ b/components/charts/src/chart/series/Series.tsx @@ -0,0 +1,371 @@ +/** + * @module Chart/Series + */ +import * as React from 'react'; +import { useContext, useEffect, useRef } from 'react'; +import { ChartDataLabelProps, ChartMarkerProps, ChartSeriesProps, SeriesProps } from '../base/interfaces'; +import { ChartContext } from '../layout/ChartProvider'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartMarker } from './Marker'; + +/** + * Creates a replacer function for JSON.stringify that handles circular references. + * Uses a WeakSet to track seen objects and avoid serializing circular structures. + * + * @returns {function} A replacer function that can be used with JSON.stringify. + * @private + */ +export function getCircularReplacer(): (key: string, value: object | string | number | boolean | null) => + object | string | number | boolean | null | undefined { + const seen: WeakSet = new WeakSet(); + return function (_key: string, value: object | string | number | boolean | null): + object | string | number | boolean | null | undefined { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + } + // Don't try serializing React elements (too complex) + if (value && typeof value === 'object' && ( + ('$$typeof' in value) || + ('_owner' in value)) + ) { + return undefined; + } + return value; + }; +} + +/** + * Map used to track visited objects for circular reference detection during JSON serialization. + * + * @private + * @type {Map} + */ +export const visited: Map = new Map(); + +/** + * Provides a function that returns a JSON replacer function to handle circular references. + * Used when serializing complex objects to prevent circular reference errors. + * + * @returns {Function} A function that detects and handles circular references in objects during JSON serialization. + * @private + */ +export const replacerFunc: () => (key: string, value: object | string | number | boolean | null) => + object | string | number | boolean | null | undefined = () => { + return (_key: string, value: object | string | number | boolean | null) => { + if (typeof value === 'object' && value !== null) { + if (visited.has(value)) { + return '[Circular]'; + } + return; + } + return value; + }; +}; + +/** + * Interface defining the chart context type used for communication between chart components. + * + * @private + */ +interface ChartContextType { + /** + * Function to update the series collection in the chart. + * + * @param series - Array of series models to be rendered in the chart. + * @private + */ + setChartSeries: (series: ChartSeriesProps[]) => void; +} + +/** + * Component that manages a collection of chart series and transforms them into the format expected by the chart rendering engine. + * This component processes its children (individual ChartSeries components), extracts their properties, + * and passes the processed series data to the chart context. + * + * @param {SeriesProps} props - The properties for the series collection component. + * @returns {React.ReactElement|null} A React component that processes series definitions or null. + */ +export const ChartSeriesCollection: React.FC = (props: SeriesProps) => { + const context: ChartContextType | null = useContext(ChartContext); + const previousSeriesRef: React.RefObject = useRef([]); + const childArray: React.ReactNode[] = React.Children.toArray(props.children); + + /** + * Extracts a specific property from all chart series children and returns it as a JSON string. + * Used to track changes in specific series properties for dependency arrays in useEffect. + * + * @param {React.ReactNode[]} children - Array of React children nodes to process. + * @param {string} propertyName - Name of the property to extract from each series. + * @returns {string} JSON string representation of the extracted property values. + */ + const extractProperty: (children: React.ReactNode[], propertyName: string) => string = ( + children: React.ReactNode[], + propertyName: string + ): string => JSON.stringify(children.map((child: React.ReactNode) => + React.isValidElement(child) && child.type === ChartSeries + ? (child.props as ChartSeriesProperty)[propertyName as keyof ChartSeriesProperty] + : null + )); + + // Extract commonly changed properties from series for dependency tracking + const dataSourcesSignature: string = extractProperty(childArray, 'dataSource'); + const fill: string = extractProperty(childArray, 'fill'); + const width: string = extractProperty(childArray, 'width'); + const dashArray: string = extractProperty(childArray, 'dashArray'); + const opacity: string = extractProperty(childArray, 'opacity'); + const visible: string = extractProperty(childArray, 'visible'); + const splineType: string = extractProperty(childArray, 'splineType'); + const legendShape: string = extractProperty(childArray, 'legendShape'); + const pointColorMapping: string = extractProperty(childArray, 'pointColorMapping'); + + /** + * Extracts primitive properties from an object, ignoring objects, functions, and the 'children' property. + * Used to get simple property values from component props. + * + * @param {ChartSeriesProperty} obj - The source object to extract properties from. + * @returns {Partial} A new object containing only the primitive properties from the source object. + */ + function pickPrimitiveProps(obj: ChartSeriesProperty): Partial { + const copy: Partial = {}; + for (const [key, val] of Object.entries(obj)) { + if (key === 'children') { continue; } + if (typeof val !== 'object' && typeof val !== 'function') { + (copy as Record)[key as string] = val as string | number | boolean; + } + } + return copy; + } + + /** + * String representation of marker configurations for all series. + * Used to track changes in marker properties for dependency arrays in useEffect. + */ + const markerSignature: string = JSON.stringify( + childArray.map((child: React.ReactNode) => { + if ( + React.isValidElement(child) && + child.type === ChartSeries && + (child.props as ChartSeriesProperty).children + ) { + let mSignature: Partial = {}; + React.Children.forEach( + (child.props as ChartSeriesProperty).children, + (markerChild: React.ReactNode) => { + if (React.isValidElement(markerChild) && markerChild.type === ChartMarker) { + mSignature = { + ...mSignature, + ...pickPrimitiveProps(markerChild.props as ChartSeriesProperty) + }; + } + } + ); + return mSignature; + } + return null; + }) + ); + + /** + * Extracts and processes the series array from child components. + * This core method transforms React component hierarchy into a data structure + * that can be consumed by the chart rendering engine. + * @private + * @returns {Series[]} Array of processed Series objects ready for rendering. + */ + const getSeriesArray: () => ChartSeriesProps[] = (): ChartSeriesProps[] => { + return childArray + .map((child: React.ReactNode) => { + if (!React.isValidElement(child) || child.type !== ChartSeries) { return null; } + + const seriesProps: ChartSeriesProps = { + ...defaultChartConfigs.ChartSeries, + ...(child.props as ChartSeriesProperty), + ...defaultChartConfigs.ChartSeries + } as ChartSeriesProps; + + // Get user props from the child + const childProps: ChartSeriesProperty = child.props as ChartSeriesProperty; + + // Deep merge for border property to maintain default values + if (childProps.border && defaultChartConfigs.ChartSeries.border) { + seriesProps.border = { + ...defaultChartConfigs.ChartSeries.border, + ...childProps.border + }; + const { border, ...restProps } = childProps; + Object.assign(seriesProps, restProps); + } else if (childProps.animation && defaultChartConfigs.ChartSeries.animation) { + seriesProps.animation = { + ...defaultChartConfigs.ChartSeries.animation, + ...childProps.animation + }; + const { animation, ...restProps } = childProps; + Object.assign(seriesProps, restProps); + } else { + Object.assign(seriesProps, childProps); + } + + // Process marker and data label configuration + React.Children.forEach( + (child.props as ChartSeriesProperty).children, + (markerChild: React.ReactNode) => { + if (React.isValidElement(markerChild) && markerChild.type === ChartMarker) { + const { children: markerChildren, ...markerProps } = markerChild.props as ChartSeriesProperty; + + const markerConfig: ChartMarkerProps = { + ...defaultChartConfigs.ChartSeries.marker, + ...markerProps + } as ChartMarkerProps; + + React.Children.forEach(markerChildren, (dataLabelChild: React.ReactNode) => { + if (React.isValidElement(dataLabelChild)) { + const type: React.ElementType = dataLabelChild.type as + React.ElementType; + + const isNamedComponent: boolean = + (typeof type === 'function') && + ('displayName' in type) && + ((type as { displayName?: string }).displayName === 'ChartDataLabel'); + + if (isNamedComponent) { + markerConfig.dataLabel = { + ...defaultChartConfigs.ChartSeries.marker?.dataLabel, + ...(dataLabelChild.props as ChartDataLabelProps) + }; + markerConfig.dataLabel.font = { + ...defaultChartConfigs.ChartSeries.marker?.dataLabel?.font, + ...((dataLabelChild.props as ChartDataLabelProps).font) + }; + } + } + }); + + seriesProps.marker = markerConfig; + } + } + ); + + // Create a clean copy without internal properties + const seriesCopy: ChartSeriesProps = { ...seriesProps }; + delete (seriesCopy as Record).chart; + delete (seriesCopy as Record).series; + delete (seriesCopy as Record).points; + + return seriesCopy; + }) + .filter((s: ChartSeriesProps | null): s is ChartSeriesProps => s !== null); + }; + + /** + * Creates a deep signature of all series components, their properties, and nested children. + * This signature is used to determine when the series configuration has fundamentally changed + * and needs to be reprocessed. + */ + const deepSignature: string = JSON.stringify( + childArray.map((child: React.ReactNode) => { + if (!React.isValidElement(child)) { return null; } + const typeName: string = typeof child.type === 'string' + ? child.type + : ((child.type as { name: string }).name); + const seriesPropsSignature: ChartSeriesProperty = + typeof child.props === 'object' && child.props !== null + ? { ...(child.props as ChartSeriesProperty) } + : {} as ChartSeriesProperty; + // If this is a ChartSeries, check for marker & dataLabel children + if (child.type === ChartSeries) { + React.Children.forEach( + (child.props as { children?: React.ReactNode }).children, + (markerChild: React.ReactNode) => { + if (React.isValidElement(markerChild) && markerChild.type === ChartMarker) { + const markerProps: ChartMarkerProps = { ...markerChild.props as ChartMarkerProps }; + // Look for a ChartDataLabel inside marker + if ((markerChild.props as ChartSeriesProperty).children) { + React.Children.forEach( + (markerChild.props as ChartSeriesProperty).children, + (dlChild: React.ReactNode) => { + if (React.isValidElement(dlChild)) { + const type: React.ElementType = + dlChild.type as React.ElementType; + const isNamedComponent: boolean = + (typeof type === 'function') && + ('displayName' in type) && + ((type as { name: string }).name === 'ChartDataLabel'); + if (isNamedComponent) { + markerProps.dataLabel = dlChild.props as ChartDataLabelProps; + } + } + } + ); + } + seriesPropsSignature.marker = markerProps; + } + } + ); + } + return { typeName, ...seriesPropsSignature }; + }), + getCircularReplacer() + ); + + /** + * Effect that performs a deep comparison of series data and updates the chart only when necessary. + * Prevents unnecessary re-renders by checking if the series array has actually changed. + */ + useEffect(() => { + const seriesArray: ChartSeriesProps[] = getSeriesArray(); + visited.clear(); + const shouldUpdate: boolean = + JSON.stringify(previousSeriesRef.current, replacerFunc()) !== + JSON.stringify(seriesArray, replacerFunc()); + + if (shouldUpdate) { + previousSeriesRef.current = seriesArray; + context?.setChartSeries(seriesArray); + } + }, [deepSignature]); + + /** + * Effect that updates the chart series whenever key properties change. + * This ensures the chart reflects changes to visual properties like color, width, and visibility. + */ + useEffect(() => { + const seriesArray: ChartSeriesProps[] = getSeriesArray(); + context?.setChartSeries(seriesArray); + }, [ + dataSourcesSignature, + fill, + width, + dashArray, + opacity, + visible, + markerSignature, + deepSignature, + splineType, + legendShape, + pointColorMapping + ]); + + // The component itself doesn't render anything visible + return null; +}; + +/** + * Type definition for ChartSeries props, extending the Series with optional children. + */ +type ChartSeriesProperty = ChartSeriesProps & { children?: React.ReactNode }; + +/** + * Component representing a single series in the chart. + * This is a container component that holds configuration for one data series + * and can contain child components like ChartMarker and ChartDataLabel. + * + * @param {ChartSeriesProps} props - The properties for the chart series. + * @param {React.ReactNode} [props.children] - Optional child components for this series. + * @returns {JSX.Element} A React component that renders its children. + */ +export const ChartSeries: React.FC = ({ children }: ChartSeriesProperty) => { + return <>{children}; +}; diff --git a/components/charts/src/chart/utils/constants.ts b/components/charts/src/chart/utils/constants.ts new file mode 100644 index 0000000..5b0c88c --- /dev/null +++ b/components/charts/src/chart/utils/constants.ts @@ -0,0 +1,20 @@ +/** + * Constants used throughout the chart component + */ + +// Layout and spacing constants +export const DEFAULT_PADDING: number = 5; +export const CHART_AREA_PADDING: number = 10; + +// Contrast ratio calculation constants for accessibility +export const CONTRAST_RATIO: { + RED_FACTOR: number; + GREEN_FACTOR: number; + BLUE_FACTOR: number; + DIVISOR: number; +} = { + RED_FACTOR: 299, + GREEN_FACTOR: 587, + BLUE_FACTOR: 114, + DIVISOR: 1000 +}; diff --git a/components/charts/src/chart/utils/getData.tsx b/components/charts/src/chart/utils/getData.tsx new file mode 100644 index 0000000..fdd4b68 --- /dev/null +++ b/components/charts/src/chart/utils/getData.tsx @@ -0,0 +1,201 @@ +import { Chart, Points, Rect, SeriesProperties } from '../chart-area/chart-interfaces'; +import { withInBounds } from './helper'; + +/** + * Interface for point data result + * @private + */ +export interface PointData { + point: Points | null; + series: SeriesProperties | null; +} + +/** + * Gets the data point at the current mouse position. + * + * @param {Chart} chart - The chart instance + * @returns {PointData} Object containing point and series data + * @private + */ +export function getData(chart: Chart): PointData { + let point: Points | null = null; + let series: SeriesProperties | null = null; + let width: number; + let height: number; + let mouseX: number; + let mouseY: number; + const insideRegion: boolean = false; + + // Search through all visible series for point at current mouse position + for (let i: number = chart.visibleSeries.length - 1; i >= 0; i--) { + series = chart.visibleSeries[i as number]; + width = (series.type === 'Scatter' || (series.marker?.visible)) + ? (series.marker?.height ?? 0 + 5) / 2 : 0; + height = (series.type === 'Scatter' || (series.marker?.visible)) + ? (series.marker?.width ?? 0 + 5) / 2 : 0; + mouseX = chart.mouseX; + mouseY = chart.mouseY; + + + // Check if point is within bounds + if (series.visible && series.clipRect && withInBounds(mouseX, mouseY, series.clipRect, width, height)) { + point = getRectPoint(series, series.clipRect, mouseX, mouseY, insideRegion); + if (point) { + break; + } + } + } + + return { point, series }; +} + +/** + * Finds a point in the given rectangle + * + * @param {Series} series - The current series + * @param {Rect} rect - The rectangle to search in + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {boolean} insideRegion - Flag to track if inside a region + * @returns {Points | null} Point if found, null otherwise + *@private + */ +function getRectPoint( + series: SeriesProperties, + rect: Rect, + x: number, + y: number, + insideRegion: boolean +): Points | null { + + + for (const point of series.points) { + if (!point.regionData) { + if (!point.regions || !point.regions.length) { + continue; + } + } + + if ((series.isRectSeries && series.marker?.visible )) { + if (isPointInThresholdRegion(x, y, point, rect, series)) { + return point; + } + } + + if (!insideRegion && checkRegionContainsPoint(point?.regions, rect, x, y)) { + return point; + } else if (insideRegion && checkRegionContainsPoint(point?.regions, rect, x, y)) { + return point; + } + } + + return null; +} + +/** + * Check if a point exists within the specified regions + * + * @param {Rect[]} regionRect - Array of rectangles defining regions + * @param {Rect} rect - The chart rectangle + * @param {number} x - X coordinate to check + * @param {number} y - Y coordinate to check + * @param {Chart} chart - The chart instance + * @returns {boolean} True if point is in region + * @private + */ +function checkRegionContainsPoint( + regionRect: Rect[] | null, + rect: Rect, + x: number, + y: number +): boolean | undefined { + return regionRect?.some((region: Rect) => { + return withInBounds( + x, y, + { + x: (rect.x) + region.x, + y: ( rect.y) + region.y, + width: region.width, + height: region.height + } + ); + }); +} + +/** + * Checks if the given point is within the threshold region of a data point + * + * @param {number} x - The x-coordinate of the point to check + * @param {number} y - The y-coordinate of the point to check + * @param {Points} point - The data point + * @param {Rect} rect - The rectangle representing the threshold region + * @param {Series} series - The series to which the data point belongs + * @param {Chart} chart - The chart instance + * @returns {boolean} True if the point is within the threshold region + * @private + */ +function isPointInThresholdRegion( + x: number, + y: number, + point: Points, + rect: Rect, + series: SeriesProperties +): boolean { + if (!point.regions || point.regions.length === 0) { + return false; + } + const isBar: boolean = series.type === 'Bar'; + const isInversed: boolean = series.yAxis.isAxisInverse; + const isTransposed: boolean = series.chart.iSTransPosed; + const heightValue: number = 10; + let yValue: number = 0; + let xValue: number = 0; + let width: number; + let height: number = width = 2 * heightValue; + + if (isInversed && isTransposed) { + if (isBar) { + yValue = point.regions[0].height - heightValue; + width = point.regions[0].width; + } else { + xValue = -heightValue; + height = point.regions[0].height; + } + } else if (isInversed || point.yValue! < 0) { + if (isBar) { + xValue = -heightValue; + height = point.regions[0].height; + } else { + yValue = point.regions[0].height - heightValue; + width = point.regions[0].width; + } + } else if (isTransposed) { + if (isBar) { + yValue = -heightValue; + width = point.regions[0].width; + } else { + xValue = point.regions[0].width - heightValue; + height = point.regions[0].height; + } + } else { + if (isBar) { + xValue = point.regions[0].width - heightValue; + height = point.regions[0].height; + } else { + yValue = -heightValue; + width = point.regions[0].width; + } + } + + return point?.regions?.some((region: Rect) => { + return withInBounds( + x, y, + { + x: (rect.x ) + region.x + xValue, + y: ( rect.y ) + region.y + yValue, + width, + height + } + ); + }); +} diff --git a/components/charts/src/chart/utils/helper.tsx b/components/charts/src/chart/utils/helper.tsx new file mode 100644 index 0000000..06b506d --- /dev/null +++ b/components/charts/src/chart/utils/helper.tsx @@ -0,0 +1,1740 @@ +import { LabelPosition, TextOverflow, TitlePosition } from '../base/enum'; +import { ChartBorderProps, ChartSeriesProps, ChartFontProps, ChartLocationProps} from '../base/interfaces'; +import { getNumberFormat, HorizontalAlignment, isNullOrUndefined, merge, NumberFormatOptions } from '@syncfusion/react-base'; +import { AxisTextStyle } from '../chart-axis/base'; +import { extend } from '@syncfusion/react-base'; +import { RectOption } from '../base/Legend-base'; +import { AxisModel, Chart, ColumnProps, MarginModel, PathOptions, Points, Rect, RowProps, SeriesProperties, ChartSizeProps, TextOption, TextStyleModel, VisibleRangeProps } from '../chart-area/chart-interfaces'; + +/** + * Measures the size of the given text using the specified font and theme font style. + * + * @param {string} text - The text to measure. + * @param {TextStyleModel} font - The font style used for measuring the text. + * @param {TextStyleModel} themeFontStyle - Additional theme font styles that could influence text rendering. + * @returns {Size} The calculated size of the text, including width and height dimensions. + * @private + */ +export function measureText(text: string, font: TextStyleModel, themeFontStyle: TextStyleModel): ChartSizeProps { + const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const textEl: SVGTextElement = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + textEl.textContent = text; + + const fontStyle: string = font.fontStyle || themeFontStyle.fontStyle; + const fontWeight: string = font.fontWeight || themeFontStyle.fontWeight; + const fontSize: string = font.fontSize || themeFontStyle.fontSize; + const fontFamily: string = font.fontFamily || themeFontStyle.fontFamily; + + textEl.style.fontStyle = fontStyle; + textEl.style.fontWeight = fontWeight; + textEl.style.fontSize = fontSize; + textEl.style.fontFamily = fontFamily; + + svg.appendChild(textEl); + svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + svg.setAttribute('width', '0'); + svg.setAttribute('height', '0'); + svg.style.position = 'absolute'; + svg.style.visibility = 'hidden'; + svg.style.pointerEvents = 'none'; + + document.body.appendChild(svg); + const bbox: DOMRect = textEl.getBoundingClientRect(); + document.body.removeChild(svg); + return { width: bbox.width, height: bbox.height }; +} + +/** + * Calculates the horizontal position of a chart title based on the alignment setting. + * + * @param {Rect} rect - The rectangle object representing the position and size of the container. + * @param {Alignment} textAlignment - The settings for the title's style, including text alignment. + * @returns {number} The x-coordinate for the title position. + * @private + */ +export function titlePositionX(rect: Rect, textAlignment: HorizontalAlignment): number { + if (textAlignment === 'Left') { + return rect.x; + } else if (textAlignment === 'Center') { + return rect.x + rect.width / 2; + } else { + return rect.x + rect.width; + } +} + +/** + * Trims the text to fit within the specified width. + * + * @param {number} maxWidth - The maximum width allowed for the text. + * @param {string} text - The text string to trim. + * @param {TextStyleModel} font - The font style used for the text. + * @param {boolean} isRtlEnabled - Boolean indicating if right-to-left text is enabled. + * @param {TextStyleModel} themeFontStyle - The theme font style. + * @returns {string} - The trimmed text that fits within the specified width. + * @private + */ +export function useTextTrim( + maxWidth: number, text: string, font: TextStyleModel, + isRtlEnabled: boolean, themeFontStyle: TextStyleModel): string { + let label: string = text; + let size: number = measureText(text, font, themeFontStyle).width; + + if (size > maxWidth) { + for (let i: number = text.length - 1; i >= 0; --i) { + label = isRtlEnabled ? '...' + text.substring(0, i) : text.substring(0, i) + '...'; + size = measureText(label, font, themeFontStyle).width; + if (size <= maxWidth) { + return label; + } + } + } + return label; +} + +/** + * Wraps the input text into multiple lines based on the specified maximum width and font style. + * + * @param {string} currentLabel - The text to be wrapped. + * @param {number} maximumWidth - The maximum width allowed for each line of text. + * @param {TextStyleModel} font - The font style used for rendering the text. + * @param {boolean} isRtlEnabled - Specifies whether right-to-left text direction is enabled. + * @param {TextStyleModel} [themeFontStyle] - The font style used as the base for the text wrapping operation. + * @param {boolean} [clip=false] - Specifies whether text exceeding the maximum width should be clipped. + * @param {number} [maximumLabelHeight] - The total height available for the wrapped text. + * @returns {string[]} An array of strings representing the wrapped lines of text. + * @private + */ +export function useTextWrap( + currentLabel: string, maximumWidth: number, font: TextStyleModel, isRtlEnabled: boolean, themeFontStyle: TextStyleModel, + clip?: boolean, maximumLabelHeight?: number): string[] { + const textCollection: string[] = currentLabel.split(' '); + let label: string = ''; + const labelCollection: string[] = []; + let text: string; + let textHeight: number = 0; + for (let i: number = 0, len: number = textCollection.length; i < len; i++) { + text = textCollection[i as number]; + const measuredWidth: number = measureText(label + (label === '' ? '' : ' ' + text), font, themeFontStyle).width; + void (measuredWidth < maximumWidth + ? label = label + (label === '' ? '' : ' ') + text + : maximumLabelHeight + ? ( + (label !== '' && textHeight < maximumLabelHeight) + ? ( + labelCollection.push( + clip ? label : useTextTrim(maximumWidth, label, font, isRtlEnabled, themeFontStyle) + ), + label = text + ) + : textHeight < maximumLabelHeight + ? ( + labelCollection.push( + clip ? text : useTextTrim(maximumWidth, text, font, isRtlEnabled, themeFontStyle) + ), + text = '' + ) + : null + ) + : ( + label !== '' + ? ( + labelCollection.push( + clip ? label : useTextTrim(maximumWidth, label, font, isRtlEnabled, themeFontStyle) + ), + label = text + ) + : ( + labelCollection.push( + clip ? text : useTextTrim(maximumWidth, text, font, isRtlEnabled, themeFontStyle) + ), + text = '' + ) + )); + + void (label && i === len - 1 && labelCollection.push( + clip ? label : useTextTrim(maximumWidth, label, font, isRtlEnabled, themeFontStyle) + )); + textHeight += measureText(text as string, font, themeFontStyle).width; + } + return labelCollection; +} + +/** + * Retrieves the title text for display, considering styling and overflow settings. + * + * @param {string} title - The title text to be processed. + * @param {TextStyleModel} style - The style to be applied to the title text. + * @param {number} width - The width constraint for displaying the title. + * @param {boolean} isRtlEnabled - Flag indicating if right-to-left text is enabled. + * @param {TextStyleModel} themeFontStyle - Additional font styling based on the theme. + * @param {TextOverflow} textOverflow - The overflow strategy for handling long text. + * @returns {string[]} An array of strings representing the processed title text. + * @private + */ +export function getTitle( + title: string, style: TextStyleModel, width: number, isRtlEnabled: boolean, + themeFontStyle: TextStyleModel, textOverflow: TextOverflow): string[] { + let titleCollection: string[] = []; + switch (textOverflow) { + case 'Wrap': + titleCollection = useTextWrap(title, width, style, isRtlEnabled, themeFontStyle, title.indexOf(' ') < 0 ? true : false); + break; + case 'Trim': + titleCollection.push(useTextTrim(width, title, style, isRtlEnabled, themeFontStyle)); + break; + default: + titleCollection.push(title); + break; + } + return titleCollection; +} + +/** + * Converts a string to a number based on the given container size. + * + * @param {string | null | undefined} value - The string value to convert to a number. + * @param {number} containerSize - The size of the container, used as a context for conversion. + * @returns {number} The converted number. Returns 0 if the input value is null or undefined. + * @private + */ +export function stringToNumber(value: string | null | undefined, containerSize: number): number { + if (!value) { return 0; } // Return 0 instead of null to avoid type issues + if (value.includes('%')) { + const percentage: number = parseFloat(value); + return isNaN(percentage) ? 0 : (containerSize / 100) * percentage; + } + const numberValue: number = parseFloat(value); + return isNaN(numberValue) ? 0 : numberValue; +} + +/** + * Determines the text anchor position based on the alignment, RTL setting, and title position. + * + * @param {HorizontalAlignment} alignment - The alignment setting for the text. + * @param {boolean} enableRtl - A boolean that represents whether right-to-left text is enabled. + * @param {TitlePosition} position - The position where the title is placed (e.g., 'Left', 'Center', etc.). + * @returns {string} The computed text anchor for the given alignment, RTL, and position. + * @private + */ +export function getTextAnchor(alignment: HorizontalAlignment, enableRtl: boolean, position: TitlePosition): string { + if (position === 'Left') { + let anchor: string = alignment === 'Left' ? 'end' : alignment === 'Right' ? 'start' : 'middle'; + anchor = enableRtl ? (anchor === 'end' ? 'start' : anchor === 'start' ? 'end' : anchor) : anchor; + return anchor; + } + if (position === 'Right') { + let anchor: string = alignment === 'Left' ? 'start' : alignment === 'Right' ? 'end' : 'middle'; + anchor = enableRtl ? (anchor === 'end' ? 'start' : anchor === 'start' ? 'end' : anchor) : anchor; + return anchor; + } + if (enableRtl) { + return alignment === 'Left' ? 'end' : alignment === 'Right' ? 'start' : 'middle'; + } + return alignment === 'Left' ? 'start' : alignment === 'Right' ? 'end' : 'middle'; +} + +/** + * Checks if both minimum and maximum values of the axis are set. + * + * @param {AxisModel} axis - The axis model object containing axis properties. + * @returns {boolean} True if both minimum and maximum values are defined; otherwise, false. + * @private + */ +export function setRange(axis: AxisModel): boolean { + return (axis.minimum != null && axis.maximum != null); +} + +/** + * Calculates the actual number of desired intervals for the given axis based on the available size. + * + * @param {ChartSizeProps} availableSize - The size available for rendering the axis, with properties for width and height. + * @param {AxisModel} axis - The axis model object that contains properties for defining the axis. + * @returns {number} The calculated number of intervals adjusted for the available space. + * @private + */ +export function getActualDesiredIntervalsCount(availableSize: ChartSizeProps, axis: AxisModel): number { + + const size: number = axis.orientation === 'Horizontal' ? availableSize.width : availableSize.height; + if (isNullOrUndefined(axis.desiredIntervals)) { + let desiredIntervalsCount: number = (axis.orientation === 'Horizontal' ? 0.533 : 1) * (axis.maxLabelDensity as number); + desiredIntervalsCount = Math.max((size * (desiredIntervalsCount / 100)), 1); + return desiredIntervalsCount; + } else { + return axis.desiredIntervals as number; + } +} + +/** + * Computes the logarithm of a value to the specified base. + * + * @param {number} value - The numerical value to compute the logarithm for. + * @param {number} base - The base to which the logarithm should be computed. + * @returns {number} The logarithm of the value to the specified base. + * @private + */ +export function logBase(value: number, base: number): number { + return Math.log(value) / Math.log(base); +} + +/** + * Determines if a value is within a specified visible range. + * + * @param {number} value - The value to check against the specified range. + * @param {VisibleRangeProps} range - The range object containing minimum and maximum boundaries. + * @returns {boolean} True if the value is within the specified range; otherwise, false. + * @private + */ +export function withIn(value: number, range: VisibleRangeProps): boolean { + return (value <= range.maximum) && (value >= range.minimum); +} + +/** + * Checks if a label contains a line break. + * + * @param {string} label - The label to check. + * @returns {boolean} - True if the label contains a line break, otherwise false. + * @private + */ +export function isBreakLabel(label: string): boolean { + return label.indexOf('
') !== -1; +} + +/** + * Converts a value to a coefficient relative to a specified axis. + * + * @param {number} value - The numerical value to convert to a coefficient. + * @param {AxisModel} axis - The axis model containing the visible range information. + * @returns {number} The coefficient representing the position of the value relative to the axis range. + * @private + */ +export function valueToCoefficient(value: number, axis: AxisModel): number { + const range: VisibleRangeProps = axis.visibleRange; + const result: number = (value - range.minimum) / (range.delta); + const isInverse: boolean = axis.isAxisInverse as boolean; + return isInverse ? (1 - result) : result; +} + + +/** + * Converts the first character of a string to lowercase. + * + * @param {string} str - The string to convert. + * @returns {string} The converted string. + * @private + */ +export function firstToLowerCase(str: string): string { + return str.substr(0, 1).toLowerCase() + str.substr(1); +} + +/** + * Extracts and returns a list of visible points from the given series. + * + * @param {SeriesProperties} series - The series object containing an array of points. + * @returns {Points[]} An array of visible points cloned from the series. + * @private + */ +export function useVisiblePoints(series: SeriesProperties): Points[] { + + const points: Points[] = []; + series.points.map((point: Points) => { + points.push(extend({}, point) as Points); + }); + const tempPoints: Points[] = []; + let tempPoint: Points; + let pointIndex: number = 0; + for (let i: number = 0; i < points.length; i++) { + tempPoint = points[i as number]; + if (isNullOrUndefined(tempPoint.x)) { + continue; + } else { + tempPoint.index = pointIndex++; + tempPoints.push(tempPoint); + } + } + return tempPoints; +} + + +/** + * Calculates the chart coordinates (in pixels) for a given data point based on the X and Y axes. + * + * @param {number} x - The X value of the data point. + * @param {number} y - The Y value of the data point. + * @param {AxisModel} xAxis - The X-axis model used to scale the X value. + * @param {AxisModel} yAxis - The Y-axis model used to scale the Y value. + * @param {boolean} [isInverted=false] - Optional flag indicating whether the chart is inverted (horizontal orientation). + * @returns {ChartLocationProps} The pixel coordinates representing the location of the data point on the chart. + * @private + */ +export function getPoint( + x: number, + y: number, + xAxis: AxisModel, + yAxis: AxisModel, + isInverted?: boolean +): ChartLocationProps { + x = ((xAxis.valueType === 'Logarithmic') ? + logBase(((x > 0) ? x : Math.pow(xAxis.logBase as number, xAxis.visibleRange.minimum)), xAxis.logBase as number) : x); + y = ((yAxis.valueType === 'Logarithmic') ? + logBase(((y > 0) ? y : Math.pow(yAxis.logBase as number, yAxis.visibleRange.minimum)), yAxis.logBase as number) : y); + x = valueToCoefficient(x, xAxis); + y = valueToCoefficient(y, yAxis); + const xLength: number = (isInverted ? xAxis.rect.height : xAxis.rect.width); + const yLength: number = (isInverted ? yAxis.rect.width : yAxis.rect.height); + const locationX: number = isInverted ? y * (yLength) : x * (xLength); + const locationY: number = isInverted ? (1 - x) * (xLength) : (1 - y) * (yLength); + return { x: locationX, y: locationY }; +} + +/** + * Determines if a value is strictly inside a specified visible range, excluding the boundaries. + * + * @param {number} value - The numerical value to check against the specified range. + * @param {VisibleRangeProps} range - The range object containing minimum and maximum limits. + * @returns {boolean} True if the value is strictly within the specified range (not equal to min or max); otherwise, false. + * @private + */ +export function inside(value: number, range: VisibleRangeProps): boolean { + return (value < range.maximum) && (value > range.minimum); +} + +/** + * Calculates the size of text when rotated at a specified angle. + * + * @param {string} text - The text to measure. + * @param {TextStyleModel} font - The font settings to be used for the text. + * @param {number} angle - The angle at which the text is rotated. + * @param {TextStyleModel} themeFont - The theme font settings to apply. + * @returns {Size} The dimensions of the rotated text. + * @private + */ +export function getRotatedTextSize(text: string, font: TextStyleModel, angle: number, themeFont: TextStyleModel): ChartSizeProps { + const textLines: string[] = isBreakLabel(text) ? text.split('
') : [text]; + + let maxWidth: number = 0; + let totalHeight: number = 0; + //let lineHeight = 0; + + for (const line of textLines) { + const size: ChartSizeProps = measureText(line, font, themeFont); + maxWidth = Math.max(maxWidth, size.width); + totalHeight += size.height; + //lineHeight = size.height; // last one used for spacing below + } + + // Add small line padding to simulate spacing + const totalWithPadding: number = totalHeight + ((textLines.length - 1) * 2); // adjust padding here as needed + + // Rotation math + const radians: number = (angle * Math.PI) / 180; + const rotatedWidth: number = Math.abs(Math.cos(radians) * maxWidth) + Math.abs(Math.sin(radians) * totalWithPadding); + const rotatedHeight: number = Math.abs(Math.sin(radians) * maxWidth) + Math.abs(Math.cos(radians) * totalWithPadding); + return { width: rotatedWidth, height: rotatedHeight }; +} + + +/** + * Calculates the maximum width and height required to display an array of text strings + * using the specified text and default font styles. + * + * @param {string[]} texts - An array of text strings whose maximum size is to be determined. + * @param {TextStyleModel} textStyle - The font style to apply to each text string. + * @param {TextStyleModel} defaultFont - The default font style to fall back on if necessary. + * @returns {ChartSizeProps} An object containing the maximum width and height required for the text strings. + * @private + */ +export function getMaxTextSize(texts: string[], textStyle: TextStyleModel, defaultFont: TextStyleModel): ChartSizeProps { + let maxWidth: number = 0; + let maxHeight: number = 0; + + texts.forEach((text: string) => { + const size: ChartSizeProps = measureText(text, textStyle, defaultFont); + maxWidth = Math.max(maxWidth, size.width); + maxHeight = Math.max(maxHeight, size.height); + }); + + return { width: maxWidth, height: maxHeight }; +} + +/** + * Calculates the maximum size (width and height) of an array of text strings when rotated at a given angle. + * + * @param {string[]} texts - An array of text strings for which the maximum size needs to be computed. + * @param {number} angle - The angle at which the text is to be rotated. + * @param {TextStyleModel} textStyle - The style settings for the text. + * @param {TextStyleModel} defaultFont - The default font styling to use. + * @returns {ChartSizeProps} An object representing the maximum width and height of the rotated text. + * @private + */ +export function getMaxRotatedTextSize( + texts: string[], angle: number, textStyle: TextStyleModel, defaultFont: TextStyleModel): ChartSizeProps { + let maxWidth: number = 0; + let maxHeight: number = 0; + texts.forEach((text: string) => { + const size: ChartSizeProps = getRotatedTextSize(text, textStyle, angle, defaultFont); + maxWidth = Math.max(maxWidth, size.width); + maxHeight = Math.max(maxHeight, size.height); + }); + + return { width: maxWidth, height: maxHeight }; +} + +export const getPathLength: (d: string) => number = (d: string) => { + // If path contains Bezier curves (spline series) + if (d.includes('C')) { + return approximateBezierCurveLength(d); + } + const commands: RegExpMatchArray | null = d.match(/[ML][^ML]*/g); + let totalLength: number = 0; + let prevX: number = 0; + let prevY: number = 0; + commands?.forEach((command: string, i: number) => { + const coords: number[] = command.slice(1).trim().split(' ').map(Number); + const [x, y] = coords; + if (i === 0) { + prevX = x; + prevY = y; + } else { + totalLength += Math.sqrt(Math.pow(x - prevX, 2) + Math.pow(y - prevY, 2)); + } + prevX = x; + prevY = y; + }); + + return totalLength; +}; + +/** + * Calculates approximate length of a path containing Bezier curves + * + * @param {string} d - SVG path data + * @returns {number} - Approximate length of the path + * @private + */ +function approximateBezierCurveLength(d: string): number { + // Parse path into segments + const segments: Array<{command: string; params: number[]}> = parsePathSegments(d); + let totalLength: number = 0; + + // Current position tracker + let currentX: number = 0; + let currentY: number = 0; + + for (const segment of segments) { + const { command, params } = segment; + + switch (command) { + case 'M': // Move to + currentX = params[0]; + currentY = params[1]; + break; + + case 'L': {// Line to + const dx: number = params[0] - currentX; + const dy: number = params[1] - currentY; + totalLength += Math.sqrt(dx * dx + dy * dy); + currentX = params[0]; + currentY = params[1]; + break; + } + + case 'C': // Cubic Bezier + // Cubic Bezier has 6 params: [x1, y1, x2, y2, x, y] + totalLength += cubicBezierLength( + currentX, currentY, + params[0], params[1], + params[2], params[3], + params[4], params[5], + 10 // Sample points - higher for better accuracy + ); + currentX = params[4]; // End x + currentY = params[5]; // End y + break; + } + } + + return totalLength; +} + +/** + * Parse SVG path data into command segments + * + * @param {string} d - SVG path data + * @returns {Array<{command: string, params: number[]}>} - Parsed segments + * @private + */ +function parsePathSegments(d: string): Array<{command: string; params: number[]}> { + const segments: Array<{command: string; params: number[]}> = []; + const commands: RegExpMatchArray | [] = d.match(/([MLCc])([^MLCc]*)/g) || []; + + for (const cmd of commands) { + const command: string = cmd[0]; + const paramStr: string = cmd.substring(1).trim(); + const params: number[] = paramStr.split(/[\s,]+/).map(Number).filter((n: number) => !isNaN(n)); + + segments.push({ + command, + params + }); + } + + return segments; +} + +/** + * Calculate approximate length of a cubic Bezier curve using sampling + * + * @param {number} x0 - Start x + * @param {number} y0 - Start y + * @param {number} x1 - Control point 1 x + * @param {number} y1 - Control point 1 y + * @param {number} x2 - Control point 2 x + * @param {number} y2 - Control point 2 y + * @param {number} x3 - End x + * @param {number} y3 - End y + * @param {number} samples - Number of sample points + * @returns {number} - Approximate curve length + * @private + */ +function cubicBezierLength( + x0: number, y0: number, + x1: number, y1: number, + x2: number, y2: number, + x3: number, y3: number, + samples: number +): number { + let length: number = 0; + let prevX: number = x0; + let prevY: number = y0; + + // Sample points along the curve + for (let i: number = 1; i <= samples; i++) { + const t: number = i / samples; + + // Cubic Bezier formula + const t1: number = 1 - t; + const tOneThird: number = t1 * t1 * t1; + const tOneSecondT: number = 3 * t1 * t1 * t; + const tOneT2: number = 3 * t1 * t * t; + const tThree: number = t * t * t; + + const x: number = tOneThird * x0 + tOneSecondT * x1 + tOneT2 * x2 + tThree * x3; + const y: number = tOneThird * y0 + tOneSecondT * y1 + tOneT2 * y2 + tThree * y3; + + // Add distance between sample points + const dx: number = x - prevX; + const dy: number = y - prevY; + length += Math.sqrt(dx * dx + dy * dy); + + prevX = x; + prevY = y; + } + + return length; +} + +/** + * Converts a degree value to radians. + * + * @param {number} degree - The degree value to convert. + * @returns {number} - The equivalent value in radians. + * @private + */ +export function degreeToRadian(degree: number): number { + return degree * (Math.PI / 180); +} + +/** + * Helper function to determine whether there is an intersection between the two polygons described + * by the lists of vertices. Uses the Separating Axis Theorem. + * + * @param {ChartLocationProps[]} a an array of connected points [{x:, y:}, {x:, y:},...] that form a closed polygon. + * @param {ChartLocationProps[]} b an array of connected points [{x:, y:}, {x:, y:},...] that form a closed polygon. + * @returns {boolean} if there is any intersection between the 2 polygons, false otherwise. + * @private + */ +export function isRotatedRectIntersect(a: ChartLocationProps[], b: ChartLocationProps[]): boolean { + const polygons: ChartLocationProps[][] = [a, b]; + let minA: number; let maxA: number; let projected: number; let i: number; + let i1: number; let j: number; let minB: number; let maxB: number; + + for (i = 0; i < polygons.length; i++) { + + // for each polygon, look at each edge of the polygon, and determine if it separates + // the two shapes + const polygon: ChartLocationProps[] = polygons[i as number]; + for (i1 = 0; i1 < polygon.length; i1++) { + + // grab 2 vertices to create an edge + const i2: number = (i1 + 1) % polygon.length; + const p1: ChartLocationProps = polygon[i1 as number]; + const p2: ChartLocationProps = polygon[i2 as number]; + + // find the line perpendicular to this edge + const normal: ChartLocationProps = { x: p2.y - p1.y, y: p1.x - p2.x }; + + minA = maxA = 0; + // for each vertex in the first shape, project it onto the line perpendicular to the edge + // and keep track of the min and max of these values + for (j = 0; j < a.length; j++) { + projected = normal.x * a[j as number].x + normal.y * a[j as number].y; + minA = isNullOrUndefined(minA) || projected < minA ? projected : minA; + maxA = isNullOrUndefined(maxA) || projected > maxA ? projected : maxA; + } + + // for each vertex in the second shape, project it onto the line perpendicular to the edge + // and keep track of the min and max of these values + minB = maxB = 0; + for (j = 0; j < b.length; j++) { + projected = normal.x * b[j as number].x + normal.y * b[j as number].y; + minB = isNullOrUndefined(minB) || projected < minB ? projected : minB; + maxB = isNullOrUndefined(maxB) || projected > maxB ? projected : maxB; + } + // if there is no overlap between the projects, the edge we are looking at separates the two + // polygons, and we know there is no overlap + return !(maxA < minB || maxB < minA); + } + } + return true; +} + +/** + * Checks if the provided coordinates are within the bounds of the rectangle. + * + * @param {number} x - The x-coordinate to check. + * @param {number} y - The y-coordinate to check. + * @param {Rect} bounds - The bounding rectangle. + * @param {number} width - The width of the area to include in the bounds check. + * @param {number} height - The height of the area to include in the bounds check. + * @returns {boolean} - Returns true if the coordinates are within the bounds; otherwise, false. + * @private + */ +export function withInBounds(x: number, y: number, bounds: Rect, width: number = 0, height: number = 0): boolean { + return (x >= bounds.x - width && x <= bounds.x + bounds.width + width && y >= bounds.y - height + && y <= bounds.y + bounds.height + height); +} + +/** + * Function type definition for creating rectangle options. + * + * @param {string} id - Unique identifier for the rectangle + * @param {string} fill - Fill color of the rectangle + * @param {Object} border - Border properties + * @param {number} border.width - Width of the border + * @param {string|null} border.color - Color of the border, can be null + * @param {number} opacity - Opacity value between 0 and 1 + * @param {Object} rect - Rectangle dimensions and position + * @param {number} rect.x - X coordinate of the rectangle + * @param {number} rect.y - Y coordinate of the rectangle + * @param {number} rect.width - Width of the rectangle + * @param {number} rect.height - Height of the rectangle + * @param {number} [rx] - Optional horizontal corner radius + * @param {number} [ry] - Optional vertical corner radius + * @param {string} [transform] - Optional transformation string + * @param {string} [dashArray] - Optional dash array pattern for stroke + * @returns {RectOption} A rectangle option object + * @private + */ +type CreateRectOptionFn = ( + id: string, + fill: string, + border: { width: number; color: string | null }, + opacity: number, + rect: { x: number; y: number; width: number; height: number }, + rx?: number, + ry?: number, + transform?: string, + dashArray?: string +) => RectOption; + +/** + * Creates a rectangle option object with the specified properties + * @param {string} id - Unique identifier for the rectangle + * @param {string} fill - Fill color of the rectangle + * @param {Object} border - Border properties + * @param {number} border.width - Width of the border + * @param {string|null} border.color - Color of the border, can be null + * @param {number} opacity - Opacity value between 0 and 1 + * @param {Object} rect - Rectangle dimensions and position + * @param {number} rect.x - X coordinate of the rectangle + * @param {number} rect.y - Y coordinate of the rectangle + * @param {number} rect.width - Width of the rectangle + * @param {number} rect.height - Height of the rectangle + * @param {number} [rx] - Optional horizontal corner radius, defaults to 0 + * @param {number} [ry] - Optional vertical corner radius, defaults to 0 + * @param {string} [transform] - Optional transformation string, defaults to empty string + * @param {string} [dashArray] - Optional dash array pattern for stroke, defaults to empty string + * @returns {RectOption} A rectangle option object with all properties configured + * @private + */ +export const createRectOption: CreateRectOptionFn = ( + id: string, + fill: string, + border: { width: number; color: string | null }, + opacity: number, + rect: { x: number; y: number; width: number; height: number }, + rx?: number, + ry?: number, + transform?: string, + dashArray?: string +) => ({ + id, + fill, + stroke: border.width !== 0 && border.color ? border.color : 'transparent', + strokeWidth: border.width, + strokeDasharray: dashArray ?? '', + opacity, + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + rx: rx as number, + ry: ry as number, + transform: transform as string, + d: '' +}); + +/** + * Sets the color of a data point. + * + * @param {Points} point - The data point. + * @param {string} color - The fallback/default color to use. + * @returns {string} The computed color for the point. + * @private + */ +export function setPointColor( + point: Points, + color: string +): string { + color = point.interior || color; + return color; +} + +/** + * Sets the border color of a data point. + * + * @param {Points} point - The data point for which to set the border color. + * @param {ChartBorderProps} border - The border color to set. + * @returns {ChartBorderProps} - The updated border color. + * @private + */ +export function setBorderColor(point: Points, border: ChartBorderProps): ChartBorderProps { + border.width = point && point.isEmpty ? ((point.series as SeriesProperties)?.emptyPointSettings?.border?.width || border.width) : + border.width; + border.width = Math.max(0, border.width!); + border.color = point && point.isEmpty ? ((point.series as SeriesProperties)?.emptyPointSettings?.border?.color || border.color) : + border.color; + border.color = border.color || 'transparent'; + return border; +} + +/** + * Calculates the shapes based on the specified parameters. + * + * @param {ChartLocation} location - The location for the shape. + * @param {Size} size - The size of the shape. + * @param {string} shape - The type of shape. + * @param {PathOptions} options - Additional options for the path. + * @param {string} url - A URL for the shape if applicable. + * @returns {PathOptions} The calculated shapes. + * @private + */ +export const calculateShapes: ( + location: ChartLocationProps, + size: ChartSizeProps, + shape: string, + options: PathOptions, + url: string +) => PathOptions | Element = ( + location: ChartLocationProps, + size: ChartSizeProps, + shape: string, + options: PathOptions, + url: string +): PathOptions | Element => { + let dir: string; + const width: number = size.width; + const height: number = size.height; + const lx: number = location.x; + const ly: number = location.y; + const y: number = location.y + (-height / 2); + const x: number = location.x + (-width / 2); + const eq: number = 72; + let xVal: number; + let yVal: number; + switch (shape) { + case 'Bubble': + case 'Circle': + merge(options, { 'rx': width / 2, 'ry': height / 2, 'cx': lx, 'cy': ly }); + break; + case 'Plus': + dir = 'M' + ' ' + x + ' ' + ly + ' ' + 'L' + ' ' + (lx + (width / 2)) + ' ' + ly + ' ' + + 'M' + ' ' + lx + ' ' + (ly + (height / 2)) + ' ' + 'L' + ' ' + lx + ' ' + + (ly + (-height / 2)); + merge(options, { 'd': dir }); + break; + case 'Cross': + dir = 'M' + ' ' + x + ' ' + (ly + (-height / 2)) + ' ' + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly + (height / 2)) + ' ' + + 'M' + ' ' + x + ' ' + (ly + (height / 2)) + ' ' + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly + (-height / 2)); + merge(options, { 'd': dir }); + break; + case 'Multiply': + dir = 'M ' + (lx) + ' ' + (ly) + ' L ' + + (lx) + ' ' + (ly) + ' M ' + + (lx) + ' ' + (ly) + ' L ' + (lx) + ' ' + (ly); + merge(options, { 'd': dir, stroke: options.fill }); + break; + case 'HorizontalLine': + dir = 'M' + ' ' + x + ' ' + ly + ' ' + 'L' + ' ' + (lx + (width / 2)) + ' ' + ly; + merge(options, { 'd': dir }); + break; + case 'VerticalLine': + dir = 'M' + ' ' + lx + ' ' + (ly + (height / 2)) + ' ' + 'L' + ' ' + lx + ' ' + (ly + (-height / 2)); + merge(options, { 'd': dir }); + break; + case 'Diamond': + dir = 'M' + ' ' + x + ' ' + ly + ' ' + + 'L' + ' ' + lx + ' ' + (ly + (-height / 2)) + ' ' + + 'L' + ' ' + (lx + (width / 2)) + ' ' + ly + ' ' + + 'L' + ' ' + lx + ' ' + (ly + (height / 2)) + ' ' + + 'L' + ' ' + x + ' ' + ly + ' z'; + merge(options, { 'd': dir }); + break; + case 'Rectangle': + case 'Hilo': + case 'HiloOpenClose': + case 'Candle': + case 'Waterfall': + case 'BoxAndWhisker': + case 'StepArea': + case 'RangeStepArea': + case 'StackingStepArea': + case 'Square': + case 'Flag': + dir = 'M' + ' ' + x + ' ' + (ly + (-height / 2)) + ' ' + + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly + (-height / 2)) + ' ' + + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly + (height / 2)) + ' ' + + 'L' + ' ' + x + ' ' + (ly + (height / 2)) + ' ' + + 'L' + ' ' + x + ' ' + (ly + (-height / 2)) + ' z'; + merge(options, { 'd': dir }); + break; + case 'Pyramid': + case 'Triangle': + dir = 'M' + ' ' + x + ' ' + (ly + (height / 2)) + ' ' + + 'L' + ' ' + lx + ' ' + (ly + (-height / 2)) + ' ' + + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly + (height / 2)) + ' ' + + 'L' + ' ' + x + ' ' + (ly + (height / 2)) + ' z'; + merge(options, { 'd': dir }); + break; + case 'Funnel': + case 'InvertedTriangle': + dir = 'M' + ' ' + (lx + (width / 2)) + ' ' + (ly - (height / 2)) + ' ' + + 'L' + ' ' + lx + ' ' + (ly + (height / 2)) + ' ' + + 'L' + ' ' + (lx - (width / 2)) + ' ' + (ly - (height / 2)) + ' ' + + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly - (height / 2)) + ' z'; + merge(options, { 'd': dir }); + break; + case 'Pentagon': + dir = ''; + for (let i: number = 0; i <= 5; i++) { + xVal = (width / 2) * Math.cos((Math.PI / 180) * (i * eq)); + yVal = (height / 2) * Math.sin((Math.PI / 180) * (i * eq)); + if (i === 0) { + dir = 'M' + ' ' + (lx + xVal) + ' ' + (ly + yVal) + ' '; + } else { + dir = dir.concat('L' + ' ' + (lx + xVal) + ' ' + (ly + yVal) + ' '); + } + } + dir = dir.concat('Z'); + merge(options, { 'd': dir }); + break; + case 'Image': + merge(options, { 'href': url, 'height': height, 'width': width, x: x, y: y }); + break; + case 'Star': { + const cornerPoints: number = 5; + const outerRadius: number = Math.min(width, height) / 2; + const innerRadius: number = outerRadius / 2; + const angle: number = Math.PI / cornerPoints; + let starPath: string = ''; + for (let i: number = 0; i < 2 * cornerPoints; i++) { + const radius: number = (i % 2 === 0) ? outerRadius : innerRadius; + const currentX: number = lx + radius * Math.cos(i * angle - Math.PI / 2); + const currentY: number = ly + radius * Math.sin(i * angle - Math.PI / 2); + starPath += (i === 0 ? 'M' : 'L') + currentX + ',' + currentY; + } + starPath += 'Z'; + merge(options, { 'd': starPath }); + break; + } + } + options = calculateLegendShapes(location, { width: width, height: height }, shape, options); + return options; +}; + +/** + * Calculates the legend shapes based on the provided parameters. + * + * @param {ChartLocationProps} location - The location for the shape. + * @param {Size} size - The size of the shape. + * @param {string} shape - The type of shape. + * @param {PathOptions} options - Additional options for the path. + * @returns {PathOptions} - The calculated legend shape. + * @private + */ +export const calculateLegendShapes: ( + location: ChartLocationProps, size: ChartSizeProps, shape: string, options: PathOptions) => PathOptions = + (location: ChartLocationProps, size: ChartSizeProps, shape: string, options: PathOptions): PathOptions => { + const padding: number = 10; + let dir: string = ''; + const space: number = 2; + const height: number = size.height; + const width: number = size.width; + const lx: number = location.x; + const ly: number = location.y; + switch (shape) { + case 'Line': + case 'StackingLine': + case 'StackingLine100': + dir = 'M' + ' ' + (lx + (-width * (3 / 4))) + ' ' + (ly) + ' ' + + 'L' + ' ' + (lx + (width * (3 / 4))) + ' ' + (ly); + merge(options, { 'd': dir }); + break; + case 'StepLine': + options.fill = 'transparent'; + dir = 'M' + ' ' + (lx + (-width / 2) - (padding / 4)) + ' ' + (ly + (height / 2)) + ' ' + 'L' + ' ' + (lx + + (-width / 2) + (width / 10)) + ' ' + (ly + (height / 2)) + ' ' + 'L' + ' ' + (lx + (-width / 2) + (width / 10)) + + ' ' + (ly) + ' ' + 'L' + ' ' + (lx + (-width / 10)) + ' ' + (ly) + ' ' + 'L' + ' ' + (lx + (-width / 10)) + + ' ' + (ly + (height / 2)) + ' ' + 'L' + ' ' + (lx + (width / 5)) + ' ' + (ly + (height / 2)) + ' ' + 'L' + + ' ' + (lx + (width / 5)) + ' ' + (ly + (-height / 2)) + ' ' + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly + + (-height / 2)) + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly + (height / 2)) + ' ' + 'L' + '' + (lx + (width / 2) + + (padding / 4)) + ' ' + (ly + (height / 2)); + merge(options, { 'd': dir }); + break; + case 'UpArrow': + options.fill = options.stroke; + options.stroke = 'transparent'; + dir = 'M' + ' ' + (lx + (-width / 2)) + ' ' + (ly + (height / 2)) + ' ' + + 'L' + ' ' + (lx) + ' ' + (ly - (height / 2)) + ' ' + + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly + (height / 2)) + + 'L' + ' ' + (lx + (width / 2) - space) + ' ' + (ly + (height / 2)) + ' ' + + 'L' + ' ' + (lx) + ' ' + (ly - (height / 2) + (2 * space)) + + 'L' + (lx - (width / 2) + space) + ' ' + (ly + (height / 2)) + ' Z'; + merge(options, { 'd': dir }); + break; + case 'DownArrow': + options.fill = options.stroke; + options.stroke = 'transparent'; + dir = 'M' + ' ' + (lx - (width / 2)) + ' ' + (ly - (height / 2)) + ' ' + + 'L' + ' ' + (lx) + ' ' + (ly + (height / 2)) + ' ' + + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly - (height / 2)) + + 'L' + ' ' + (lx + (width / 2) - space) + ' ' + (ly - (height / 2)) + ' ' + + 'L' + ' ' + (lx) + ' ' + (ly + (height / 2) - (2 * space)) + + 'L' + (lx - (width / 2) + space) + ' ' + (ly - (height / 2)) + ' Z'; + merge(options, { 'd': dir }); + break; + case 'RightArrow': + options.fill = options.stroke; + options.stroke = 'transparent'; + dir = 'M' + ' ' + (lx + (-width / 2)) + ' ' + (ly - (height / 2)) + ' ' + + 'L' + ' ' + (lx + (width / 2)) + ' ' + (ly) + ' ' + 'L' + ' ' + + (lx + (-width / 2)) + ' ' + (ly + (height / 2)) + ' L' + ' ' + (lx + (-width / 2)) + ' ' + + (ly + (height / 2) - space) + ' ' + 'L' + ' ' + (lx + (width / 2) - (2 * space)) + ' ' + (ly) + + ' L' + (lx + (-width / 2)) + ' ' + (ly - (height / 2) + space) + ' Z'; + merge(options, { 'd': dir }); + break; + case 'LeftArrow': + options.fill = options.stroke; + options.stroke = 'transparent'; + dir = 'M' + ' ' + (lx + (width / 2)) + ' ' + (ly - (height / 2)) + ' ' + + 'L' + ' ' + (lx + (-width / 2)) + ' ' + (ly) + ' ' + 'L' + ' ' + + (lx + (width / 2)) + ' ' + (ly + (height / 2)) + ' ' + 'L' + ' ' + + (lx + (width / 2)) + ' ' + (ly + (height / 2) - space) + ' L' + ' ' + (lx + (-width / 2) + (2 * space)) + + ' ' + (ly) + ' L' + (lx + (width / 2)) + ' ' + (ly - (height / 2) + space) + ' Z'; + merge(options, { 'd': dir }); + break; + case 'Column': + case 'StackingColumn': + case 'StackingColumn100': + dir = 'M' + ' ' + (lx - 3 * (width / 5)) + ' ' + (ly - (height / 5)) + ' ' + 'L' + ' ' + + (lx + 3 * (-width / 10)) + ' ' + (ly - (height / 5)) + ' ' + 'L' + ' ' + + (lx + 3 * (-width / 10)) + ' ' + (ly + (height / 2)) + ' ' + 'L' + ' ' + (lx - 3 * + (width / 5)) + ' ' + (ly + (height / 2)) + ' ' + 'Z' + ' ' + 'M' + ' ' + + (lx + (-width / 10) - (width / 20)) + ' ' + (ly - (height / 4) - (padding / 2)) + + ' ' + 'L' + ' ' + (lx + (width / 10) + (width / 20)) + ' ' + (ly - (height / 4) - + (padding / 2)) + ' ' + 'L' + ' ' + (lx + (width / 10) + (width / 20)) + ' ' + (ly + + (height / 2)) + ' ' + 'L' + ' ' + (lx + (-width / 10) - (width / 20)) + ' ' + (ly + + (height / 2)) + ' ' + 'Z' + ' ' + 'M' + ' ' + (lx + 3 * (width / 10)) + ' ' + (ly) + ' ' + + 'L' + ' ' + (lx + 3 * (width / 5)) + ' ' + (ly) + ' ' + 'L' + ' ' + + (lx + 3 * (width / 5)) + ' ' + (ly + (height / 2)) + ' ' + 'L' + ' ' + + (lx + 3 * (width / 10)) + ' ' + (ly + (height / 2)) + ' ' + 'Z'; + merge(options, { 'd': dir }); + break; + case 'Bar': + case 'StackingBar': + case 'StackingBar100': + dir = 'M' + ' ' + (lx + (-width / 2) + (-padding / 4)) + ' ' + (ly - 3 * (height / 5)) + ' ' + + 'L' + ' ' + (lx + 3 * (width / 10)) + ' ' + (ly - 3 * (height / 5)) + ' ' + 'L' + ' ' + + (lx + 3 * (width / 10)) + ' ' + (ly - 3 * (height / 10)) + ' ' + 'L' + ' ' + + (lx - (width / 2) + (-padding / 4)) + ' ' + (ly - 3 * (height / 10)) + ' ' + 'Z' + ' ' + + 'M' + ' ' + (lx + (-width / 2) + (-padding / 4)) + ' ' + (ly - (height / 5) + + (padding / 20)) + ' ' + 'L' + ' ' + (lx + (width / 2) + (padding / 4)) + ' ' + (ly + - (height / 5) + (padding / 20)) + ' ' + 'L' + ' ' + (lx + (width / 2) + (padding / 4)) + + ' ' + (ly + (height / 10) + (padding / 20)) + ' ' + 'L' + ' ' + (lx - (width / 2) + + (-padding / 4)) + ' ' + (ly + (height / 10) + (padding / 20)) + ' ' + 'Z' + ' ' + 'M' + + ' ' + (lx - (width / 2) + (-padding / 4)) + ' ' + (ly + (height / 5) + + (padding / 10)) + ' ' + 'L' + ' ' + (lx + (-width / 4)) + ' ' + (ly + (height / 5) + + (padding / 10)) + ' ' + 'L' + ' ' + (lx + (-width / 4)) + ' ' + (ly + (height / 2) + + (padding / 10)) + ' ' + 'L' + ' ' + (lx - (width / 2) + (-padding / 4)) + + ' ' + (ly + (height / 2) + (padding / 10)) + ' ' + 'Z'; + merge(options, { 'd': dir }); + break; + case 'Spline': + options.fill = 'transparent'; + dir = 'M' + ' ' + (lx - (width / 2)) + ' ' + (ly + (height / 5)) + ' ' + 'Q' + ' ' + + lx + ' ' + (ly - height) + ' ' + lx + ' ' + (ly + (height / 5)) + + ' ' + 'M' + ' ' + lx + ' ' + (ly + (height / 5)) + ' ' + 'Q' + ' ' + (lx + + (width / 2)) + ' ' + (ly + (height / 2)) + ' ' + (lx + (width / 2)) + ' ' + + (ly - (height / 2)); + merge(options, { 'd': dir }); + break; + case 'Area': + dir = 'M' + ' ' + (lx - (width / 2) - (padding / 4)) + ' ' + (ly + (height / 2)) + + ' ' + 'L' + ' ' + (lx + (-width / 4) + (-padding / 8)) + ' ' + (ly - (height / 2)) + + ' ' + 'L' + ' ' + (lx) + ' ' + (ly + (height / 4)) + ' ' + 'L' + ' ' + (lx + + (width / 4) + (padding / 8)) + ' ' + (ly + (-height / 2) + (height / 4)) + ' ' + + 'L' + ' ' + (lx + (height / 2) + (padding / 4)) + ' ' + (ly + (height / 2)) + ' ' + 'Z'; + merge(options, { 'd': dir }); + break; + case 'SplineArea': + dir = 'M' + ' ' + (lx - (width / 2)) + ' ' + (ly + (height / 5)) + ' ' + 'Q' + ' ' + lx + + ' ' + (ly - height) + ' ' + lx + ' ' + (ly + (height / 5)) + ' ' + 'Z' + ' ' + 'M' + + ' ' + lx + ' ' + (ly + (height / 5)) + ' ' + 'Q' + ' ' + (lx + (width / 2)) + ' ' + + (ly + (height / 2)) + ' ' + (lx + (width / 2)) + ' ' + + (ly - (height / 2)) + ' ' + ' Z'; + merge(options, { 'd': dir }); + break; + } + return options; + }; + + +/** + * Draws a symbol at the specified location. + * + * @param {ChartLocationProps} location - The location to draw the symbol. + * @param {string} shape - The shape of the symbol. + * @param {Size} size - The size of the symbol. + * @param {string} url - The URL of the image symbol. + * @param {PathOptions} options - The options for drawing the symbol. + * @returns {Element} - The element representing the drawn symbol. + * @private + */ +export function drawSymbol( + location: ChartLocationProps, shape: string, size: ChartSizeProps, url: string, options: PathOptions +): Element { + const shapeOption: PathOptions | Element = calculateShapes(location, size, shape, options, url); + return shapeOption as Element; +} +/** + * Compares two data sources for equality. + * + * @param {any} a - The first data source to compare. + * @param {any} b - The second data source to compare. + * @returns {boolean} True if the data sources are equal, false otherwise. + * @private + */ +export function areDataSourcesEqual(a: any, b: any): any { + if (!a || !b || a.length !== b.length) { return false; } + return a.every((point: Points, i: number) => point.x === b[i as number].x && point.y === b[i as number].y); +} + +/** + * Trims the text and performs line breaks based on the maximum width and font settings. + * + * @param {number} maxWidth - The maximum width allowed for the text. + * @param {string} text - The text to be trimmed. + * @param {AxisTextStyle} font - The font settings for the text. + * @param {TextStyleModel} themeFontStyle - Optional. The font style based on the theme. + * @returns {string[]} - An array of trimmed text lines with line breaks. + * @private + */ +export function lineBreakLabelTrim(maxWidth: number, text: string, font: AxisTextStyle, themeFontStyle: TextStyleModel): string[] { + const labelCollection: string[] = []; + const breakLabels: string[] = text.split('
'); + for (let i: number = 0; i < breakLabels.length; i++) { + text = breakLabels[i as number]; + let size: number = measureText(text, font as TextStyleModel, themeFontStyle).width; + void((size > maxWidth) ? ( + (() => { + const textLength: number = text.length; + for (let i: number = textLength - 1; i >= 0; --i) { + text = text.substring(0, i) + '...'; + size = measureText(text, font as TextStyleModel, themeFontStyle).width; + if (size <= maxWidth) { + labelCollection.push(text); + break; + } + } + })() + ) : labelCollection.push(text)); + + } + return labelCollection; +} + +/** + * Gets the rectangle location constrained by the outer boundary + * + * @param {ChartLocationProps} startLocation - The starting location of the rectangle + * @param {ChartLocationProps} endLocation - The ending location of the rectangle + * @param {Rect} outerRect - The outer rectangle boundary + * @returns {Rect} The constrained rectangle + * @private + */ +export function getRectLocation(startLocation: ChartLocationProps, endLocation: ChartLocationProps, outerRect: Rect): Rect { + const x: number = (endLocation.x < outerRect.x) ? outerRect.x : + (endLocation.x > (outerRect.x + outerRect.width)) ? outerRect.x + outerRect.width : endLocation.x; + const y: number = (endLocation.y < outerRect.y) ? outerRect.y : + (endLocation.y > (outerRect.y + outerRect.height)) ? outerRect.y + outerRect.height : endLocation.y; + + return ({ + x: (x > startLocation.x ? startLocation.x : x), + y: (y > startLocation.y ? startLocation.y : y), + width: Math.abs(x - startLocation.x), + height: Math.abs(y - startLocation.y) + } + ); +} + +/** + * Returns the value constrained within the specified minimum and maximum limits. + * + * @param {number} value - The input value. + * @param {number} min - The minimum limit. + * @param {number} max - The maximum limit. + * @returns {number} - The constrained value. + * @private + */ +export function minMax(value: number, min: number, max: number): number { + return value > max ? max : (value < min ? min : value); +} + +/** + * Checks if zooming is enabled for the axis. + * + * @param {AxisModel} axis - The axis to check for zooming. + * @returns {boolean} - Returns true if zooming is enabled for the axis, otherwise false. + * @private + */ +export function isZoomSet(axis: AxisModel): boolean { + return ((axis.zoomFactor as number) < 1 && (axis.zoomPosition as number) >= 0); +} +/** + * Calculates the minimum points delta between data points on the provided axis. + * + * @param {AxisModel} axis - The axis for which to calculate the minimum points delta. + * @param {Series[]} seriesCollection - The collection of series in the chart. + * @returns {number} The minimum points delta. + * @private + */ +export function getMinPointsDelta(axis: AxisModel, seriesCollection: SeriesProperties[]): number { + let minDelta: number = Number.MAX_VALUE; + let xValues: (number | null)[]; + let minVal: number; + let seriesMin: number; + const stackingGroups: string[] = []; + + for (let index: number = 0; index < seriesCollection.length; index++) { + const series: SeriesProperties = seriesCollection[index as number]; + xValues = []; + if (series.visible && + (axis.name === series.xAxisName || (axis.name === 'primaryXAxis' && series.xAxisName === null) + || (axis.name === series.chart.axisCollection[0].name && !series.xAxisName))) { + xValues = series.points.map((point: Points) => { + return point.xValue; + }); + xValues.sort((first: number | null, second: number | null) => { + if (first === null && second === null) { return 0; } + if (first === null) { return -1; } + if (second === null) { return 1; } + return first - second; + }); + if (xValues.length === 1) { + if (axis.valueType === 'Category') { + const minValue: number = series.xAxis.visibleRange.minimum; + const delta: number = (xValues[0] as number) - minValue; + minDelta = delta !== 0 ? Math.min(minDelta, delta) : minDelta; + } else if (axis.valueType === 'DateTime') { + const timeOffset: number = seriesCollection.length === 1 ? 25920000 : 2592000000; + seriesMin = (series.xMin === series.xMax) ? (series.xMin - timeOffset) : series.xMin; + minVal = (xValues[0] as number) - (!isNullOrUndefined(seriesMin) ? seriesMin : axis.visibleRange.minimum); + minDelta = minVal !== 0 ? Math.min(minDelta, minVal) : minDelta; + } else { + seriesMin = series.xMin; + minVal = (xValues[0] as number) - (!isNullOrUndefined(seriesMin) ? + seriesMin : axis.visibleRange.minimum); + if (minVal !== 0) { + minDelta = Math.min(minDelta, minVal); + } + } + } else { + for (let i: number = 0; i < xValues.length; i++) { + const value: number | null = xValues[i as number]; + if (i > 0 && value) { + minVal = series.type && series.type.indexOf('Stacking') > -1 && axis.valueType === 'Category' ? + stackingGroups.length : (value as number) - (xValues[(i as number) - 1] as number); + if (minVal !== 0) { + minDelta = Math.min(minDelta, minVal); + } + } + } + } + } + } + if (minDelta === Number.MAX_VALUE) { + minDelta = 1; + } + return minDelta; +} + +/** + * Checks if the given value is within the range of the provided axis. + * + * @param {number} value - The value to check against the axis range. + * @param {AxisModel} axis - The axis model containing the range to check against. + * @returns {number} The original value if it's within range, or the closest boundary value if outside the range. + * @private + */ +export function logWithIn(value: number, axis: AxisModel): number { + return axis.valueType === 'Logarithmic' ? logBase(value, axis.logBase as number) : value; +} + +/** + * Checks if a point is within the range of the previous and next points in a series. + * + * @param {Points} previousPoint - The previous point in the series. + * @param {Points} currentPoint - The current point to check. + * @param {Points} nextPoint - The next point in the series. + * @param {Series} series - The series to which the points belong. + * @returns {boolean} - A boolean indicating if the point is within the range. + * @private + */ +export function withInRange(previousPoint: Points, currentPoint: Points, nextPoint: Points, series: SeriesProperties): boolean { + if (series.chart.delayRedraw && series.chart.enableAnimation) { + return true; + } + const mX2: number = logWithIn(currentPoint.xValue || 0, series.xAxis); + const mX1: number = previousPoint ? logWithIn(previousPoint.xValue || 0, series.xAxis) : mX2; + const mX3: number = nextPoint ? logWithIn(nextPoint.xValue || 0, series.xAxis) : mX2; + const xStart: number = Math.floor(series.xAxis.visibleRange.minimum as number); + const xEnd: number = Math.ceil(series.xAxis.visibleRange.maximum as number); + return ((mX1 >= xStart && mX1 <= xEnd) || (mX2 >= xStart && mX2 <= xEnd) || + (mX3 >= xStart && mX3 <= xEnd) || (xStart >= mX1 && xStart <= mX3)); +} + +/** + * Creates a rectangle with the specified position and size. + * + * @param {number} x - The x-coordinate of the rectangle's top-left corner. + * @param {number} y - The y-coordinate of the rectangle's top-left corner. + * @param {number} width - The width of the rectangle. + * @param {number} height - The height of the rectangle. + * @returns {Rect} The rectangle object containing x, y, width, and height. + * @private + */ +const createRect: (x: number, y: number, width: number, height: number) => +Rect = (x: number, y: number, width: number, height: number): Rect => ({ + x, y, width, height +}); + +/** + * Calculates the rectangle to be used for rendering a label or element, + * considering location, text size, and margin. + * + * @param {ChartLocationProps} location - The location object containing x and y coordinates. + * @param {Size} textSize - The size of the text as a Size object (width and height). + * @param {MarginModel} margin - The margin values as a MarginModel object (left, right, top, bottom). + * @returns {Rect} The computed rectangle based on given parameters. + * @private + */ +export function calculateRect(location: ChartLocationProps, textSize: ChartSizeProps, margin: MarginModel): Rect { + return createRect( + location.x - (textSize.width / 2) - margin.left, + location.y - (textSize.height / 2) - margin.top, + textSize.width + margin.left + margin.right, + textSize.height + margin.top + margin.bottom + ); +} + +/** + * Gets the array of formatted data label text(s) for the given data point and series. + * + * @param {Points} currentPoint - The current data point. + * @param {SeriesProperties} series - The properties of the series to which the point belongs. + * @param {Chart} chart - The chart object for accessing locale and formatting options. + * @returns {string[]} An array of text strings for the data label(s) of the point. + * @private + */ +export function getDataLabelText(currentPoint: Points, series: SeriesProperties, chart: Chart): string[] { + const labelFormat: string = (series.marker?.dataLabel?.format + ? series.marker.dataLabel.format : series.yAxis.labelStyle.format) as string; + const text: string[] = []; + const customLabelFormat: boolean = labelFormat.match('{value}') !== null; + switch (series.seriesType) { + case 'XY': + text.push(currentPoint.text || (currentPoint.yValue as number).toString()); + if ((labelFormat) && !currentPoint.text) { + const option: NumberFormatOptions = { + locale: chart.locale, + useGrouping: false, + format: customLabelFormat ? '' : labelFormat + }; + series.yAxis.format = getNumberFormat(option); + for (let i: number = 0; i < text.length; i++) { + text[i as number] = customLabelFormat ? labelFormat.replace('{value}', series.yAxis.format(parseFloat(text[i as number]))) : + series.yAxis.format(parseFloat(text[i as number])); + } + } + return text; + default: + // Add default case to ensure all code paths return a value + return text; + } +} + +/** + * Retrieves the points of a rectangle. + * + * @param {Rect} rect - The rectangle whose points are to be retrieved. + * @returns {ChartLocationProps[]} - The points of the rectangle. + * @private + */ +export function getRectanglePoints(rect: Rect): ChartLocationProps[] { + const loc1: ChartLocationProps = {x: rect.x, y: rect.y}; + const loc2: ChartLocationProps = {x: rect.x + rect.width, y: rect.y}; + const loc3: ChartLocationProps = {x: rect.x + rect.width, y: rect.y + rect.height}; + const loc4: ChartLocationProps = {x: rect.x, y: rect.y + rect.height}; + return [loc1, loc2, loc3, loc4]; +} + +/** + * Gets the visible points from the given series by filtering out points with null or undefined x value. + * Assigns a continuous index to each visible point. + * + * @param {SeriesProperties} series - The series object containing the points array. + * @returns {Points[]} An array of visible Points with updated index. + * @private + */ +export function getVisiblePoints(series: SeriesProperties): Points[] { + const points: Points[] = series.points; + const tempPoints: Points[] = []; + let tempPoint: Points; + let pointIndex: number = 0; + for (let i: number = 0; i < points.length; i++) { + tempPoint = points[i as number]; + void(isNullOrUndefined(tempPoint.x) || (tempPoint.index = pointIndex++, tempPoints.push(tempPoint))); + continue; + } + return tempPoints; +} + +/** + * Checks whether a given rectangle collides with any rectangle in the provided collection, + * considering an offset by the given clipping rectangle. + * + * @param {Rect} rect - The rectangle to check for collision. + * @param {Rect[]} collections - An array of rectangles to check against. + * @param {Rect} clipRect - The clipping rectangle whose x and y are used as offsets. + * @returns {boolean} True if a collision is detected; otherwise, false. + * @private + */ +export function isCollide(rect: Rect, collections: Rect[], clipRect: Rect): boolean { + const currentRect: Rect = {x: rect.x + clipRect.x, y: rect.y + clipRect.y, width: rect.width, height: rect.height}; + const isCollide: boolean = collections.some((rect: Rect) => { + return (currentRect.x < rect.x + rect.width && currentRect.x + currentRect.width > rect.x && + currentRect.y < rect.y + rect.height && currentRect.height + currentRect.y > rect.y); + }); + return isCollide; +} + +/** + * Checks whether any of the given label rectangle coordinates overlap with the chart area boundary. + * The input label locations are offset using the clip rectangle before boundary check. + * + * @param {ChartLocationProps[]} rectCoordinates - Array of chart locations (label center points). + * @param {Chart} chart - The chart object (to access chart area rectangle). + * @param {Rect} clip - The clip rectangle, its x and y used as offset to the label positions. + * @returns {boolean} True if any label rectangle is out of chart bounds; otherwise, false. + * @private + */ +export function isDataLabelOverlapWithChartBound(rectCoordinates: ChartLocationProps[], chart: Chart, clip: Rect): boolean { + for (let index: number = 0; index < rectCoordinates.length; index++) { + if (!withInBounds(rectCoordinates[index as number].x + clip.x, + rectCoordinates[index as number].y + clip.y, chart.chartAreaRect)) { + return true; + } + } + return false; +} + +/** + * Rotates the size of text based on the provided angle. + * + * @param {Font} font - The font style of the text. + * @param {string} text - The text to be rotated. + * @param {number} angle - The angle of rotation. + * @param {Chart | Chart3D} chart - The chart instance. + * @param {Font} themeFontStyle - The font style based on the theme. + * @returns {Size} - The rotated size of the text. + * @private + */ +export function rotateTextSize( + font: ChartFontProps, text: string, angle: number, chart: Chart, themeFontStyle: ChartFontProps): ChartSizeProps { + const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const textEl: SVGTextElement = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + + const textCollection: string[] = []; + let labelText: string = ''; + + labelText = isBreakLabel(text) + ? text.split('
')[0] + : text as string; + + textEl.textContent = labelText; + + const fontStyle: string = font.fontStyle as string; + const fontWeight: string = font.fontWeight as string; + const fontSize: string = font.fontSize as string; + const fontFamily: string = font.fontFamily as string; + textEl.setAttribute('id', 'rotate_text'); + textEl.style.fontStyle = fontStyle; + textEl.style.fontWeight = fontWeight; + textEl.style.fontSize = fontSize; + textEl.style.fontFamily = fontFamily; + textEl.setAttribute('text-anchor', 'middle'); + textEl.setAttribute('transform', `rotate(${angle}, 0, 0)`); + textEl.setAttribute('x', `${chart.chartAreaRect.x}`); + textEl.setAttribute('y', `${chart.chartAreaRect.y}`); + + // Handle multi-line text with tspan elements + if (textCollection.length > 1) { + textEl.textContent = ''; + + for (let i: number = 0; i < textCollection.length; i++) { + const tspanEl: SVGTSpanElement = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); + tspanEl.textContent = textCollection[i as number]; + tspanEl.setAttribute('x', '0'); + + if (i === 0) { + tspanEl.setAttribute('dy', '0'); + } else { + + const height: number = measureText(textCollection[i as number], + font as TextStyleModel, themeFontStyle as TextStyleModel).height; + tspanEl.setAttribute('dy', height.toString()); + } + + textEl.appendChild(tspanEl); + } + } + + svg.appendChild(textEl); + svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + svg.setAttribute('width', `${chart.chartAreaRect.width}`); + svg.setAttribute('height', `${chart.chartAreaRect.height}`); + + + document.body.appendChild(svg); + const bbox: DOMRect = textEl.getBoundingClientRect(); + document.body.removeChild(svg); + + return { width: bbox.width, height: bbox.height }; +} + +/** + * Represents the result of a text element creation operation. + * This interface contains all necessary properties to render text in SVG. + * + * @private + */ +interface TextElementResult { + /** + * Contains rendering options for the text element. + */ + renderOptions: { + /** Optional identifier for the text element */ + id?: string | number; + /** X-coordinate position for the text */ + x: number; + /** Y-coordinate position for the text */ + y: number; + /** Fill color for the text */ + fill: string; + /** Font size for the text */ + 'font-size': string | number; + /** Font style (normal, italic, etc.) */ + 'font-style': string; + /** Font family name */ + 'font-family': string; + /** Font weight (normal, bold, numeric values) */ + 'font-weight': string | number; + /** Optional text anchor position (start, middle, end) */ + 'text-anchor'?: string; + /** Optional rotation angle for the label */ + labelRotation?: number; + /** Optional SVG transform attribute value */ + transform?: string; + /** Optional opacity value */ + opacity?: number; + /** Optional dominant-baseline attribute value */ + 'dominant-baseline'?: string; + }; + /** The text content to be displayed */ + text: string; + /** X-coordinate translation offset */ + transX: number; + /** Y-coordinate translation offset */ + transY: number; +} + +/** + * Creates and returns the rendering options and actual text content for an SVG/text label in the chart. + * + * @param {TextOption} option - The text element configuration, including position, id, text, anchor, etc. + * @param {Font} font - The font style options to apply (size, family, style, weight, opacity). + * @param {string} color - The color to use for the text fill. + * @param {boolean} isMinus - If true and text is an array, selects the last element; otherwise, uses the first. Used for special minus handling. + * @param {Rect} [seriesClipRect] - Optional clipping rectangle, used for calculating translated X/Y coordinates for the text. + * @param {boolean} [isDataLabelWrap] - Optional flag, if true applies additional centering logic (for wrapped data labels). + * @returns {TextElementResult} An object containing the SVG render options, the actual text content, and translation values. + * @private + */ +export function textElement( + option: TextOption, font: ChartFontProps, color: string, + isMinus: boolean = false, + seriesClipRect?: Rect, + isDataLabelWrap?: boolean +): TextElementResult | TextOption { + let renderOptions: TextElementResult['renderOptions'] = { + x: 0, + y: 0, + fill: '', + 'font-size': '', + 'font-style': '', + 'font-family': '', + 'font-weight': '' + }; + const width: number = 0; + + const maxWidth: number = 0; + + const dx: number = (option.text!.length > 1 && isDataLabelWrap) ? (option.x! + maxWidth / 2 - width / 2) : option.x as number; + renderOptions = { + 'id': option.id, + 'x': dx, + 'y': option.y, + 'fill': color ? color : 'black', + 'font-size': font.fontSize as string, + 'font-style': font.fontStyle as string, + 'font-family': font.fontFamily as string, + 'font-weight': font.fontWeight as string, + 'text-anchor': option.anchor, + 'labelRotation': option.labelRotation, + 'transform': option.transform, + 'opacity': font.opacity, + 'dominant-baseline': option.baseLine + }; + const text: string = typeof option.text === 'string' ? option.text : isMinus ? option.text![option.text!.length - 1] : option.text![0]; + const transX: number = seriesClipRect?.x as number; + const transY: number = seriesClipRect?.y as number; + return {renderOptions, text, transX, transY}; +} +/** + * Interface representing stack values for chart rendering. + * + * @private + */ +export interface StackValuesType { + /** + * Start values of the stack + */ + startValues?: number[]; + + /** + * End values of the stack + */ + endValues?: number[]; +} + +/** + * Finds the collection of series based on the column, row, and stack status. + * + * @param {Column} column - The column object containing axes details. + * @param {Row} row - The row object containing axes details. + * @param {boolean} isStack - Specifies whether the series are stacked. + * @returns {Series[]} - Returns a collection of series. + * @private + */ +export function findSeriesCollection(column: ColumnProps, row: RowProps, isStack: boolean): SeriesProperties[] { + const seriesCollection: SeriesProperties[] = []; + + for (const rowAxis of row.axes) { + for (const rowSeries of rowAxis.series) { + for (const axis of column.axes) { + for (const series of axis.series) { + if (series === rowSeries && series.visible && isRectangularSeriesType(series, isStack)) { + seriesCollection.push(series); + } + } + } + } + } + + return seriesCollection; +} + +/** + * Checks if the series in the chart are rectangular. + * + * @param {Series} series - The series to be checked. + * @param {boolean} isStack - Specifies whether the series are stacked. + * @returns {boolean} - Returns true if the series in the chart are rectangular, otherwise false. + * @private + */ +export function isRectangularSeriesType(series: SeriesProperties, isStack: boolean): boolean { + const type: string = (series.type ?? '').toLowerCase(); + return ( + type.includes('column') || type.includes('bar') || isStack + ); +} + +/** + * Gets the label position based on the position index + * + * @param {number} positionIndex - Index of the position in the positions array + * @returns {LabelPosition} The corresponding label position or 'Top' as default + * @private + */ +export function getPosition(positionIndex: number): LabelPosition { + const positions: LabelPosition[] = ['Outer', 'Top', 'Bottom', 'Middle', 'Auto']; + return positions[positionIndex as number] || 'Top'; +} + +/** + * Checks if any visible series exists before the specified index. + * Used to determine if a series should be keyboard focusable with tabindex. + * + * @param {ChartSeriesProps[]} visibleSeries - Array of series to check + * @param {number} index - The index position to check against + * @returns {boolean} Returns true if any visible series exists before the specified index + * @private + */ +export function checkTabindex(visibleSeries: ChartSeriesProps[], index: number): boolean { + for (let i: number = 0; i < index; i++) { + if (visibleSeries[i as number].visible) { + return true; + } + } + return false; +} diff --git a/components/charts/src/chart/utils/theme.tsx b/components/charts/src/chart/utils/theme.tsx new file mode 100644 index 0000000..af4b08f --- /dev/null +++ b/components/charts/src/chart/utils/theme.tsx @@ -0,0 +1,219 @@ +import { Browser } from '@syncfusion/react-base'; +import { Theme } from '../base/enum'; +import { Rect, TextStyleModel } from '../chart-area/chart-interfaces'; + +export interface IThemeStyle { + axisLabel: string; + axisTitle: string; + axisLine: string; + majorGridLine: string; + minorGridLine: string; + majorTickLine: string; + minorTickLine: string; + legendLabel: string; + background: string; + areaBorder: string; + errorBar: string; + crosshairLine: string; + crosshairBackground: string; + crosshairFill: string; + crosshairLabel: string; + tooltipFill: string; + tooltipBoldLabel: string; + tooltipLightLabel: string; + tooltipHeaderLine: string; + markerShadow: string | null; + selectionRectFill: string; + selectionRectStroke: string; + selectionCircleStroke: string; + tabColor: string; + bearFillColor: string; + bullFillColor: string; + toolkitSelectionColor: string; + toolkitFill: string; + toolkitIconRectOverFill: string; + toolkitIconRectSelectionFill: string; + toolkitIconRect: Rect; // Use appropriate type for Rect + chartTitleFont: TextStyleModel; + axisLabelFont: TextStyleModel + legendTitleFont: TextStyleModel; + legendLabelFont: TextStyleModel; + tooltipLabelFont: TextStyleModel; + axisTitleFont: TextStyleModel; + datalabelFont: TextStyleModel; + chartSubTitleFont: TextStyleModel; + crosshairLabelFont: TextStyleModel; + stripLineLabelFont: TextStyleModel; +} + +/** + * Returns the theme style based on the provided theme. + * + * @param {Theme} theme - The theme for which the color style needs to be retrieved. + * @returns {IThemeStyle} The style associated with the specified theme. + * @private + */ +export function getThemeColor(theme: Theme): IThemeStyle { + let style: IThemeStyle; + switch (theme) { + case 'Material3': + style = { + axisLabel: '#49454E', + axisTitle: '#1E192B', + axisLine: '#E7E3F0', + majorGridLine: '#F3F1F8', + minorGridLine: '#F3F1F8', + majorTickLine: '#F3F1F8', + minorTickLine: ' #F3F1F8', + legendLabel: '#49454E', + background: 'transparent', + areaBorder: '#E7E0EC', + errorBar: '#79747E', + crosshairLine: '#49454E', + crosshairBackground: 'rgba(73, 69, 78, 0.1)', + crosshairFill: '#313033', + crosshairLabel: '#F4EFF4', + tooltipFill: '#313033', + tooltipBoldLabel: '#F4EFF4', + tooltipLightLabel: '#F4EFF4', + tooltipHeaderLine: '#F4EFF4', + markerShadow: null, + selectionRectFill: 'rgb(98, 0, 238, 0.06)', + selectionRectStroke: '#6200EE', + selectionCircleStroke: '#79747E', + tabColor: '#49454E', + bearFillColor: '#5887FF', + bullFillColor: '#F7523F', + toolkitSelectionColor: '#49454E', + toolkitFill: '#49454E', + toolkitIconRectOverFill: '#EADDFF', + toolkitIconRectSelectionFill: '#EADDFF', + toolkitIconRect: { x: -4, y: -5, height: 26, width: 26 }, + chartTitleFont: { + color: '#1C1B1F', fontFamily: 'Roboto', fontSize: '16px', fontStyle: 'Regular', fontWeight: '400' + }, + axisLabelFont: { + color: '#1C1B1F', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Regular', fontWeight: '400' + }, + legendTitleFont: { + color: '#1C1B1F', fontFamily: 'Roboto', fontSize: '14px', fontStyle: 'Normal', fontWeight: '600' + }, + legendLabelFont: { + color: '#1C1B1F', fontFamily: 'Roboto', fontSize: Browser.isDevice ? '12px' : '14px', fontStyle: 'Regular', fontWeight: '400' + }, + tooltipLabelFont: { + color: '#F4EFF4', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Normal', fontWeight: '' + }, + axisTitleFont: { + color: '#1C1B1F', fontFamily: 'Roboto', fontSize: '14px', fontStyle: 'Regular', fontWeight: '400' + }, + datalabelFont: { + color: '#1E192B', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Normal', fontWeight: '400' + }, + chartSubTitleFont: { + color: '#49454E', fontFamily: 'Roboto', fontSize: '14px', fontStyle: 'Regular', fontWeight: '400' + }, + crosshairLabelFont: { + color: '#F4EFF4', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Normal', fontWeight: '400' + }, + stripLineLabelFont: { + color: '#79747E', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Normal', fontWeight: '400' + } + }; + break; + case 'Material3Dark': + style = { + axisLabel: '#CAC4D0', + axisTitle: '#E8DEF8', + axisLine: '#322E3A', + majorGridLine: '#2A2831', + minorGridLine: '#2A2831', + majorTickLine: '#2A2831', + minorTickLine: ' #2A2831', + legendLabel: '#CAC4D0', + background: 'transparent', + areaBorder: '#49454F', + errorBar: '#938F99', + crosshairLine: '#CAC4D0', + crosshairBackground: 'rgba(73, 69, 78, 0.1)', + crosshairFill: '#E6E1E5', + crosshairLabel: '#313033', + tooltipFill: '#E6E1E5', + tooltipBoldLabel: '#313033', + tooltipLightLabel: '#313033', + tooltipHeaderLine: '#313033', + markerShadow: null, + selectionRectFill: 'rgba(78, 170, 255, 0.06)', + selectionRectStroke: '#4EAAFF', + selectionCircleStroke: '#938F99', + tabColor: '#CAC4D0', + bearFillColor: '#B3F32F', + bullFillColor: '#FF9E45', + toolkitSelectionColor: '#CAC4D0', + toolkitFill: '#CAC4D0', + toolkitIconRectOverFill: '#4F378B', + toolkitIconRectSelectionFill: '#4F378B', + toolkitIconRect: { x: -4, y: -5, height: 26, width: 26 }, + chartTitleFont: { + color: '#E6E1E5', fontFamily: 'Roboto', fontSize: '16px', fontStyle: 'SemiBold', fontWeight: '600' + }, + axisLabelFont: { + color: '#E6E1E5', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Regular', fontWeight: '400' + }, + legendTitleFont: { + color: '#E6E1E5', fontFamily: 'Roboto', fontSize: '14px', fontStyle: 'Normal', fontWeight: '600' + }, + legendLabelFont: { + color: '#E6E1E5', fontFamily: 'Roboto', fontSize: Browser.isDevice ? '12px' : '14px', fontStyle: 'Regular', fontWeight: '400' + }, + tooltipLabelFont: { + color: '#313033', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Normal', fontWeight: '' + }, + axisTitleFont: { + color: '#E6E1E5', fontFamily: 'Roboto', fontSize: '14px', fontStyle: 'Regular', fontWeight: '400' + }, + datalabelFont: { + color: '#E8DEF8', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Normal', fontWeight: '400' + }, + chartSubTitleFont: { + color: '#CAC4D0', fontFamily: 'Roboto', fontSize: '14px', fontStyle: 'Regular', fontWeight: '400' + }, + crosshairLabelFont: { + color: '#313033', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Normal', fontWeight: '400' + }, + stripLineLabelFont: { + color: '#E6E1E5', fontFamily: 'Roboto', fontSize: '12px', fontStyle: 'Normal', fontWeight: '400' + } + }; + break; + } + + return style; +} + + +/** + * Gets an array of series colors for chart visualization. + * + * @param {Theme} theme - The theme for which to retrieve the series colors. + * @returns {string[]} - An array of series colors. + * @private + */ +export function getSeriesColor(theme: Theme): string[] { + let palette: string[]; + switch (theme) { + case 'Material3': + palette = [ + '#1E88E5', '#F25087', '#FB8C00', '#43A047', '#E53935', + '#706C6C', '#F2BD02', '#00ACC1', '#7443B2', '#324070' + ]; + break; + case 'Material3Dark': + palette = [ + '#1E88E5', '#F25087', '#FB8C00', '#43A047', '#E53935', + '#706C6C', '#F2BD02', '#00ACC1', '#7443B2', '#324070' + ]; + break; + } + return palette; +} diff --git a/components/charts/src/chart/zooming/ChartZooming.tsx b/components/charts/src/chart/zooming/ChartZooming.tsx new file mode 100644 index 0000000..048574a --- /dev/null +++ b/components/charts/src/chart/zooming/ChartZooming.tsx @@ -0,0 +1,34 @@ + +import * as React from 'react'; +import { ChartContext } from '../layout/ChartProvider'; +import { defaultChartConfigs } from '../base/default-properties'; +import { ChartZoomSettingsProps } from '../base/interfaces'; +import { ChartProviderChildProps } from '../chart-area/chart-interfaces'; + +/** + * React component that configures zoom settings for the chart. + * This component doesn't render any UI elements but manages zoom configuration through context. + * + * @param {ChartZoomSettingsProps} props - Zoom settings configuration properties. + * @returns {null} This component doesn't render any elements. + */ +export const ChartZoomSettings: React.FC = (props: ChartZoomSettingsProps) => { + const context: ChartProviderChildProps = React.useContext(ChartContext); + + // Memoize the merged zoom config + const zoomConfig: ChartZoomSettingsProps = React.useMemo(() => ({ + ...defaultChartConfigs.ChartZoom, + ...props + }), [ + props.selectionZoom, props.accessibility, props.mouseWheelZoom, + props.pinchZoom, props.pan, + props.mode, props.toolbar + ]); + + // Only update context when zoomConfig changes + React.useEffect(() => { + context?.setChartZoom(zoomConfig); + }, [zoomConfig]); + + return null; +}; diff --git a/components/charts/src/index.ts b/components/charts/src/index.ts new file mode 100644 index 0000000..b148251 --- /dev/null +++ b/components/charts/src/index.ts @@ -0,0 +1 @@ +export * from './chart/index'; diff --git a/components/inputs/tsconfig.json b/components/charts/tsconfig.json similarity index 100% rename from components/inputs/tsconfig.json rename to components/charts/tsconfig.json diff --git a/components/data/CHANGELOG.md b/components/data/CHANGELOG.md new file mode 100644 index 0000000..30d5cee --- /dev/null +++ b/components/data/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## [Unreleased] \ No newline at end of file diff --git a/components/base/README.md b/components/data/README.md similarity index 70% rename from components/base/README.md rename to components/data/README.md index 3f691a4..2dead91 100644 --- a/components/base/README.md +++ b/components/data/README.md @@ -1,14 +1,6 @@ -# React Base Library +# React data -A common package of Essential® studio react components which contains base libraries, providers and functions. - -**Key Features** - -* Animation -* Ripple -* Internationalization -* Localization -* Right to Left +`@syncfusion/react-data` is a data management package to perform data operations such as grouping, sorting in client applications. It will act as an abstraction for using local data sources like array of JavaScript objects and remote data sources like web services returning JSON, JSONP, oData or XML.

Trusted by the world's leading companies @@ -19,10 +11,10 @@ Trusted by the world's leading companies ## Setup -To install `base` and its dependent packages, use the following command, +To install `@syncfusion/react-data` and its dependent packages, use the following command: ```sh -npm install @syncfusion/react-base +npm install @syncfusion/react-data ``` ## Support @@ -33,7 +25,7 @@ Product support is available through following mediums. * Live chat ## Changelog -Check the changelog [here](https://github.com/syncfusion/react-ui-components/blob/master/components/base/CHANGELOG.md). Get minor improvements and bug fixes every week to stay up to date with frequent updates. +Check the changelog [here](https://github.com/syncfusion/react-ui-components/blob/master/components/data/CHANGELOG.md). Get minor improvements and bug fixes every week to stay up to date with frequent updates. ## License and copyright @@ -43,4 +35,4 @@ Check the changelog [here](https://github.com/syncfusion/react-ui-components/blo See [LICENSE FILE](https://github.com/syncfusion/react-ui-components/blob/master/license?utm_source=npm&utm_campaign=notification) for more info. -© Copyright 2025 Syncfusion, Inc. All Rights Reserved. The Syncfusion Essential Studio license and copyright applies to this distribution. +© Copyright 2025 Syncfusion, Inc. All Rights Reserved. The Syncfusion Essential Studio license and copyright applies to this distribution. diff --git a/components/icons/gulpfile.js b/components/data/gulpfile.js similarity index 100% rename from components/icons/gulpfile.js rename to components/data/gulpfile.js diff --git a/components/base/license b/components/data/license similarity index 100% rename from components/base/license rename to components/data/license diff --git a/components/base/package.json b/components/data/package.json similarity index 71% rename from components/base/package.json rename to components/data/package.json index d843ac1..5b9d0d6 100644 --- a/components/base/package.json +++ b/components/data/package.json @@ -1,18 +1,17 @@ { - "name": "@syncfusion/react-base", - "version": "30.1.37", - "description": "A common package of core React base, methods and class definitions", + "name": "@syncfusion/react-data", + "version": "31.1.17", + "description": "A shared React package providing foundational data management classes, utilities, and methods for handling data operations across UI components.", "author": "Syncfusion Inc.", "license": "SEE LICENSE IN license", "keywords": [ "syncfusion", "web-components", "react", - "react-base", + "react-data", "syncfusion-react", "base", - "library", - "react-base" + "library" ], "repository": { "type": "git", @@ -21,8 +20,8 @@ "homepage": "https://www.syncfusion.com/react-ui-components", "module": "./index.js", "readme": "README.md", - "bin": { - "syncfusion-license": "bin/syncfusion-license.js" + "dependencies": { + "@syncfusion/react-base": "~31.1.17" }, "devDependencies": { "gulp": "4.0.2", diff --git a/components/data/src/adaptors.ts b/components/data/src/adaptors.ts new file mode 100644 index 0000000..cd2bc10 --- /dev/null +++ b/components/data/src/adaptors.ts @@ -0,0 +1,3037 @@ +/* eslint-disable valid-jsdoc */ +/* eslint-disable prefer-rest-params */ +/* eslint-disable security/detect-object-injection */ +import { IFetch } from '@syncfusion/react-base'; +import { merge, extend, isNullOrUndefined, getValue } from '@syncfusion/react-base'; +import { DataUtil, Aggregates, Group, GraphQLParams } from './util'; +import { DataManager, DataOptions } from './manager'; +import { Query, Predicate, QueryOptions, QueryList, ParamOption } from './query'; +const consts: { [key: string]: string } = { GroupGuid: '{271bbba0-1ee7}' }; +/** + * Adaptors are specific data source type aware interfaces that are used by DataManager to communicate with DataSource. + * This is the base adaptor class that other adaptors can extend. + * + * @hidden + */ +export class Adaptor { + /** + * Specifies the datasource option. + * + * @default null + */ + public dataSource: DataOptions; + + public updateType: string; + + public updateKey: string; + + /** + * It contains the datamanager operations list like group, searches, etc., + * + * @default null + * @hidden + */ + public pvt: PvtOptions; + + /** + * Constructor for Adaptor class + * + * @param {DataOptions} ds? + * @param ds + * @hidden + * @returns aggregates + */ + constructor(ds?: DataOptions) { + this.dataSource = ds; + this.pvt = {}; + } + + // common options for all the adaptors + protected options: RemoteOptions = { + from: 'table', + requestType: 'json', + sortBy: 'sorted', + select: 'select', + skip: 'skip', + group: 'group', + take: 'take', + search: 'search', + count: 'requiresCounts', + where: 'where', + aggregates: 'aggregates', + expand: 'expand' + }; + + /** + * Returns the data from the query processing. + * + * @param {Object} data + * @param {DataOptions} ds? + * @param {Query} query? + * @param {Request} xhr? + * @param ds + * @param query + * @param xhr + * @returns Object + */ + public processResponse(data: Object, ds?: DataOptions, query?: Query, xhr?: Request): Object { + return data; + } + + /** + * Specifies the type of adaptor. + * + * @default Adaptor + */ + public type: Object = Adaptor; +} + +/** + * JsonAdaptor is used to process JSON data. It contains methods to process the given JSON data based on the queries. + * + * @hidden + */ +export class JsonAdaptor extends Adaptor { + + /** + * Process the JSON data based on the provided queries. + * + * @param {DataManager} dataManager + * @param {Query} query + * @returns Object + */ + public processQuery(dataManager: DataManager, query: Query): Object { + let result: Object = dataManager.dataSource.json.slice(0); + let count: number = (result as Object[]).length; + let countFlg: boolean = true; + let ret: Object[]; + let key: QueryOptions; + const lazyLoad: object = {}; + let keyCount: number = 0; + const group: object[] = []; + const sort: { comparer: (a: Object, b: Object) => number, fieldName: string }[] = []; + let page: { pageIndex: number, pageSize: number }; + for (let i: number = 0; i < query.lazyLoad.length; i++) { + keyCount++; + lazyLoad[query.lazyLoad[i].key] = query.lazyLoad[i].value; + } + const agg: { [key: string]: Object } = {}; + let isGroupByFormat: boolean = false; + if(query.lazyLoad.length) { + for (let i: number = 0; i < query.queries.length; i++) { + key = query.queries[i]; + if(key.fn === 'onGroup' && !isNullOrUndefined(key.e.format)) { + isGroupByFormat = true; + break; + } + } + } + for (let i: number = 0; i < query.queries.length; i++) { + key = query.queries[i]; + if ((key.fn === 'onPage' || key.fn === 'onGroup' || (key.fn === 'onSortBy' && !isGroupByFormat)) && query.lazyLoad.length) { + if (key.fn === 'onGroup') { + group.push(key.e); + } + if (key.fn === 'onPage') { + page = key.e as { pageIndex: number, pageSize: number }; + } + if (key.fn === 'onSortBy') { + sort.unshift(key.e as { comparer: (a: Object, b: Object) => number, fieldName: string }); + } + continue; + } + ret = this[key.fn].call(this, result, key.e, query); + if (key.fn === 'onAggregates') { + agg[key.e.field + ' - ' + key.e.type] = ret; + } else { + result = ret !== undefined ? ret : result; + } + if (key.fn === 'onPage' || key.fn === 'onSkip' || key.fn === 'onTake' || key.fn === 'onRange') { + countFlg = false; + } + if (countFlg) { + count = (result as Object[]).length; + } + } + + if (keyCount) { + const args: LazyLoadGroupArgs = { + query: query, lazyLoad: lazyLoad as LazyLoad, result: result as Object[], group: group, page: page, sort: sort + }; + const lazyLoadData: { result: Object[], count: number } = this.lazyLoadGroup(args); + result = lazyLoadData.result; + count = lazyLoadData.count; + } + + if (query.isCountRequired) { + result = { + result: result, + count: count, + aggregates: agg + }; + } + return result; + } + + /** + * Perform lazy load grouping in JSON array based on the given query and lazy load details. + * + * @param {LazyLoadGroupArgs} args + */ + public lazyLoadGroup(args: LazyLoadGroupArgs): { result: Object[], count: number } { + let count: number = 0; + const agg: object[] = this.getAggregate(args.query); + let result: Object[] = args.result; + if (!isNullOrUndefined(args.lazyLoad.onDemandGroupInfo)) { + const req: OnDemandGroupInfo = args.lazyLoad.onDemandGroupInfo; + for (let i: number = req.where.length - 1; i >= 0; i--) { + result = this.onWhere(result, req.where[i]); + } + if (args.group.length !== req.level) { + const field: string = (<{ fieldName?: string }>args.group[req.level]).fieldName; + result = DataUtil.group(result, field, agg, null, null, (<{ comparer?: Function }>args.group[req.level]).comparer, true); + if(args.sort.length) { + result = this.onSortBy(result, args.sort[parseInt(req.level.toString(), 10)], args.query, true); + } + } else { + for (let i: number = args.sort.length - 1; i >= req.level; i--) { + result = this.onSortBy(result, args.sort[parseInt(i.toString(), 10)], args.query, false); + } + } + count = result.length; + const data: Object[] = result; + result = result.slice(req.skip); + result = result.slice(0, req.take); + if (args.group.length !== req.level) { + this.formGroupResult(result, data); + } + } else { + const field: string = (<{ fieldName?: string }>args.group[0]).fieldName; + result = DataUtil.group(result, field, agg, null, null, (<{ comparer?: Function }>args.group[0]).comparer, true); + count = result.length; + const data: Object[] = result; + if (args.sort.length) { + const sort: { comparer: (a: Object, b: Object) => number, fieldName: string } = args.sort.length > 1 ? + args.sort.filter((x) => x.fieldName === field)[0] : args.sort[0]; + result = this.onSortBy(result, sort, args.query, true); + } + if (args.page) { + result = this.onPage(result, args.page, args.query); + } + this.formGroupResult(result, data); + } + return { result: result, count: count }; + } + + private formGroupResult(result: Object[], data: Object[]): Object[] { + if (result.length && data.length) { + const uid: string = 'GroupGuid'; const childLevel: string = 'childLevels'; const level: string = 'level'; + const records: string = 'records'; + result[uid] = data[uid]; + result[childLevel] = data[childLevel]; + result[level] = data[level]; + result[records] = data[records]; + } + return result; + } + + /** + * Separate the aggregate query from the given queries + * + * @param {Query} query + */ + public getAggregate(query: Query): Object[] { + const aggQuery: QueryOptions[] = Query.filterQueries(query.queries, 'onAggregates') as QueryOptions[]; + const agg: Object[] = []; + if (aggQuery.length) { + let tmp: QueryOptions; + for (let i: number = 0; i < aggQuery.length; i++) { + tmp = aggQuery[i].e; + agg.push({ type: tmp.type, field: DataUtil.getValue(tmp.field, query) }); + } + } + return agg; + } + + /** + * Performs batch update in the JSON array which add, remove and update records. + * + * @param {DataManager} dm + * @param {CrudOptions} changes + * @param {RemoteArgs} e + */ + public batchRequest(dm: DataManager, changes: CrudOptions, e: RemoteArgs): CrudOptions { + let i: number; + const deletedRecordsLen: number = changes.deletedRecords.length; + for (i = 0; i < changes.addedRecords.length; i++) { + this.insert(dm, changes.addedRecords[i]); + } + for (i = 0; i < changes.changedRecords.length; i++) { + this.update(dm, e.key, changes.changedRecords[i]); + } + for (i = 0; i < deletedRecordsLen; i++) { + this.remove(dm, e.key, changes.deletedRecords[i]); + } + return changes; + } + + /** + * Performs filter operation with the given data and where query. + * + * @param {Object[]} ds + * @param {{validate:Function}} e + * @param e.validate + */ + public onWhere(ds: Object[], e: { validate: Function }): Object[] { + if (!ds || !ds.length) { return ds; } + return ds.filter((obj: Object) => { + if (e) { return e.validate(obj); } + }); + } + + /** + * Returns aggregate function based on the aggregate type. + * + * @param {Object[]} ds + * @param e + * @param {string} } type + * @param e.field + * @param e.type + */ + public onAggregates(ds: Object[], e: { field: string, type: string }): Function { + const fn: Function = DataUtil.aggregates[e.type] as Function; + if (!ds || !fn || ds.length === 0) { return null; } + return fn(ds, e.field); + } + + /** + * Performs search operation based on the given query. + * + * @param {Object[]} ds + * @param {QueryOptions} e + */ + public onSearch(ds: Object[], e: QueryOptions): Object[] { + if (!ds || !ds.length) { return ds; } + + if (e.fieldNames.length === 0) { + DataUtil.getFieldList(ds[0], e.fieldNames); + } + + return ds.filter((obj: Object): boolean => { + for (let j: number = 0; j < e.fieldNames.length; j++) { + if ((e.comparer).call(obj, DataUtil.getObject(e.fieldNames[j], obj), e.searchKey, e.ignoreCase, e.ignoreAccent)) { + return true; + } + } + return false; + }); + } + + /** + * Sort the data with given direction and field. + * + * @param {Object[]} ds + * @param e + * @param {Object} b + * @param e.comparer + * @param e.fieldName + * @param query + * @param isLazyLoadGroupSort + */ + public onSortBy(ds: Object[], e: { comparer: (a: Object, b: Object) => number, fieldName: string }, query: Query, isLazyLoadGroupSort?: boolean): Object[] { + if (!ds || !ds.length) { return ds; } + let fnCompare: Function; + let field: string[] | string = DataUtil.getValue(e.fieldName, query); + if (!field) { + return ds.sort(e.comparer); + } + + if (field instanceof Array) { + field = field.slice(0); + + for (let i: number = field.length - 1; i >= 0; i--) { + if (!field[i]) { continue; } + fnCompare = e.comparer; + + if (DataUtil.endsWith(field[i], ' desc')) { + fnCompare = DataUtil.fnSort('descending'); + field[i] = field[i].replace(' desc', ''); + } + + ds = DataUtil.sort(ds, field[i], fnCompare); + } + return ds; + } + return DataUtil.sort(ds, isLazyLoadGroupSort ? 'key' : field, e.comparer); + } + + /** + * Group the data based on the given query. + * + * @param {Object[]} ds + * @param {QueryOptions} e + * @param {Query} query + */ + public onGroup(ds: Object[], e: QueryOptions, query: Query): Object[] { + if (!ds || !ds.length) { return ds; } + const agg: Object[] = this.getAggregate(query); + return DataUtil.group(ds, DataUtil.getValue(e.fieldName, query), agg, null, null, e.comparer as Function); + } + + /** + * Retrieves records based on the given page index and size. + * + * @param {Object[]} ds + * @param e + * @param {number} } pageIndex + * @param e.pageSize + * @param {Query} query + * @param e.pageIndex + */ + public onPage(ds: Object[], e: { pageSize: number, pageIndex: number }, query: Query): Object[] { + const size: number = DataUtil.getValue(e.pageSize, query); + const start: number = (DataUtil.getValue(e.pageIndex, query) - 1) * size; + const end: number = start + size; + if (!ds || !ds.length) { return ds; } + return ds.slice(start, end); + } + + /** + * Retrieves records based on the given start and end index from query. + * + * @param {Object[]} ds + * @param e + * @param {number} } end + * @param e.start + * @param e.end + */ + public onRange(ds: Object[], e: { start: number, end: number }): Object[] { + if (!ds || !ds.length) { return ds; } + return ds.slice(DataUtil.getValue(e.start), DataUtil.getValue(e.end)); + } + + /** + * Picks the given count of records from the top of the datasource. + * + * @param {Object[]} ds + * @param {{nos:number}} e + * @param e.nos + */ + public onTake(ds: Object[], e: { nos: number }): Object[] { + if (!ds || !ds.length) { return ds; } + return ds.slice(0, DataUtil.getValue(e.nos)); + } + + /** + * Skips the given count of records from the data source. + * + * @param {Object[]} ds + * @param {{nos:number}} e + * @param e.nos + */ + public onSkip(ds: Object[], e: { nos: number }): Object[] { + if (!ds || !ds.length) { return ds; } + return ds.slice(DataUtil.getValue(e.nos)); + } + + /** + * Selects specified columns from the data source. + * + * @param {Object[]} ds + * @param {{fieldNames:string}} e + * @param e.fieldNames + */ + public onSelect(ds: Object[], e: { fieldNames: string[] | Function }): Object[] { + if (!ds || !ds.length) { return ds; } + return DataUtil.select(ds, DataUtil.getValue(e.fieldNames)); + } + + /** + * Inserts new record in the table. + * + * @param {DataManager} dm + * @param {Object} data + * @param tableName + * @param query + * @param {number} position + */ + public insert(dm: DataManager, data: Object, tableName?: string, query?: Query, position?: number): Object { + if (isNullOrUndefined(position)) { + return dm.dataSource.json.push(data); + } else { + return dm.dataSource.json.splice(position, 0, data); + } + } + + /** + * Remove the data from the dataSource based on the key field value. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {Object} value + * @param {string} tableName? + * @param tableName + * @returns null + */ + public remove(dm: DataManager, keyField: string, value: Object, tableName?: string): Object { + const ds: Object[] = dm.dataSource.json; + let i: number; + if (typeof value === 'object' && !(value instanceof Date)) { + value = DataUtil.getObject(keyField, value); + } + for (i = 0; i < ds.length; i++) { + if (DataUtil.getObject(keyField, ds[i]) === value) { break; } + } + + return i !== ds.length ? ds.splice(i, 1) : null; + } + + /** + * Updates existing record and saves the changes to the table. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {Object} value + * @param {string} tableName? + * @param tableName + * @returns null + */ + public update(dm: DataManager, keyField: string, value: Object, tableName?: string): void { + const ds: Object[] = dm.dataSource.json; + let i: number; + let key: string; + if (!isNullOrUndefined(keyField)) { + key = getValue(keyField, value as { [key: string]: string }); + } + for (i = 0; i < ds.length; i++) { + if (!isNullOrUndefined(keyField) && (getValue(keyField, ds[i] as { [key: string]: string })) === key) { break; } + } + return i < ds.length ? merge(ds[i], value) : null; + } +} + +/** + * URL Adaptor of DataManager can be used when you are required to use remote service to retrieve data. + * It interacts with server-side for all DataManager Queries and CRUD operations. + * + * @hidden + */ +export class UrlAdaptor extends Adaptor { + + /** + * Process the query to generate request body. + * + * @param {DataManager} dm + * @param {Query} query + * @param {Object[]} hierarchyFilters? + * @param hierarchyFilters + * @returns p + */ + // tslint:disable-next-line:max-func-body-length + public processQuery(dm: DataManager, query: Query, hierarchyFilters?: Object[]): Object { + const queries: Requests = this.getQueryRequest(query); + const singles: QueryList = Query.filterQueryLists(query.queries, ['onSelect', 'onPage', 'onSkip', 'onTake', 'onRange']); + const params: ParamOption[] = query.params; + const url: string = dm.dataSource.url; + let temp: QueryOptions; + let skip: number; + let take: number = null; + const options: RemoteOptions = this.options; + const request: Requests = { sorts: [], groups: [], filters: [], searches: [], aggregates: [] }; + // calc Paging & Range + if ('onPage' in singles) { + temp = singles.onPage; + skip = DataUtil.getValue(temp.pageIndex, query); + take = DataUtil.getValue(temp.pageSize, query); + skip = (skip - 1) * take; + } else if ('onRange' in singles) { + temp = singles.onRange; + skip = temp.start; + take = temp.end - temp.start; + } + // Sorting + for (let i: number = 0; i < queries.sorts.length; i++) { + temp = DataUtil.getValue(queries.sorts[i].e.fieldName, query) as QueryOptions; + request.sorts.push(DataUtil.callAdaptorFunction( + this, 'onEachSort', { name: temp, direction: queries.sorts[i].e.direction }, query)); + } + // hierarchy + if (hierarchyFilters) { + temp = (this.getFiltersFrom(hierarchyFilters, query)); + if (temp) { + request.filters.push(DataUtil.callAdaptorFunction(this, 'onEachWhere', (temp).toJson(), query)); + } + } + // Filters + for (let i: number = 0; i < queries.filters.length; i++) { + let res: Object = DataUtil.callAdaptorFunction(this, 'onEachWhere', (queries.filters[i].e).toJson(), query); + if (((<{ getModuleName?: Function }>this).getModuleName && + (<{ getModuleName?: Function }>this).getModuleName() === 'ODataV4Adaptor') && + !isNullOrUndefined(queries.filters[i].e.key) && queries.filters.length > 1) { + res = '(' + res + ')'; + } + request.filters.push(res); + const keys: string[] = typeof request.filters[i] === 'object' ? Object.keys(request.filters[i]) : []; + for (const prop of keys) { + if (DataUtil.isNull((request)[prop])) { + delete request[prop]; + } + } + } + // Searches + for (let i: number = 0; i < queries.searches.length; i++) { + temp = queries.searches[i].e; + request.searches.push(DataUtil.callAdaptorFunction( + this, 'onEachSearch', { + fields: temp.fieldNames, + operator: temp.operator, + key: temp.searchKey, + ignoreCase: temp.ignoreCase, + ignoreAccent: temp.ignoreAccent + }, + query)); + } + // Grouping + for (let i: number = 0; i < queries.groups.length; i++) { + request.groups.push(DataUtil.getValue(queries.groups[i].e.fieldName, query) as QueryOptions); + } + // aggregates + for (let i: number = 0; i < queries.aggregates.length; i++) { + temp = queries.aggregates[i].e; + request.aggregates.push({ type: temp.type, field: DataUtil.getValue(temp.field, query) }); + } + const req: { [key: string]: Object } = {}; + this.getRequestQuery(options, query, singles, request, req); + // Params + DataUtil.callAdaptorFunction(this, 'addParams', { dm: dm, query: query, params: params, reqParams: req }); + if (query.lazyLoad.length) { + for (let i: number = 0; i < query.lazyLoad.length; i++) { + req[query.lazyLoad[i].key] = query.lazyLoad[i].value; + } + } + // cleanup + const keys: string[] = Object.keys(req); + for (const prop of keys) { + if (DataUtil.isNull(req[prop]) || req[prop] === '' || (req[prop]).length === 0) { + delete req[prop]; + } + } + if (!(options.skip in req && options.take in req) && take !== null) { + req[options.skip] = DataUtil.callAdaptorFunction(this, 'onSkip', skip, query); + req[options.take] = DataUtil.callAdaptorFunction(this, 'onTake', take, query); + } + const p: PvtOptions = this.pvt; + this.pvt = {}; + if (this.options.requestType === 'json') { + return { + data: JSON.stringify(req, DataUtil.parse.jsonDateReplacer), + url: url, + pvtData: p, + type: 'POST', + contentType: 'application/json; charset=utf-8' + }; + } + temp = this.convertToQueryString(req, query, dm) as QueryOptions; + temp = (dm.dataSource.url.indexOf('?') !== -1 ? '&' : '/') + temp as QueryOptions; + return { + type: 'GET', url: (temp).length ? url.replace(/\/*$/, temp) : url, pvtData: p + }; + } + + private getRequestQuery( + options: RemoteOptions, query: Query, singles: QueryList, request: Requests, request1: { [key: string]: Object }): void { + + const param: string = 'param'; + const req: { [key: string]: Object } = request1; + req[options.from] = query.fromTable; + if (options.apply && query.distincts.length) { + req[options.apply] = 'onDistinct' in this ? DataUtil.callAdaptorFunction(this, 'onDistinct', query.distincts) : ''; + } + if (!query.distincts.length && options.expand) { + req[options.expand] = 'onExpand' in this && 'onSelect' in singles ? + DataUtil.callAdaptorFunction( + this, + 'onExpand', + { selects: DataUtil.getValue(singles.onSelect.fieldNames, query), expands: query.expands }, + query) : query.expands; + } + req[options.select] = 'onSelect' in singles && !query.distincts.length ? + DataUtil.callAdaptorFunction(this, 'onSelect', DataUtil.getValue(singles.onSelect.fieldNames, query), query) : ''; + req[options.count] = query.isCountRequired ? DataUtil.callAdaptorFunction(this, 'onCount', query.isCountRequired, query) : ''; + req[options.search] = request.searches.length ? DataUtil.callAdaptorFunction(this, 'onSearch', request.searches, query) : ''; + req[options.skip] = 'onSkip' in singles ? + DataUtil.callAdaptorFunction(this, 'onSkip', DataUtil.getValue(singles.onSkip.nos, query), query) : ''; + req[options.take] = 'onTake' in singles ? + DataUtil.callAdaptorFunction(this, 'onTake', DataUtil.getValue(singles.onTake.nos, query), query) : ''; + req[options.where] = request.filters.length || request.searches.length ? + DataUtil.callAdaptorFunction(this, 'onWhere', request.filters, query) : ''; + req[options.sortBy] = request.sorts.length ? DataUtil.callAdaptorFunction(this, 'onSortBy', request.sorts, query) : ''; + req[options.group] = request.groups.length ? DataUtil.callAdaptorFunction(this, 'onGroup', request.groups, query) : ''; + req[options.aggregates] = request.aggregates.length ? + DataUtil.callAdaptorFunction(this, 'onAggregates', request.aggregates, query) : ''; + req[param] = []; + } + + /** + * Convert the object from processQuery to string which can be added query string. + * + * @param {Object} req + * @param request + * @param {Query} query + * @param {DataManager} dm + */ + public convertToQueryString(request: Object, query: Query, dm: DataManager): string { + return ''; + // this needs to be overridden + } + + /** + * Return the data from the data manager processing. + * + * @param {DataResult} data + * @param {DataOptions} ds? + * @param {Query} query? + * @param {Request} xhr? + * @param {Object} request? + * @param {CrudOptions} changes? + * @param ds + * @param query + * @param xhr + * @param request + * @param changes + */ + public processResponse( + data: DataResult, ds?: DataOptions, query?: Query, xhr?: Request, request?: Object, changes?: CrudOptions): DataResult { + if (xhr && xhr.headers.get('Content-Type') && + xhr.headers.get('Content-Type').indexOf('application/json') !== -1) { + const handleTimeZone: boolean = DataUtil.timeZoneHandling; + if (ds && !ds.timeZoneHandling) { + DataUtil.timeZoneHandling = false; + } + if (!ds.enableCache) { + data = DataUtil.parse.parseJson(data); + } + DataUtil.timeZoneHandling = handleTimeZone; + } + const requests: { pvtData?: Object, data?: string } = request; + const pvt: PvtOptions = requests.pvtData || {}; + const groupDs: Object[] = data ? data.groupDs : []; + if (xhr && xhr.headers.get('Content-Type') && + xhr.headers.get('Content-Type').indexOf('xml') !== -1) { + return (query.isCountRequired ? { result: [], count: 0 } : []) as DataResult; + } + const d: { action: string } = JSON.parse(requests.data); + if (d && d.action === 'batch' && data && data.addedRecords && !isNullOrUndefined(changes)) { + changes.addedRecords = data.addedRecords; + return changes; + } + if (data && data.d) { + data = data.d; + } + const args: DataResult = {}; + if (data && 'count' in data) { args.count = data.count; } + args.result = data && data.result ? data.result : data; + let isExpand: boolean = false; + if (Array.isArray(data.result) && data.result.length) { + const key: string = 'key'; const val: string = 'value'; const level: string = 'level'; + if (!isNullOrUndefined(data.result[0][key])) { + args.result = this.formRemoteGroupedData(args.result as Group[], 1, pvt.groups.length - 1); + } + if (query && query.lazyLoad.length && pvt.groups.length) { + for (let i: number = 0; i < query.lazyLoad.length; i++) { + if (query.lazyLoad[i][key] === 'onDemandGroupInfo') { + const value: Object = query.lazyLoad[i][val][level]; + if (pvt.groups.length === value) { + isExpand = true; + } + } + } + } + } + if (!isExpand) { + this.getAggregateResult(pvt, data, args, groupDs, query); + } + + return DataUtil.isNull(args.count) ? args.result : { result: args.result, count: args.count, aggregates: args.aggregates }; + } + + protected formRemoteGroupedData(data: Group[], level: number, childLevel: number): Group[] { + for (let i: number = 0; i < data.length; i++) { + if (data[i].items.length && Object.keys(data[i].items[0]).indexOf('key') > -1) { + this.formRemoteGroupedData(data[i].items, level + 1, childLevel - 1); + } + } + + const uid: string = 'GroupGuid'; const childLvl: string = 'childLevels'; const lvl: string = 'level'; + const records: string = 'records'; + data[uid] = consts[uid]; + data[lvl] = level; + data[childLvl] = childLevel; + data[records] = data[0].items.length ? this.getGroupedRecords(data, !isNullOrUndefined(data[0].items[records])) : []; + return data; + } + + private getGroupedRecords(data: Group[], hasRecords: boolean): Object[] { + let childGroupedRecords: Object[] = []; + const records: string = 'records'; + for (let i: number = 0; i < data.length; i++) { + if (!hasRecords) { + for (let j: number = 0; j < data[i].items.length; j++) { + childGroupedRecords.push(data[i].items[j]); + } + } else { + childGroupedRecords = childGroupedRecords.concat(data[i].items[records]); + } + } + return childGroupedRecords; + } + + /** + * Add the group query to the adaptor`s option. + * + * @param {Object[]} e + * @returns void + */ + public onGroup(e: QueryOptions[]): QueryOptions[] { + this.pvt.groups = e; + return e; + } + + /** + * Add the aggregate query to the adaptor`s option. + * + * @param {Aggregates[]} e + * @returns void + */ + public onAggregates(e: Aggregates[]): void { + this.pvt.aggregates = e; + } + + /** + * Prepare the request body based on the newly added, removed and updated records. + * The result is used by the batch request. + * + * @param {DataManager} dm + * @param {CrudOptions} changes + * @param {Object} e + * @param query + * @param original + */ + public batchRequest(dm: DataManager, changes: CrudOptions, e: Object, query: Query, original?: Object): Object { + let url: string; + let key: string; + return { + type: 'POST', + url: dm.dataSource.batchUrl || dm.dataSource.crudUrl || dm.dataSource.removeUrl || dm.dataSource.url, + contentType: 'application/json; charset=utf-8', + dataType: 'json', + data: JSON.stringify(extend({}, { + changed: changes.changedRecords, + added: changes.addedRecords, + deleted: changes.deletedRecords, + action: 'batch', + table: e[url], + key: e[key] + }, DataUtil.getAddParams(this, dm, query))) + }; + } + + /** + * Method will trigger before send the request to server side. + * Used to set the custom header or modify the request options. + * + * @param {DataManager} dm + * @param {Request} request + * @param {Fetch} settings? + * @returns void + */ + public beforeSend(dm: DataManager, request: Request, settings?: IFetch): void { + // need to extend this method + } + + /** + * Prepare and returns request body which is used to insert a new record in the table. + * + * @param {DataManager} dm + * @param {Object} data + * @param {string} tableName + * @param query + */ + public insert(dm: DataManager, data: Object, tableName: string, query: Query): Object { + return { + url: dm.dataSource.insertUrl || dm.dataSource.crudUrl || dm.dataSource.url, + data: JSON.stringify(extend({}, { + value: data, + table: tableName, + action: 'insert' + }, DataUtil.getAddParams(this, dm, query))) + }; + } + + /** + * Prepare and return request body which is used to remove record from the table. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {number|string} value + * @param {string} tableName + * @param query + */ + public remove(dm: DataManager, keyField: string, value: number | string, tableName: string, query: Query): Object { + return { + type: 'POST', + url: dm.dataSource.removeUrl || dm.dataSource.crudUrl || dm.dataSource.url, + data: JSON.stringify(extend({}, { + key: value, + keyColumn: keyField, + table: tableName, + action: 'remove' + }, DataUtil.getAddParams(this, dm, query))) + }; + } + + /** + * Prepare and return request body which is used to update record. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {Object} value + * @param {string} tableName + * @param query + */ + public update(dm: DataManager, keyField: string, value: Object, tableName: string, query: Query): Object { + return { + type: 'POST', + url: dm.dataSource.updateUrl || dm.dataSource.crudUrl || dm.dataSource.url, + data: JSON.stringify(extend({}, { + value: value, + action: 'update', + keyColumn: keyField, + key: DataUtil.getObject(keyField, value), + table: tableName + }, DataUtil.getAddParams(this, dm, query))) + }; + } + + /** + * To generate the predicate based on the filtered query. + * + * @param {Object[]|string[]|number[]} data + * @param {Query} query + * @hidden + */ + public getFiltersFrom(data: Object[] | string[] | number[], query: Query): Predicate { + const key: string = query.fKey; + let value: string | number | boolean; + let prop: string = key; + const pKey: string = query.key; + const predicats: Predicate[] = []; + + if (typeof data[0] !== 'object') { prop = null; } + + for (let i: number = 0; i < data.length; i++) { + if (typeof data[0] === 'object') { + value = DataUtil.getObject(pKey || prop, data[i]); + } else { + value = (data)[i]; + } + predicats.push(new Predicate(key, 'equal', value)); + } + + return Predicate.or(predicats); + } + + protected getAggregateResult(pvt: PvtOptions, data: DataResult, args: DataResult, groupDs?: Object[], query?: Query): DataResult { + let pData: DataResult | Object[] = data; + if (data && data.result) { pData = data.result; } + if (pvt && pvt.aggregates && pvt.aggregates.length) { + const agg: Aggregates[] = pvt.aggregates; + let fn: Function; + let aggregateData: DataResult = pData; + const res: { [key: string]: Aggregates } = {}; + if (data.aggregate) { aggregateData = data.aggregate; } + for (let i: number = 0; i < agg.length; i++) { + fn = DataUtil.aggregates[agg[i].type]; + if (fn) { + res[agg[i].field + ' - ' + agg[i].type] = fn(aggregateData, agg[i].field); + } + } + args.aggregates = res; + } + + const key: string = 'key'; + const isServerGrouping: boolean = Array.isArray(data.result) && data.result.length && !isNullOrUndefined(data.result[0][key]); + if (pvt && pvt.groups && pvt.groups.length && !isServerGrouping) { + const groups: string[] = (pvt.groups); + for (let i: number = 0; i < groups.length; i++) { + const level: number = null; + if (!isNullOrUndefined(groupDs)) { + groupDs = DataUtil.group(groupDs, groups[i]); + } + const groupQuery: QueryOptions = Query.filterQueries(query.queries, 'onGroup')[i].e; + pData = DataUtil.group(pData, groups[i], pvt.aggregates, level, groupDs, groupQuery.comparer as Function); + } + args.result = pData; + } + return args; + } + + protected getQueryRequest(query: Query): Requests { + const req: Requests = { sorts: [], groups: [], filters: [], searches: [], aggregates: [] }; + req.sorts = Query.filterQueries(query.queries, 'onSortBy'); + req.groups = Query.filterQueries(query.queries, 'onGroup'); + req.filters = Query.filterQueries(query.queries, 'onWhere'); + req.searches = Query.filterQueries(query.queries, 'onSearch'); + req.aggregates = Query.filterQueries(query.queries, 'onAggregates'); + return req; + } + + public addParams(options: { dm: DataManager, query: Query, params: ParamOption[], reqParams: { [key: string]: Object } }): void { + const req: { params: Object } = options.reqParams as { params: Object }; + if (options.params.length) { + req.params = {}; + } + for (const tmp of options.params) { + if (req[tmp.key]) { + throw new Error('Query() - addParams: Custom Param is conflicting other request arguments'); + } + req[tmp.key] = tmp.value; + if (tmp.fn) { + req[tmp.key] = tmp.fn.call(options.query, tmp.key, options.query, options.dm); + } + req.params[tmp.key] = req[tmp.key]; + } + } + +} + +/** + * OData Adaptor that is extended from URL Adaptor, is used for consuming data through OData Service. + * + * @hidden + */ +export class ODataAdaptor extends UrlAdaptor { + + protected getModuleName(): string { + return 'ODataAdaptor'; + } + + /** + * Specifies the root url of the provided odata url. + * + * @hidden + * @default null + */ + public rootUrl: string; + + /** + * Specifies the resource name of the provided odata table. + * + * @hidden + * @default null + */ + public resourceTableName: string; + + // options replaced the default adaptor options + protected options: RemoteOptions = extend({}, this.options, { + requestType: 'get', + accept: 'application/json;odata=light;q=1,application/json;odata=verbose;q=0.5', + multipartAccept: 'multipart/mixed', + sortBy: '$orderby', + select: '$select', + skip: '$skip', + take: '$top', + count: '$inlinecount', + where: '$filter', + expand: '$expand', + batch: '$batch', + changeSet: '--changeset_', + batchPre: 'batch_', + contentId: 'Content-Id: ', + batchContent: 'Content-Type: multipart/mixed; boundary=', + changeSetContent: 'Content-Type: application/http\nContent-Transfer-Encoding: binary ', + batchChangeSetContentType: 'Content-Type: application/json; charset=utf-8 ', + updateType: 'PUT' + }); + constructor(props?: RemoteOptions) { + super(); + extend(this.options, props || {}); + } + /** + * Generate request string based on the filter criteria from query. + * + * @param {Predicate} pred + * @param {boolean} requiresCast? + * @param predicate + * @param query + * @param requiresCast + */ + public onPredicate(predicate: Predicate, query: Query | boolean, requiresCast?: boolean): string { + let returnValue: string = ''; + let operator: string; + let guid: string; + let val: string | Date = predicate.value; + const type: string = typeof val; + let field: string = predicate.field ? ODataAdaptor.getField(predicate.field) : null; + + if (val instanceof Date) { + val = 'datetime\'' + DataUtil.parse.replacer(val) + '\''; + } + + if (type === 'string') { + val = val.replace(/'/g, '\'\''); + if (predicate.ignoreCase) { + val = val.toLowerCase(); + } + if (predicate.operator !== 'like') { + val = encodeURIComponent(val); + } + if (predicate.operator !== 'wildcard' && predicate.operator !== 'like') { + val = '\'' + val + '\''; + } + + if (requiresCast) { + field = 'cast(' + field + ', \'Edm.String\')'; + } + if (DataUtil.parse.isGuid(val)) { + guid = 'guid'; + } + if (predicate.ignoreCase) { + if (!guid) { field = 'tolower(' + field + ')'; } + val = (val).toLowerCase(); + } + } + + if (predicate.operator === 'isEmpty' || predicate.operator === 'isnull' || predicate.operator === 'isNotEmpty' || + predicate.operator === 'isNotNull') { + operator = predicate.operator.indexOf('isnot') !== -1 ? DataUtil.odBiOperator['notequal'] : DataUtil.odBiOperator['equal']; + val = predicate.operator === 'isnull' || predicate.operator === 'isNotNull' ? null : '\'\''; + } + else { + operator = DataUtil.odBiOperator[predicate.operator]; + } + if (operator) { + returnValue += field; + returnValue += operator; + if (guid) { + returnValue += guid; + } + return returnValue + val; + } + + if (!isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ) { + operator = DataUtil.odv4UniOperator[predicate.operator]; + } else { + operator = DataUtil.odUniOperator[predicate.operator]; + } + + if (operator === 'like') { + val = val; + if (val.indexOf('%') !== -1) { + if (val.charAt(0) === '%' && val.lastIndexOf('%') < 2) { + val = val.substring(1, val.length); + operator = !isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ? + DataUtil.odv4UniOperator['startswith'] : DataUtil.odUniOperator['startswith']; + } + else if (val.charAt(val.length - 1) === '%' && val.indexOf('%') > val.length - 3) { + val = val.substring(0, val.length - 1); + operator = !isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ? + DataUtil.odv4UniOperator['endswith'] : DataUtil.odUniOperator['endswith']; + } + else if (val.lastIndexOf('%') !== val.indexOf('%') && val.lastIndexOf('%') > val.indexOf('%') + 1) { + val = val.substring(val.indexOf('%') + 1, val.lastIndexOf('%')); + operator = !isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ? + DataUtil.odv4UniOperator['contains'] : DataUtil.odUniOperator['contains']; + } + else { + operator = !isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ? + DataUtil.odv4UniOperator['contains'] : DataUtil.odUniOperator['contains']; + } + } + val = encodeURIComponent(val); + val = '\'' + val + '\''; + } + else if (operator === 'wildcard') { + val = val; + if (val.indexOf('*') !== -1) { + const splittedStringValue: string[] = val.split('*'); + let splittedValue: string; + let count: number = 0; + if (val.indexOf('*') !== 0 && splittedStringValue[0].indexOf('%3f') === -1 && + splittedStringValue[0].indexOf('?') === -1) { + splittedValue = splittedStringValue[0]; + splittedValue = '\'' + splittedValue + '\''; + operator = !isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ? + DataUtil.odv4UniOperator['startswith'] : DataUtil.odUniOperator['startswith']; + returnValue += operator + '('; + returnValue += field + ','; + if (guid) { returnValue += guid; } + returnValue += splittedValue + ')'; + count++; + } + if (val.lastIndexOf('*') !== val.length - 1 && splittedStringValue[splittedStringValue.length - 1].indexOf('%3f') === -1 && + splittedStringValue[splittedStringValue.length - 1].indexOf('?') === -1) { + splittedValue = splittedStringValue[splittedStringValue.length - 1]; + splittedValue = '\'' + splittedValue + '\''; + operator = !isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ? + DataUtil.odv4UniOperator['endswith'] : DataUtil.odUniOperator['endswith']; + if (count > 0) { + returnValue += ' and '; + } + returnValue += operator + '('; + returnValue += field + ','; + if (guid) { returnValue += guid; } + returnValue += splittedValue + ')'; + count++; + } + if (splittedStringValue.length > 2) { + for (let i: number = 1; i < splittedStringValue.length - 1; i++) { + if (splittedStringValue[i].indexOf('%3f') === -1 && splittedStringValue[i].indexOf('?') === -1) { + splittedValue = splittedStringValue[i]; + splittedValue = '\'' + splittedValue + '\''; + operator = !isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ? + DataUtil.odv4UniOperator['contains'] : DataUtil.odUniOperator['contains']; + if (count > 0) { + returnValue += ' and '; + } + if (operator === 'substringof' || operator === 'not substringof') { + const temp: string = splittedValue; + splittedValue = field; + field = temp; + } + returnValue += operator + '('; + returnValue += field + ','; + if (guid) { returnValue += guid; } + returnValue += splittedValue + ')'; + count++; + } + } + } + if (count === 0) { + operator = !isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ? + DataUtil.odv4UniOperator['contains'] : DataUtil.odUniOperator['contains']; + if (val.indexOf('?') !== -1 || val.indexOf('%3f') !== -1) { + val = val.indexOf('?') !== -1 ? val.split('?').join('') : val.split('%3f').join(''); + } + val = '\'' + val + '\''; + } + else { + operator = 'wildcard'; + } + } + else { + operator = !isNullOrUndefined(this.getModuleName) && this.getModuleName() === 'ODataV4Adaptor' ? + DataUtil.odv4UniOperator['contains'] : DataUtil.odUniOperator['contains']; + if (val.indexOf('?') !== -1 || val.indexOf('%3f') !== -1) { + val = val.indexOf('?') !== -1 ? val.split('?').join('') : val.split('%3f').join(''); + } + val = '\'' + val + '\''; + } + } + if (operator === 'substringof' || operator === 'not substringof') { + const temp: string = val; + val = field; + field = temp; + } + + if (operator !== 'wildcard') { + returnValue += operator + '('; + returnValue += field + ','; + if (guid) { returnValue += guid; } + returnValue += val + ')'; + } + + return returnValue; + } + + public addParams(options: { dm: DataManager, query: Query, params: ParamOption[], reqParams: { [key: string]: Object } }): void { + super.addParams(options); + delete options.reqParams.params; + } + + /** + * Generate request string based on the multiple filter criteria from query. + * + * @param {Predicate} pred + * @param {boolean} requiresCast? + * @param predicate + * @param query + * @param requiresCast + */ + public onComplexPredicate(predicate: Predicate, query: Query, requiresCast?: boolean): string { + const res: string[] = []; + for (let i: number = 0; i < predicate.predicates.length; i++) { + res.push('(' + this.onEachWhere(predicate.predicates[i], query, requiresCast) + ')'); + } + return res.join(' ' + predicate.condition + ' '); + } + + /** + * Generate query string based on the multiple filter criteria from query. + * + * @param {Predicate} filter + * @param {boolean} requiresCast? + * @param query + * @param requiresCast + */ + public onEachWhere(filter: Predicate, query: Query, requiresCast?: boolean): string { + return filter.isComplex ? this.onComplexPredicate(filter, query, requiresCast) : this.onPredicate(filter, query, requiresCast); + } + + /** + * Generate query string based on the multiple filter criteria from query. + * + * @param {string[]} filters + */ + public onWhere(filters: string[]): string { + if (this.pvt.search) { + filters.push(this.onEachWhere((this.pvt.search), null, true)); + } + return filters.join(' and '); + } + + /** + * Generate query string based on the multiple search criteria from query. + * + * @param e + * @param {string} operator + * @param {string} key + * @param {boolean} } ignoreCase + * @param e.fields + * @param e.operator + * @param e.key + * @param e.ignoreCase + */ + public onEachSearch(e: { fields: string[], operator: string, key: string, ignoreCase: boolean }): void { + if (e.fields && e.fields.length === 0) { + DataUtil.throwError('Query() - Search : oData search requires list of field names to search'); + } + + const filter: Object[] = (this.pvt.search) || []; + for (let i: number = 0; i < e.fields.length; i++) { + filter.push(new Predicate(e.fields[i], e.operator, e.key, e.ignoreCase)); + } + this.pvt.search = filter; + } + + /** + * Generate query string based on the search criteria from query. + * + * @param {Object} e + */ + public onSearch(e: Object): string { + this.pvt.search = Predicate.or(this.pvt.search); + return ''; + } + + /** + * Generate query string based on multiple sort criteria from query. + * + * @param {QueryOptions} e + */ + public onEachSort(e: QueryOptions): string { + const res: string[] = []; + if (e.name instanceof Array) { + for (let i: number = 0; i < e.name.length; i++) { + res.push(ODataAdaptor.getField(e.name[i]) + (e.direction === 'descending' ? ' desc' : '')); + } + } else { + res.push(ODataAdaptor.getField(e.name) + (e.direction === 'descending' ? ' desc' : '')); + } + return res.join(','); + } + + /** + * Returns sort query string. + * + * @param {string[]} e + */ + public onSortBy(e: string[]): string { + return e.reverse().join(','); + } + + /** + * Adds the group query to the adaptor option. + * + * @param {Object[]} e + * @returns string + */ + public onGroup(e: QueryOptions[]): QueryOptions[] { + this.pvt.groups = e; + return []; + } + + /** + * Returns the select query string. + * + * @param {string[]} e + */ + public onSelect(e: string[]): string { + for (let i: number = 0; i < e.length; i++) { + e[i] = ODataAdaptor.getField(e[i]); + } + return e.join(','); + } + + /** + * Add the aggregate query to the adaptor option. + * + * @param {Object[]} e + * @returns string + */ + public onAggregates(e: Object[]): string { + this.pvt.aggregates = e; + return ''; + } + + /** + * Returns the query string which requests total count from the data source. + * + * @param {boolean} e + * @returns string + */ + public onCount(e: boolean): string { + return e === true ? 'allpages' : ''; + } + + /** + * Method will trigger before send the request to server side. + * Used to set the custom header or modify the request options. + * + * @param {DataManager} dm + * @param {Request} request + * @param {Fetch} settings? + * @param settings + */ + public beforeSend(dm: DataManager, request: Request, settings?: IFetch): void { + if (DataUtil.endsWith(settings.url, this.options.batch) && settings.type.toLowerCase() === 'post') { + request.headers.set('Accept', this.options.multipartAccept); + request.headers.set('DataServiceVersion', '2.0'); + //request.overrideMimeType('text/plain; charset=x-user-defined'); + } else { + request.headers.set('Accept', this.options.accept); + } + request.headers.set('DataServiceVersion', '2.0'); + request.headers.set('MaxDataServiceVersion', '2.0'); + } + + /** + * Returns the data from the query processing. + * + * @param {DataResult} data + * @param {DataOptions} ds? + * @param {Query} query? + * @param {Request} xhr? + * @param {Fetch} request? + * @param {CrudOptions} changes? + * @param ds + * @param query + * @param xhr + * @param request + * @param changes + * @returns aggregateResult + */ + public processResponse( + data: DataResult, ds?: DataOptions, query?: Query, xhr?: Request, request?: IFetch, changes?: CrudOptions): Object { + const metaCheck: string = 'odata.metadata'; + if ((request && request.type === 'GET') && !this.rootUrl && data[metaCheck]) { + const dataUrls: string[] = data[metaCheck].split('/$metadata#'); + this.rootUrl = dataUrls[0]; + this.resourceTableName = dataUrls[1]; + } + const pvtData: string = 'pvtData'; + if (!isNullOrUndefined(data.d)) { + const dataCopy: Object[] = ((query && query.isCountRequired) ? (data.d).results : data.d); + const metaData: string = '__metadata'; + if (!isNullOrUndefined(dataCopy)) { + for (let i: number = 0; i < dataCopy.length; i++) { + if (!isNullOrUndefined(dataCopy[i][metaData])) { + delete dataCopy[i][metaData]; + } + } + } + } + const pvt: PvtOptions = request && request[pvtData]; + + const emptyAndBatch: CrudOptions | DataResult = this.processBatchResponse(data, query, xhr, request, changes); + if (emptyAndBatch) { + return emptyAndBatch; + } + + const versionCheck: string = xhr && request.fetchRequest.headers.get('DataServiceVersion'); + let count: number = null; + const version: number = (versionCheck && parseInt(versionCheck, 10)) || 2; + + if (query && query.isCountRequired) { + const oDataCount: string = '__count'; + if (data[oDataCount] || data['odata.count']) { + count = data[oDataCount] || data['odata.count']; + } + if (data.d) { data = data.d; } + if (data[oDataCount] || data['odata.count']) { + count = data[oDataCount] || data['odata.count']; + } + } + + if (version === 3 && data.value) { data = data.value; } + if (data.d) { data = data.d; } + if (version < 3 && data.results) { data = data.results as DataResult; } + + const args: DataResult = {}; + args.count = count; + args.result = data; + this.getAggregateResult(pvt, data, args, null, query); + + return DataUtil.isNull(count) ? args.result : { result: args.result, count: args.count, aggregates: args.aggregates }; + } + + /** + * Converts the request object to query string. + * + * @param {Object} req + * @param request + * @param {Query} query + * @param {DataManager} dm + * @returns tableName + */ + public convertToQueryString(request: Object, query: Query, dm: DataManager): string { + let res: string[] | string = []; + const table: string = 'table'; + const tableName: string = request[table] || ''; + const format: string = '$format'; + delete request[table]; + + if (dm.dataSource.requiresFormat) { + request[format] = 'json'; + } + const keys: string[] = Object.keys(request); + for (const prop of keys) { + (res).push(prop + '=' + request[prop]); + } + res = (res).join('&'); + + if (dm.dataSource.url && dm.dataSource.url.indexOf('?') !== -1 && !tableName) { + return (res); + } + + return res.length ? tableName + '?' + res : tableName || ''; + } + + private localTimeReplacer(key: string, convertObj: Object): Object { + for (const prop of !isNullOrUndefined(convertObj) ? Object.keys(convertObj) : []) { + if ((convertObj[prop] instanceof Date)) { + convertObj[prop] = DataUtil.dateParse.toLocalTime(convertObj[prop]); + } + } + return convertObj; + } + + /** + * Prepare and returns request body which is used to insert a new record in the table. + * + * @param {DataManager} dm + * @param {Object} data + * @param {string} tableName? + * @param tableName + */ + public insert(dm: DataManager, data: Object, tableName?: string): Object { + return { + url: (dm.dataSource.insertUrl || dm.dataSource.url).replace(/\/*$/, tableName ? '/' + tableName : ''), + data: JSON.stringify(data, this.options.localTime ? this.localTimeReplacer : null) + }; + } + + /** + * Prepare and return request body which is used to remove record from the table. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {number} value + * @param {string} tableName? + * @param tableName + */ + public remove(dm: DataManager, keyField: string, value: number, tableName?: string): Object { + let url: string; + if (typeof value === 'string' && !DataUtil.parse.isGuid(value)) { + url = `('${value}')`; + } else { + url = `(${value})`; + } + return { + type: 'DELETE', + url: (dm.dataSource.removeUrl || dm.dataSource.url).replace(/\/*$/, tableName ? '/' + tableName : '') + url + }; + } + + /** + * Updates existing record and saves the changes to the table. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {Object} value + * @param {string} tableName? + * @param tableName + * @param query + * @param original + * @returns this + */ + public update(dm: DataManager, keyField: string, value: Object, tableName?: string, query?: Query, original?: Object): Object { + if (this.options.updateType === 'PATCH' && !isNullOrUndefined(original)) { + value = this.compareAndRemove(value, original, keyField); + } + let url: string; + if (typeof value[keyField] === 'string' && !DataUtil.parse.isGuid(value[keyField])) { + url = `('${value[keyField]}')`; + } else { + url = `(${value[keyField]})`; + } + return { + type: this.options.updateType, + url: (dm.dataSource.updateUrl || dm.dataSource.url).replace(/\/*$/, tableName ? '/' + tableName : '') + url, + data: JSON.stringify(value, this.options.localTime ? this.localTimeReplacer : null), + accept: this.options.accept + }; + } + + /** + * Prepare the request body based on the newly added, removed and updated records. + * The result is used by the batch request. + * + * @param {DataManager} dm + * @param {CrudOptions} changes + * @param {RemoteArgs} e + * @param query + * @param original + * @returns {Object} + */ + public batchRequest(dm: DataManager, changes: CrudOptions, e: RemoteArgs, query: Query, original?: CrudOptions): Object { + const initialGuid: string = e.guid = DataUtil.getGuid(this.options.batchPre); + const url: string = (dm.dataSource.batchUrl || this.rootUrl) ? + (dm.dataSource.batchUrl || this.rootUrl) + '/' + this.options.batch : + (dm.dataSource.batchUrl || dm.dataSource.url).replace(/\/*$/, '/' + this.options.batch); + e.url = this.resourceTableName ? this.resourceTableName : e.url; + const args: RemoteArgs = { + url: e.url, + key: e.key, + cid: 1, + cSet: DataUtil.getGuid(this.options.changeSet) + }; + let req: string = '--' + initialGuid + '\n'; + + req += 'Content-Type: multipart/mixed; boundary=' + args.cSet.replace('--', '') + '\n'; + + this.pvt.changeSet = 0; + + req += this.generateInsertRequest(changes.addedRecords, args, dm); + req += this.generateUpdateRequest(changes.changedRecords, args, dm, original ? original.changedRecords : []); + req += this.generateDeleteRequest(changes.deletedRecords, args, dm); + + req += args.cSet + '--\n'; + req += '--' + initialGuid + '--'; + + return { + type: 'POST', + url: url, + dataType: 'json', + contentType: 'multipart/mixed; charset=UTF-8;boundary=' + initialGuid, + data: req + }; + } + + /** + * Generate the string content from the removed records. + * The result will be send during batch update. + * + * @param {Object[]} arr + * @param {RemoteArgs} e + * @param dm + * @returns this + */ + public generateDeleteRequest(arr: Object[], e: RemoteArgs, dm: DataManager): string { + if (!arr) { return ''; } + let req: string = ''; + + const stat: { method: string, url: Function, data: Function } = { + 'method': 'DELETE ', + 'url': (data: Object[], i: number, key: string): string => { + const url: object = DataUtil.getObject(key, data[i]); + if (typeof url === 'number' || DataUtil.parse.isGuid(url)) { + return '(' + url as string + ')'; + } else if (url instanceof Date) { + const dateTime: Date = data[i][key]; + return '(' + dateTime.toJSON() + ')'; + } else { + return `('${url}')`; + } + }, + 'data': (data: Object[], i: number): string => '' + }; + req = this.generateBodyContent(arr, e, stat, dm); + + return req + '\n'; + } + + /** + * Generate the string content from the inserted records. + * The result will be send during batch update. + * + * @param {Object[]} arr + * @param {RemoteArgs} e + * @param dm + */ + public generateInsertRequest(arr: Object[], e: RemoteArgs, dm: DataManager): string { + if (!arr) { return ''; } + let req: string = ''; + + const stat: { method: string, url: Function, data: Function } = { + 'method': 'POST ', + 'url': (data: Object[], i: number, key: string): string => '', + 'data': (data: Object[], i: number): string => JSON.stringify(data[i]) + '\n\n' + }; + req = this.generateBodyContent(arr, e, stat, dm); + + return req; + } + + /** + * Generate the string content from the updated records. + * The result will be send during batch update. + * + * @param {Object[]} arr + * @param {RemoteArgs} e + * @param dm + * @param org + */ + public generateUpdateRequest(arr: Object[], e: RemoteArgs, dm: DataManager, org?: Object[]): string { + if (!arr) { return ''; } + let req: string = ''; + arr.forEach((change: Object) => change = this.compareAndRemove( + change, org.filter((o: Object) => DataUtil.getObject(e.key, o) === DataUtil.getObject(e.key, change))[0], + e.key) + ); + const stat: { method: string, url: Function, data: Function } = { + 'method': this.options.updateType + ' ', + 'url': (data: Object[], i: number, key: string): string => { + if (typeof data[i][key] === 'number' || DataUtil.parse.isGuid(data[i][key])) { + return '(' + data[i][key] as string + ')'; + } else if (data[i][key] instanceof Date) { + const date: Date = data[i][key]; + return '(' + date.toJSON() + ')'; + } else { + return `('${data[i][key]}')`; + } + }, + 'data': (data: Object[], i: number): string => JSON.stringify(data[i]) + '\n\n' + }; + req = this.generateBodyContent(arr, e, stat, dm); + + return req; + } + + protected static getField(prop: string): string { + return prop.replace(/\./g, '/'); + } + + private generateBodyContent(arr: Object[], e: RemoteArgs, stat: { method: string, url: Function, data: Function }, dm: DataManager) + : string { + let req: string = ''; + for (let i: number = 0; i < arr.length; i++) { + req += '\n' + e.cSet + '\n'; + req += this.options.changeSetContent + '\n\n'; + req += stat.method; + if (stat.method === 'POST ') { + req += (dm.dataSource.insertUrl || dm.dataSource.crudUrl || e.url) + stat.url(arr, i, e.key) + ' HTTP/1.1\n'; + } else if (stat.method === 'PUT ' || stat.method === 'PATCH ') { + req += (dm.dataSource.updateUrl || dm.dataSource.crudUrl || e.url) + stat.url(arr, i, e.key) + ' HTTP/1.1\n'; + } else if (stat.method === 'DELETE ') { + req += (dm.dataSource.removeUrl || dm.dataSource.crudUrl || e.url) + stat.url(arr, i, e.key) + ' HTTP/1.1\n'; + } + req += 'Accept: ' + this.options.accept + '\n'; + req += 'Content-Id: ' + this.pvt.changeSet++ + '\n'; + req += this.options.batchChangeSetContentType + '\n'; + if (!isNullOrUndefined(arr[i]['@odata.etag'])) { + req += 'If-Match: ' + arr[i]['@odata.etag'] + '\n\n'; + delete arr[i]['@odata.etag']; + } else { + req += '\n'; + } + + req += stat.data(arr, i); + } + return req; + } + + protected processBatchResponse( + data: DataResult, query?: Query, xhr?: Request, request?: IFetch, changes?: CrudOptions): CrudOptions | DataResult { + if (xhr && xhr.headers.get('Content-Type') && xhr.headers.get('Content-Type').indexOf('xml') !== -1) { + return (query.isCountRequired ? { result: [], count: 0 } : []) as DataResult; + } + if (request && this.options.batch && DataUtil.endsWith(request.url, this.options.batch) && request.type.toLowerCase() === 'post') { + let guid: string = xhr.headers.get('Content-Type'); + let cIdx: number; + let jsonObj: Object; let d: string | string[] = data + ''; + guid = guid.substring(guid.indexOf('=batchresponse') + 1); + d = (d).split(guid); + if ((d).length < 2) { return {}; } + + d = (d)[1]; + const exVal: RegExpExecArray = /(?:\bContent-Type.+boundary=)(changesetresponse.+)/i.exec(d); + if (exVal) { (d).replace(exVal[0], ''); } + + const changeGuid: string = exVal ? exVal[1] : ''; + d = (d).split(changeGuid); + for (let i: number = (d).length; i > -1; i--) { + if (!/\bContent-ID:/i.test((d)[i]) || !/\bHTTP.+201/.test((d)[i])) { + continue; + } + + cIdx = parseInt(/\bContent-ID: (\d+)/i.exec((d)[i])[1], 10); + + if (changes.addedRecords[cIdx]) { + jsonObj = DataUtil.parse.parseJson(/^\{.+\}/m.exec(d[i])[0]); + extend({}, changes.addedRecords[cIdx], this.processResponse(jsonObj)); + } + } + return changes; + } + return null; + } + + public compareAndRemove(data: Object, original: Object, key?: string): Object { + if (isNullOrUndefined(original)) { return data; } + Object.keys(data).forEach((prop: string) => { + if (prop !== key && prop !== '@odata.etag') { + if (DataUtil.isPlainObject(data[prop])) { + this.compareAndRemove(data[prop], original[prop]); + const final: string[] = Object.keys(data[prop]).filter((data: string) => data !== '@odata.etag'); + if (final.length === 0) { delete data[prop]; } + } else if (data[prop] === original[prop]) { + delete data[prop]; + } else if (data[prop] && original[prop] && data[prop].valueOf() === original[prop].valueOf()) { + delete data[prop]; + } + } + }); + return data; + } +} + +/** + * The OData v4 is an improved version of OData protocols. + * The DataManager uses the ODataV4Adaptor to consume OData v4 services. + * + * @hidden + */ +export class ODataV4Adaptor extends ODataAdaptor { + + /** + * @hidden + */ + protected getModuleName(): string { + return 'ODataV4Adaptor'; + } + + // options replaced the default adaptor options + protected options: RemoteOptions = extend({}, this.options, { + requestType: 'get', + accept: 'application/json, text/javascript, */*; q=0.01', + multipartAccept: 'multipart/mixed', + sortBy: '$orderby', + select: '$select', + skip: '$skip', + take: '$top', + count: '$count', + search: '$search', + where: '$filter', + expand: '$expand', + batch: '$batch', + changeSet: '--changeset_', + batchPre: 'batch_', + contentId: 'Content-Id: ', + batchContent: 'Content-Type: multipart/mixed; boundary=', + changeSetContent: 'Content-Type: application/http\nContent-Transfer-Encoding: binary ', + batchChangeSetContentType: 'Content-Type: application/json; charset=utf-8 ', + updateType: 'PATCH', + localTime: false, + apply: '$apply' + }); + + constructor(props?: RemoteOptions) { + super(props); + extend(this.options, props || {}); + } + + /** + * Returns the query string which requests total count from the data source. + * + * @param {boolean} e + * @returns string + */ + public onCount(e: boolean): string { + return e === true ? 'true' : ''; + } + + /** + * Generate request string based on the filter criteria from query. + * + * @param {Predicate} pred + * @param {boolean} requiresCast? + * @param predicate + * @param query + * @param requiresCast + */ + public onPredicate(predicate: Predicate, query: Query | boolean, requiresCast?: boolean): string { + let returnValue: string = ''; + const val: string | number | Date | boolean | Predicate | Predicate[] | (string | number | boolean | Date)[] = predicate.value; + const isDate: boolean = val instanceof Date; + + if (query instanceof Query) { + const queries: Requests = this.getQueryRequest((query as Query)); + for (let i: number = 0; i < queries.filters.length; i++) { + if (queries.filters[i].e.key === predicate.value) { + requiresCast = true; + } + } + } + + returnValue = super.onPredicate.call(this, predicate, query, requiresCast); + + if (isDate) { + returnValue = returnValue.replace(/datetime'(.*)'$/, '$1'); + } + if (DataUtil.parse.isGuid(val)) { + returnValue = returnValue.replace('guid', '').replace(/'/g, ''); + } + return returnValue; + } + + /** + * Generate query string based on the multiple search criteria from query. + * + * @param e + * @param {string} operator + * @param {string} key + * @param {boolean} } ignoreCase + * @param e.fields + * @param e.operator + * @param e.key + * @param e.ignoreCase + */ + public onEachSearch(e: { fields: string[], operator: string, key: string, ignoreCase: boolean }): void { + const search: Object[] = this.pvt.searches || []; + search.push(e.key); + this.pvt.searches = search; + } + + /** + * Generate query string based on the search criteria from query. + * + * @param {Object} e + */ + public onSearch(e: Object): string { + return this.pvt.searches.join(' OR '); + } + + /** + * Returns the expand query string. + * + * @param {string} e + * @param e.selects + * @param e.expands + */ + public onExpand(e: { selects: string[], expands: string[] }): string { + const selected: Object = {}; const expanded: Object = {}; + const expands: string[] = e.expands.slice(); const exArr: string[] = []; + const selects: Object[] = e.selects.filter((item: string) => item.indexOf('.') > -1); + selects.forEach((select: string) => { + const splits: string[] = select.split('.'); + if (!(splits[0] in selected)) { + selected[splits[0]] = []; + } + if (splits.length === 2) { + if (selected[splits[0]].length && Object.keys(selected).indexOf(splits[0]) !== -1) { + if (selected[splits[0]][0].indexOf('$expand') !== -1 && selected[splits[0]][0].indexOf(';$select=') === -1) { + selected[splits[0]][0] = selected[splits[0]][0] + ';' + '$select=' + splits[1]; + } else { + selected[splits[0]][0] = selected[splits[0]][0] + ',' + splits[1]; + } + } else { + selected[splits[0]].push('$select=' + splits[1]); + } + } else { + const sel: string = '$select=' + splits[splits.length - 1]; + let exp: string = ''; let close: string = ''; + for (let i: number = 1; i < splits.length - 1; i++) { + exp = exp + '$expand=' + splits[i] + '('; + close = close + ')'; + } + const combineVal: string = exp + sel + close; + if (selected[splits[0]].length && Object.keys(selected).indexOf(splits[0]) !== -1 && + this.expandQueryIndex(selected[splits[0]], true)) { + const idx: number | boolean = this.expandQueryIndex(selected[splits[0]]); + selected[splits[0]][idx] = selected[splits[0]][idx] + combineVal.replace('$expand=', ','); + } else { + selected[splits[0]].push(combineVal); + } + } + }); + //Auto expand from select query + Object.keys(selected).forEach((expand: string) => { + if ((expands.indexOf(expand) === -1)) { + expands.push(expand); + } + }); + expands.forEach((expand: string) => { + expanded[expand] = expand in selected ? `${expand}(${selected[expand].join(';')})` : expand; + }); + Object.keys(expanded).forEach((ex: string) => exArr.push(expanded[ex])); + return exArr.join(','); + } + + private expandQueryIndex( query: string[], isExpand?: boolean): boolean | number { + for (let i: number = 0; i < query.length; i++) { + if (query[i].indexOf('$expand') !== -1) { + return isExpand ? true : i; + } + } + return isExpand ? false : 0; + } + + /** + * Returns the groupby query string. + * + * @param {string} e + * @param distinctFields + */ + public onDistinct(distinctFields: string[]): Object { + const fields: string = distinctFields.map((field: string) => ODataAdaptor.getField(field)).join(','); + return `groupby((${fields}))`; + } + + /** + * Returns the select query string. + * + * @param {string[]} e + */ + public onSelect(e: string[]): string { + return super.onSelect(e.filter((item: string) => item.indexOf('.') === -1)); + } + + /** + * Method will trigger before send the request to server side. + * Used to set the custom header or modify the request options. + * + * @param {DataManager} dm + * @param {Request} request + * @param {Fetch} settings + * @returns void + */ + public beforeSend(dm: DataManager, request: Request, settings: IFetch): void { + if (settings.type === 'POST' || settings.type === 'PUT' || settings.type === 'PATCH') { + request.headers.set('Prefer', 'return=representation'); + } + request.headers.set('Accept', this.options.accept); + } + + /** + * Returns the data from the query processing. + * + * @param {DataResult} data + * @param {DataOptions} ds? + * @param {Query} query? + * @param {Request} xhr? + * @param {Fetch} request? + * @param {CrudOptions} changes? + * @param ds + * @param query + * @param xhr + * @param request + * @param changes + * @returns aggregateResult + */ + public processResponse( + data: DataResult, ds?: DataOptions, query?: Query, xhr?: Request, request?: IFetch, changes?: CrudOptions): Object { + const metaName: string = '@odata.context'; + const metaV4Name: string = '@context'; + if ((request && request.type === 'GET') && !this.rootUrl && (data[metaName] || data[metaV4Name])) { + const dataUrl: string[] = data[metaName] ? data[metaName].split('/$metadata#') : data[metaV4Name].split('/$metadata#'); + this.rootUrl = dataUrl[0]; + this.resourceTableName = dataUrl[1]; + } + const pvtData: string = 'pvtData'; + const pvt: PvtOptions = request && request[pvtData]; + + const emptyAndBatch: CrudOptions | DataResult = super.processBatchResponse(data, query, xhr, request, changes); + if (emptyAndBatch) { + return emptyAndBatch; + } + + let count: number = null; + const dataCount: string = '@odata.count'; + const dataV4Count: string = '@count'; + if (query && query.isCountRequired) { + if (dataCount in data) { count = data[dataCount]; } + else if (dataV4Count in data) { count = data[dataV4Count]; } + } + data = !isNullOrUndefined(data.value) ? data.value : data; + + const args: DataResult = {}; + args.count = count; + args.result = data; + + this.getAggregateResult(pvt, data, args, null, query); + + return DataUtil.isNull(count) ? args.result : { result: args.result, count: count, aggregates: args.aggregates }; + } +} + +/** + * The Web API is a programmatic interface to define the request and response messages system that is mostly exposed in JSON or XML. + * The DataManager uses the WebApiAdaptor to consume Web API. + * Since this adaptor is targeted to interact with Web API created using OData endpoint, it is extended from ODataAdaptor + * + * @hidden + */ +export class WebApiAdaptor extends ODataAdaptor { + + protected getModuleName(): string { + return 'WebApiAdaptor'; + } + + /** + * Prepare and returns request body which is used to insert a new record in the table. + * + * @param {DataManager} dm + * @param {Object} data + * @param {string} tableName? + * @param tableName + */ + public insert(dm: DataManager, data: Object, tableName?: string): Object { + return { + type: 'POST', + url: dm.dataSource.url, + data: JSON.stringify(data) + }; + } + + /** + * Prepare and return request body which is used to remove record from the table. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {number} value + * @param {string} tableName? + * @param tableName + */ + public remove(dm: DataManager, keyField: string, value: number, tableName?: string): Object { + return { + type: 'DELETE', + url: dm.dataSource.url + '/' + value, + data: JSON.stringify(value) + }; + } + + /** + * Prepare and return request body which is used to update record. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {Object} value + * @param {string} tableName? + * @param tableName + */ + public update(dm: DataManager, keyField: string, value: Object, tableName?: string): Object { + return { + type: 'PUT', + url: dm.dataSource.url, + data: JSON.stringify(value) + }; + } + + public batchRequest(dm: DataManager, changes: CrudOptions, e: RemoteArgs): Object { + const initialGuid: string = e.guid = DataUtil.getGuid(this.options.batchPre); + const url: string = dm.dataSource.url.replace(/\/*$/, '/' + this.options.batch); + e.url = this.resourceTableName ? this.resourceTableName : e.url; + const req: string[] = []; + //insertion + for (let i: number = 0, x: number = changes.addedRecords.length; i < x; i++) { + changes.addedRecords.forEach((j: number, d: number) => { + const stat: { method: string, url: Function, data: Function } = { + 'method': 'POST ', + 'url': (data: Object[], i: number, key: string): string => '', + 'data': (data: Object[], i: number): string => JSON.stringify(data[i]) + '\n\n' + }; + req.push('--' + initialGuid); + req.push('Content-Type: application/http; msgtype=request', ''); + req.push('POST ' + '/api/' + (dm.dataSource.insertUrl || dm.dataSource.crudUrl || e.url) + + stat.url(changes.addedRecords, i, e.key) + ' HTTP/1.1'); + req.push('Content-Type: ' + 'application/json; charset=utf-8'); + req.push('Host: ' + location.host); + req.push('', j ? JSON.stringify(j) : ''); + }); + } + //updation + for (let i: number = 0, x: number = changes.changedRecords.length; i < x; i++) { + changes.changedRecords.forEach((j: number, d: number) => { + const stat: { method: string, url: Function, data: Function } = { + 'method': this.options.updateType + ' ', + 'url': (data: Object[], i: number, key: string): string => '', + 'data': (data: Object[], i: number): string => JSON.stringify(data[i]) + '\n\n' + }; + req.push('--' + initialGuid); + req.push('Content-Type: application/http; msgtype=request', ''); + req.push('PUT ' + '/api/' + (dm.dataSource.updateUrl || dm.dataSource.crudUrl || e.url) + + stat.url(changes.changedRecords, i, e.key) + ' HTTP/1.1'); + req.push('Content-Type: ' + 'application/json; charset=utf-8'); + req.push('Host: ' + location.host); + req.push('', j ? JSON.stringify(j) : ''); + }); + } + //deletion + for (let i: number = 0, x: number = changes.deletedRecords.length; i < x; i++) { + changes.deletedRecords.forEach((j: number, d: number) => { + const state: { mtd: string, url: Function, data: Function } = { + 'mtd': 'DELETE ', + 'url': (data: Object[], i: number, key: string): string => { + const url: object = DataUtil.getObject(key, data[i]); + if (typeof url === 'number' || DataUtil.parse.isGuid(url)) { + return '/' + url as string; + } else if (url instanceof Date) { + const datTime: Date = data[i][key]; + return '/' + datTime.toJSON(); + } else { + return `/'${url}'`; + } + }, + 'data': (data: Object[], i: number): string => '' + }; + req.push('--' + initialGuid); + req.push('Content-Type: application/http; msgtype=request', ''); + req.push('DELETE ' + '/api/' + (dm.dataSource.removeUrl || dm.dataSource.crudUrl || e.url) + + state.url(changes.deletedRecords, i, e.key) + ' HTTP/1.1'); + req.push('Content-Type: ' + 'application/json; charset=utf-8'); + req.push('Host: ' + location.host); + req.push('', j ? JSON.stringify(j) : ''); + }); + } + req.push('--' + initialGuid + '--', ''); + return { + type: 'POST', + url: url, + contentType: 'multipart/mixed; boundary=' + initialGuid, + data: req.join('\r\n') + }; + } + /** + * Method will trigger before send the request to server side. + * Used to set the custom header or modify the request options. + * + * @param {DataManager} dm + * @param {Request} request + * @param {Fetch} settings + * @returns void + */ + public beforeSend(dm: DataManager, request: Request, settings: IFetch): void { + request.headers.set('Accept', 'application/json, text/javascript, */*; q=0.01'); + } + + /** + * Returns the data from the query processing. + * + * @param {DataResult} data + * @param {DataOptions} ds? + * @param {Query} query? + * @param {Request} xhr? + * @param {Fetch} request? + * @param {CrudOptions} changes? + * @param ds + * @param query + * @param xhr + * @param request + * @param changes + * @returns aggregateResult + */ + public processResponse( + data: DataResult, ds?: DataOptions, query?: Query, xhr?: Request, request?: IFetch, changes?: CrudOptions): Object { + const pvtData: string = 'pvtData'; + const pvt: PvtOptions = request && request[pvtData]; + let count: number = null; + const args: DataResult = {}; + if (request && request.type.toLowerCase() !== 'post') { + const versionCheck: string = xhr && request.fetchRequest.headers.get('DataServiceVersion'); + const version: number = (versionCheck && parseInt(versionCheck, 10)) || 2; + + if (query && query.isCountRequired) { + if (!DataUtil.isNull(data.Count)) { count = data.Count; } + } + + if (version < 3 && data.Items) { data = data.Items as DataResult; } + + args.count = count; + args.result = data; + + this.getAggregateResult(pvt, data, args, null, query); + } + args.result = args.result || data; + return DataUtil.isNull(count) ? args.result : { result: args.result, count: args.count, aggregates: args.aggregates }; + } + +} + +/** + * WebMethodAdaptor can be used by DataManager to interact with web method. + * + * @hidden + */ +export class WebMethodAdaptor extends UrlAdaptor { + + /** + * Prepare the request body based on the query. + * The query information can be accessed at the WebMethod using variable named `value`. + * + * @param {DataManager} dm + * @param {Query} query + * @param {Object[]} hierarchyFilters? + * @param hierarchyFilters + * @returns application + */ + public processQuery(dm: DataManager, query: Query, hierarchyFilters?: Object[]): Object { + const obj: Object = new UrlAdaptor().processQuery(dm, query, hierarchyFilters); + const getData: string = 'data'; + const data: { param: Object[] } = DataUtil.parse.parseJson(obj[getData]); + const result: { [key: string]: Object } = {}; + const value: string = 'value'; + + if (data.param) { + for (let i: number = 0; i < data.param.length; i++) { + const param: Object = data.param[i]; + const key: string = Object.keys(param)[0]; + result[key] = param[key]; + } + } + result[value] = data; + const pvtData: string = 'pvtData'; + const url: string = 'url'; + return { + data: JSON.stringify(result, DataUtil.parse.jsonDateReplacer), + url: obj[url], + pvtData: obj[pvtData], + type: 'POST', + contentType: 'application/json; charset=utf-8' + }; + } +} + +/** + * RemoteSaveAdaptor, extended from JsonAdaptor and it is used for binding local data and performs all DataManager queries in client-side. + * It interacts with server-side only for CRUD operations. + * + * @hidden + */ +export class RemoteSaveAdaptor extends JsonAdaptor { + /** + * @hidden + */ + constructor() { + super(); + } + public insert(dm: DataManager, data: Object, tableName: string, query: Query, position?: number): Object { + this.pvt.position = position; + this.updateType = 'add'; + return { + url: dm.dataSource.insertUrl || dm.dataSource.crudUrl || dm.dataSource.url, + data: JSON.stringify(extend({}, { + value: data, + table: tableName, + action: 'insert' + }, DataUtil.getAddParams(this, dm, query))) + }; + } + public remove(dm: DataManager, keyField: string, val: Object, tableName?: string, query?: Query): Object { + super.remove(dm, keyField, val); + return { + type: 'POST', + url: dm.dataSource.removeUrl || dm.dataSource.crudUrl || dm.dataSource.url, + data: JSON.stringify(extend({}, { + key: val, + keyColumn: keyField, + table: tableName, + action: 'remove' + }, DataUtil.getAddParams(this, dm, query))) + }; + } + public update(dm: DataManager, keyField: string, val: Object, tableName: string, query?: Query): Object { + this.updateType = 'update'; + this.updateKey = keyField; + return { + type: 'POST', + url: dm.dataSource.updateUrl || dm.dataSource.crudUrl || dm.dataSource.url, + data: JSON.stringify(extend({}, { + value: val, + action: 'update', + keyColumn: keyField, + key: val[keyField], + table: tableName + }, DataUtil.getAddParams(this, dm, query))) + }; + } + + public processResponse( + data: CrudOptions, ds?: DataOptions, query?: Query, xhr?: Request, request?: IFetch, changes?: CrudOptions, e?: RemoteArgs): + Object { + let i: number; + const newData: CrudOptions = request ? JSON.parse((<{ data?: string }>request).data) : data; + data = newData.action === 'batch' ? DataUtil.parse.parseJson(data) : data; + if (this.updateType === 'add') { + super.insert(ds as DataManager, data, null, null, this.pvt.position); + } + if (this.updateType === 'update') { + super.update(ds as DataManager, this.updateKey, data); + } + this.updateType = undefined; + if (data.added) { + for (i = 0; i < data.added.length; i++) { + super.insert(ds as DataManager, data.added[i]); + } + } + if (data.changed) { + for (i = 0; i < data.changed.length; i++) { + super.update(ds as DataManager, e.key, data.changed[i]); + } + } + if (data.deleted) { + for (i = 0; i < data.deleted.length; i++) { + super.remove(ds as DataManager, e.key, data.deleted[i]); + } + } + return data; + } + + /** + * Prepare the request body based on the newly added, removed and updated records. + * Also perform the changes in the locally cached data to sync with the remote data. + * The result is used by the batch request. + * + * @param {DataManager} dm + * @param {CrudOptions} changes + * @param {RemoteArgs} e + * @param query + * @param original + */ + public batchRequest(dm: DataManager, changes: CrudOptions, e: RemoteArgs, query?: Query, original?: Object): Object { + return { + type: 'POST', + url: dm.dataSource.batchUrl || dm.dataSource.crudUrl || dm.dataSource.url, + contentType: 'application/json; charset=utf-8', + dataType: 'json', + data: JSON.stringify(extend({}, { + changed: changes.changedRecords, + added: changes.addedRecords, + deleted: changes.deletedRecords, + action: 'batch', + table: e.url, + key: e.key + }, DataUtil.getAddParams(this, dm, query))) + }; + } + + public addParams(options: { dm: DataManager, query: Query, params: ParamOption[], reqParams: { [key: string]: Object } }): void { + const urlParams: UrlAdaptor = new UrlAdaptor(); + urlParams.addParams(options); + } + + /** + * Method will trigger before send the request to server side. + * Used to set the custom header or modify the request options. + * + * @param {DataManager} dm + * @param {Request} request + * @param {Fetch} settings? + * @returns void + */ + public beforeSend(dm: DataManager, request: Request, settings?: IFetch): void { + // need to extend this method + } +} + +/** + * Fetch Adaptor that is extended from URL Adaptor, is used for handle data operations with user defined functions. + * + * @hidden + */ +export class CustomDataAdaptor extends UrlAdaptor { + + protected getModuleName(): string { + return 'CustomDataAdaptor'; + } + + // options replaced the default adaptor options + protected options: RemoteOptions = extend({}, this.options, { + getData: () => { }, + addRecord: () => { }, + updateRecord: () => { }, + deleteRecord: () => { }, + batchUpdate: () => { } + }); + constructor(props?: RemoteOptions) { + super(); + extend(this.options, props || {}); + } +} + +/** + * The GraphqlAdaptor that is extended from URL Adaptor, is used for retrieving data from the Graphql server. + * It interacts with the Graphql server with all the DataManager Queries and performs CRUD operations. + * + * @hidden + */ +export class GraphQLAdaptor extends UrlAdaptor { + protected getModuleName(): string { + return 'GraphQLAdaptor'; + } + + private opt: GraphQLAdaptorOptions; + private schema: { result: string, count?: string, aggregates?: string }; + private query: string; + public getVariables: Function; + private getQuery: Function; + + constructor(options: GraphQLAdaptorOptions) { + super(); + this.opt = options; + this.schema = this.opt.response; + this.query = this.opt.query; + /* eslint-disable @typescript-eslint/no-empty-function */ + // tslint:disable-next-line:no-empty + this.getVariables = this.opt.getVariables ? this.opt.getVariables : () => { }; + /* eslint-enable @typescript-eslint/no-empty-function */ + this.getQuery = () => this.query; + } + + /** + * Process the JSON data based on the provided queries. + * + * @param {DataManager} dm + * @param {Query} query? + * @param datamanager + * @param query + */ + public processQuery(datamanager: DataManager, query: Query): Object { + const urlQuery: { data: string } = super.processQuery.apply(this, arguments); + const dm: { data: string } = JSON.parse(urlQuery.data); + + // constructing GraphQL parameters + const keys: string[] = ['skip', 'take', 'sorted', 'table', 'select', 'where', + 'search', 'requiresCounts', 'aggregates', 'params']; + const temp: GraphQLParams = {}; + const str: string = 'searchwhereparams'; + keys.filter((e: string) => { + temp[e] = str.indexOf(e) > -1 ? JSON.stringify(dm[e]) : dm[e]; + }); + + const vars: Object = this.getVariables() || {}; + // tslint:disable-next-line:no-string-literal + vars['datamanager'] = temp; + const data: string = JSON.stringify({ + query: this.getQuery(), + variables: vars + }); + urlQuery.data = data; + return urlQuery; + } + /** + * Returns the data from the query processing. + * It will also cache the data for later usage. + * + * @param {DataResult} data + * @param {DataManager} ds? + * @param {Query} query? + * @param {Request} xhr? + * @param {Object} request? + * @param resData + * @param ds + * @param query + * @param xhr + * @param request + * @returns DataResult + */ + public processResponse(resData: DataResult, ds?: DataManager, query?: Query, xhr?: Request, request?: Object): DataResult { + const res: { data: Object[] } = resData as { data: Object[] }; + let count: number; + let aggregates: Object; + + const result: Object | string = getValue(this.schema.result, res.data); + + if (this.schema.count) { + count = getValue(this.schema.count, res.data); + } + + if (this.schema.aggregates) { + aggregates = getValue(this.schema.aggregates, res.data); + aggregates = !isNullOrUndefined(aggregates) ? DataUtil.parse.parseJson(aggregates) : aggregates; + } + const pvt: PvtOptions = (request as { pvtData?: Object }).pvtData || {}; + const args: DataResult = { result: result, aggregates: aggregates }; + const data: DataResult = args; + if ( pvt && pvt.groups && pvt.groups.length) { + this.getAggregateResult(pvt, data, args, null, query); + } + return !isNullOrUndefined(count) ? { result: args.result, count: count, aggregates: aggregates } : args.result; + } + + /** + * Prepare and returns request body which is used to insert a new record in the table. + */ + + public insert(): {data: string} { + const inserted: { data: string } = super.insert.apply(this, arguments); + return this.generateCrudData(inserted, 'insert'); + } + + /** + * Prepare and returns request body which is used to update a new record in the table. + */ + public update(): {data: string} { + const inserted: { data: string } = super.update.apply(this, arguments); + return this.generateCrudData(inserted, 'update'); + } + + /** + * Prepare and returns request body which is used to remove a new record in the table. + */ + public remove(): {data: string} { + const inserted: { data: string } = super.remove.apply(this, arguments); + return this.generateCrudData(inserted, 'remove'); + } + + /** + * Prepare the request body based on the newly added, removed and updated records. + * The result is used by the batch request. + * + * @param {DataManager} dm + * @param {CrudOptions} changes + * @param {Object} e + * @param e.key + * @param {Query} query + * @param {Object} original + */ + public batchRequest(dm: DataManager, changes: CrudOptions, e: {key: string}, query: Query, original?: Object): Object { + const batch: { data: string } = super.batchRequest.apply(this, arguments); + // tslint:disable-next-line:typedef + const bData = JSON.parse(batch.data); + bData.key = e.key; + batch.data = JSON.stringify(bData); + return this.generateCrudData(batch, 'batch'); + } + + private generateCrudData(crudData: { data: string }, action: string): {data: string} { + const parsed: Object = JSON.parse(crudData.data); + crudData.data = JSON.stringify({ + query: this.opt.getMutation(action), + variables: parsed + }); + return crudData; + } +} + +/** + * Cache Adaptor is used to cache the data of the visited pages. It prevents new requests for the previously visited pages. + * You can configure cache page size and duration of caching by using cachingPageSize and timeTillExpiration properties of the DataManager + * + * @hidden + */ +export class CacheAdaptor extends UrlAdaptor { + private cacheAdaptor: CacheAdaptor; + private pageSize: number; + private guidId: string; + private isCrudAction: boolean = false; + private isInsertAction: boolean = false; + + /** + * Constructor for CacheAdaptor class. + * + * @param {CacheAdaptor} adaptor? + * @param {number} timeStamp? + * @param {number} pageSize? + * @param adaptor + * @param timeStamp + * @param pageSize + * @hidden + */ + constructor(adaptor?: CacheAdaptor, timeStamp?: number, pageSize?: number) { + super(); + if (!isNullOrUndefined(adaptor)) { + this.cacheAdaptor = adaptor; + } + this.pageSize = pageSize; + this.guidId = DataUtil.getGuid('cacheAdaptor'); + const obj: Object = { keys: [], results: [] }; + window.localStorage.setItem(this.guidId, JSON.stringify(obj)); + const guid: string = this.guidId; + if (!isNullOrUndefined(timeStamp)) { + setInterval( + () => { + const data: { + results: { timeStamp: number }[], + keys: string[] + } = DataUtil.parse.parseJson(window.localStorage.getItem(guid)); + const forDel: Object[] = []; + for (let i: number = 0; i < data.results.length; i++) { + const currentTime: number = +new Date(); + const requestTime: number = +new Date(data.results[i].timeStamp); + data.results[i].timeStamp = currentTime - requestTime; + if (currentTime - requestTime > timeStamp) { + forDel.push(i); + } + } + for (let i: number = 0; i < forDel.length; i++) { + data.results.splice((forDel[i]), 1); + data.keys.splice((forDel[i]), 1); + } + window.localStorage.removeItem(guid); + window.localStorage.setItem(guid, JSON.stringify(data)); + }, + timeStamp); + } + } + + /** + * It will generate the key based on the URL when we send a request to server. + * + * @param {string} url + * @param {Query} query? + * @param query + * @hidden + */ + public generateKey(url: string, query: Query): string { + const queries: Requests = this.getQueryRequest(query); + const singles: Object = Query.filterQueryLists(query.queries, ['onSelect', 'onPage', 'onSkip', 'onTake', 'onRange']); + let key: string = url; + const page: string = 'onPage'; + if (page in singles) { + key += singles[page].pageIndex; + } + queries.sorts.forEach((obj: QueryOptions) => { + key += obj.e.direction + obj.e.fieldName; + }); + queries.groups.forEach((obj: QueryOptions) => { + key += obj.e.fieldName; + }); + queries.searches.forEach((obj: QueryOptions) => { + key += obj.e.searchKey; + }); + + for (let filter: number = 0; filter < queries.filters.length; filter++) { + const currentFilter: QueryOptions = queries.filters[filter]; + if (currentFilter.e.isComplex) { + const newQuery: Query = query.clone(); + newQuery.queries = []; + for (let i: number = 0; i < currentFilter.e.predicates.length; i++) { + newQuery.queries.push({ fn: 'onWhere', e: currentFilter.e.predicates[i], filter: query.queries.filter }); + } + key += currentFilter.e.condition + this.generateKey(url, newQuery); + } else { + key += currentFilter.e.field + currentFilter.e.operator + currentFilter.e.value; + } + } + return key; + } + + /** + * Process the query to generate request body. + * If the data is already cached, it will return the cached data. + * + * @param {DataManager} dm + * @param {Query} query? + * @param {Object[]} hierarchyFilters? + * @param query + * @param hierarchyFilters + */ + public processQuery(dm: DataManager, query?: Query, hierarchyFilters?: Object[]): Object { + const key: string = this.generateKey(dm.dataSource.url, query); + const cachedItems: DataResult = DataUtil.parse.parseJson(window.localStorage.getItem(this.guidId)); + const data: DataResult = cachedItems ? cachedItems.results[cachedItems.keys.indexOf(key)] : null; + if (data != null && !this.isCrudAction && !this.isInsertAction) { + return data; + } + this.isCrudAction = null; this.isInsertAction = null; + /* eslint-disable prefer-spread */ + return this.cacheAdaptor.processQuery.apply(this.cacheAdaptor, [].slice.call(arguments, 0)); + /* eslint-enable prefer-spread */ + } + + /** + * Returns the data from the query processing. + * It will also cache the data for later usage. + * + * @param {DataResult} data + * @param {DataManager} ds? + * @param {Query} query? + * @param {Request} xhr? + * @param {Fetch} request? + * @param {CrudOptions} changes? + * @param ds + * @param query + * @param xhr + * @param request + * @param changes + */ + public processResponse( + data: DataResult, ds?: DataManager, query?: Query, xhr?: Request, request?: IFetch, changes?: CrudOptions): DataResult { + if (this.isInsertAction || (request && this.cacheAdaptor.options.batch && + DataUtil.endsWith(request.url, this.cacheAdaptor.options.batch) && request.type.toLowerCase() === 'post')) { + return this.cacheAdaptor.processResponse(data, ds, query, xhr, request, changes); + } + /* eslint-disable prefer-spread */ + data = this.cacheAdaptor.processResponse.apply(this.cacheAdaptor, [].slice.call(arguments, 0)); + /* eslint-enable prefer-spread */ + const key: string = query ? this.generateKey(ds.dataSource.url, query) : ds.dataSource.url; + let obj: DataResult = {}; + obj = DataUtil.parse.parseJson(window.localStorage.getItem(this.guidId)); + const index: number = obj.keys.indexOf(key); + if (index !== -1) { + (obj.results).splice(index, 1); + obj.keys.splice(index, 1); + } + obj.results[obj.keys.push(key) - 1] = { keys: key, result: data.result, timeStamp: new Date(), count: data.count }; + while ((obj.results).length > this.pageSize) { + (obj.results).splice(0, 1); + obj.keys.splice(0, 1); + } + window.localStorage.setItem(this.guidId, JSON.stringify(obj)); + return data; + } + + /** + * Method will trigger before send the request to server side. Used to set the custom header or modify the request options. + * + * @param {DataManager} dm + * @param {Request} request + * @param {Fetch} settings? + * @param settings + */ + public beforeSend(dm: DataManager, request: Request, settings?: IFetch): void { + if (!isNullOrUndefined(this.cacheAdaptor.options.batch) && DataUtil.endsWith(settings.url, this.cacheAdaptor.options.batch) + && settings.type.toLowerCase() === 'post') { + request.headers.set('Accept', this.cacheAdaptor.options.multipartAccept); + } + + if (!dm.dataSource.crossDomain) { + request.headers.set('Accept', this.cacheAdaptor.options.accept); + } + } + + /** + * Updates existing record and saves the changes to the table. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {Object} value + * @param {string} tableName + */ + public update(dm: DataManager, keyField: string, value: Object, tableName: string): Object { + this.isCrudAction = true; + return this.cacheAdaptor.update(dm, keyField, value, tableName); + } + + /** + * Prepare and returns request body which is used to insert a new record in the table. + * + * @param {DataManager} dm + * @param {Object} data + * @param {string} tableName? + * @param tableName + */ + public insert(dm: DataManager, data: Object, tableName?: string): Object { + this.isInsertAction = true; + return this.cacheAdaptor.insert(dm, data, tableName); + } + + /** + * Prepare and return request body which is used to remove record from the table. + * + * @param {DataManager} dm + * @param {string} keyField + * @param {Object} value + * @param {string} tableName? + * @param tableName + */ + public remove(dm: DataManager, keyField: string, value: Object, tableName?: string): Object[] { + this.isCrudAction = true; + return this.cacheAdaptor.remove(dm, keyField, value, tableName); + } + + /** + * Prepare the request body based on the newly added, removed and updated records. + * The result is used by the batch request. + * + * @param {DataManager} dm + * @param {CrudOptions} changes + * @param {RemoteArgs} e + */ + public batchRequest(dm: DataManager, changes: CrudOptions, e: RemoteArgs): CrudOptions { + return this.cacheAdaptor.batchRequest(dm, changes, e); + } +} + +/** + * @hidden + */ +export interface CrudOptions { + changedRecords?: Object[]; + addedRecords?: Object[]; + deletedRecords?: Object[]; + changed?: Object[]; + added?: Object[]; + deleted?: Object[]; + action?: string; + table?: string; + key?: string; +} + +/** + * @hidden + */ +export interface PvtOptions { + groups?: QueryOptions[]; + aggregates?: Aggregates[]; + search?: Object | Predicate; + changeSet?: number; + searches?: Object[]; + position?: number; +} + +/** + * @hidden + */ +export interface DataResult { + nodeType?: number; + addedRecords?: Object[]; + d?: DataResult | Object[]; + Count?: number; + count?: number; + result?: Object; + results?: Object[] | DataResult; + aggregate?: DataResult; + aggregates?: Aggregates; + value?: Object; + Items?: Object[] | DataResult; + keys?: string[]; + groupDs?: Object[]; +} + +/** + * @hidden + */ +export interface Requests { + sorts: QueryOptions[]; + groups: QueryOptions[]; + filters: QueryOptions[]; + searches: QueryOptions[]; + aggregates: QueryOptions[]; +} + +/** + * @hidden + */ +export interface RemoteArgs { + guid?: string; + url?: string; + key?: string; + cid?: number; + cSet?: string; +} + +/** + * @hidden + */ +export interface RemoteOptions { + from?: string; + requestType?: string; + sortBy?: string; + select?: string; + skip?: string; + group?: string; + take?: string; + search?: string; + count?: string; + where?: string; + aggregates?: string; + expand?: string; + accept?: string; + multipartAccept?: string; + batch?: string; + changeSet?: string; + batchPre?: string; + contentId?: string; + batchContent?: string; + changeSetContent?: string; + batchChangeSetContentType?: string; + updateType?: string; + localTime?: boolean; + apply?: string; + getData?: Function; + updateRecord?: Function; + addRecord?: Function; + deleteRecord?: Function; + batchUpdate?: Function; +} + +/** + * @hidden + */ +export interface GraphQLAdaptorOptions { + response: { result: string, count?: string, aggregates?: string}; + query: string; + getQuery?: () => string; + getVariables?: Function; + getMutation?: (action: string) => string; +} + +/** + * @hidden + */ +interface TempOptions { + pageIndex?: number; + pageSize?: number; + fn?: Function; +} + +/** + * @hidden + */ +export interface LazyLoad { + isLazyLoad: boolean; + onDemandGroupInfo: OnDemandGroupInfo; +} + +/** + * @hidden + */ +export interface OnDemandGroupInfo { + level: number; + skip: number; + take: number; + where: Predicate[]; +} + +/** + * @hidden + */ +export interface LazyLoadGroupArgs { + query: Query; + lazyLoad: LazyLoad; + result: Object[]; + group: Object[]; + sort: { comparer: (a: Object, b: Object) => number, fieldName: string }[]; + page: { pageIndex: number, pageSize: number }; +} + +/** + * @hidden + */ +export type ReturnType = { + result: Object[], + count?: number, + aggregates?: string +}; diff --git a/components/data/src/index.ts b/components/data/src/index.ts new file mode 100644 index 0000000..06ceac1 --- /dev/null +++ b/components/data/src/index.ts @@ -0,0 +1,7 @@ +/** + * Data modules + */ +export * from './manager'; +export * from './query'; +export * from './adaptors'; +export * from './util'; diff --git a/components/data/src/manager.ts b/components/data/src/manager.ts new file mode 100644 index 0000000..06b92db --- /dev/null +++ b/components/data/src/manager.ts @@ -0,0 +1,1004 @@ +/* eslint-disable valid-jsdoc */ +/* eslint-disable security/detect-object-injection */ +import { IFetch, Fetch } from '@syncfusion/react-base'; +import { extend, isNullOrUndefined } from '@syncfusion/react-base'; +import { DataUtil, Aggregates, Group } from './util'; +import { Predicate, Query, QueryOptions } from './query'; +import { ODataAdaptor, JsonAdaptor, CacheAdaptor, RemoteSaveAdaptor, RemoteOptions, CustomDataAdaptor, DataResult, Requests } from './adaptors'; +/** + * DataManager is used to manage and manipulate relational data. + */ +export class DataManager { + /** @hidden */ + public adaptor: AdaptorOptions; + /** @hidden */ + public defaultQuery: Query; + /** @hidden */ + public dataSource: DataOptions; + /** @hidden */ + public dateParse: boolean = true; + /** @hidden */ + public timeZoneHandling: boolean = true; + /** @hidden */ + public ready: Promise; + /** @hidden */ + public get moduleName(): string { return 'datamanager' }; + private isDataAvailable: boolean; + private persistQuery: object = {}; + private isInitialLoad: boolean = false; + private requests: IFetch[] = []; + private fetchDeffered: Deferred; + private fetchReqOption: IFetch; + private guidId: string; + private previousCacheQuery: string; + private isEnableCache: boolean = false; + private cacheQuery: string; + /** @hidden */ + public currentViewData: ReturnOption; + + /** + * Constructor for DataManager class + * + * @param {DataOptions|JSON[]} dataSource? + * @param {Query} query? + * @param {AdaptorOptions|string} adaptor? + * @param dataSource + * @param query + * @param adaptor + * @hidden + */ + constructor(dataSource?: DataOptions | JSON[] | Object[], query?: Query, adaptor?: AdaptorOptions | string) { + this.isInitialLoad = true; + this.isEnableCache = false; + if (!dataSource && !this.dataSource) { + dataSource = []; + } + adaptor = adaptor || (dataSource as DataOptions).adaptor; + if (dataSource && (dataSource as DataOptions).timeZoneHandling === false) { + this.timeZoneHandling = (dataSource as DataOptions).timeZoneHandling; + } + let data: DataOptions; + if (dataSource instanceof Array) { + data = { + json: dataSource, + offline: true + }; + } else if (typeof dataSource === 'object') { + if (!dataSource.json) { + dataSource.json = []; + } + if (!dataSource.enablePersistence) { + dataSource.enablePersistence = false; + } + if (!dataSource.id) { + dataSource.id = ''; + } + if (!dataSource.ignoreOnPersist) { + dataSource.ignoreOnPersist = []; + } + data = { + url: dataSource.url, + insertUrl: dataSource.insertUrl, + removeUrl: dataSource.removeUrl, + updateUrl: dataSource.updateUrl, + crudUrl: dataSource.crudUrl, + batchUrl: dataSource.batchUrl, + json: dataSource.json, + headers: dataSource.headers, + accept: dataSource.accept, + data: dataSource.data, + enableCache: dataSource.enableCache, + timeTillExpiration: dataSource.timeTillExpiration, + cachingPageSize: dataSource.cachingPageSize, + enableCaching: dataSource.enableCaching, + requestType: dataSource.requestType, + key: dataSource.key, + crossDomain: dataSource.crossDomain, + jsonp: dataSource.jsonp, + dataType: dataSource.dataType, + offline: dataSource.offline !== undefined ? dataSource.offline + : dataSource.adaptor instanceof RemoteSaveAdaptor || dataSource.adaptor instanceof CustomDataAdaptor ? + false : dataSource.url ? false : true, + requiresFormat: dataSource.requiresFormat, + enablePersistence: dataSource.enablePersistence, + id: dataSource.id, + ignoreOnPersist: dataSource.ignoreOnPersist + }; + } else { + DataUtil.throwError('DataManager: Invalid arguments'); + } + if (data.requiresFormat === undefined && !DataUtil.isCors()) { + data.requiresFormat = isNullOrUndefined(data.crossDomain) ? true : data.crossDomain; + } + if (data.dataType === undefined) { + data.dataType = 'json'; + } + this.isEnableCache = data.enableCache; + this.dataSource = data; + this.defaultQuery = query; + if (this.dataSource.enablePersistence && this.dataSource.id) { + window.addEventListener('unload', this.setPersistData.bind(this)); + } + + if (data.url && data.offline && !data.json.length) { + this.isDataAvailable = false; + this.adaptor = adaptor || new ODataAdaptor(); + this.dataSource.offline = false; + this.ready = this.executeQuery(query || new Query()); + this.ready.then((e: ReturnOption) => { + this.dataSource.offline = true; + this.isDataAvailable = true; + data.json = (e.result); + this.adaptor = new JsonAdaptor(); + }); + } else { + this.adaptor = data.offline ? new JsonAdaptor() : new ODataAdaptor(); + } + if (!data.jsonp && this.adaptor instanceof ODataAdaptor) { + data.jsonp = 'callback'; + } + this.adaptor = adaptor || this.adaptor; + if (this.isEnableCache) { + this.guidId = DataUtil.getGuid('cacheAdaptor'); + const obj: Object = { keys: [], results: [] }; + window.localStorage.setItem(this.guidId, JSON.stringify(obj)); + } + if (data.enableCaching) { + this.adaptor = new CacheAdaptor(this.adaptor, data.timeTillExpiration, data.cachingPageSize); + } + return this; + } + + /** + * Get the queries maintained in the persisted state. + * @param {string} id - The identifier of the persisted query to retrieve. + * @returns {object} The persisted data object. + */ + public getPersistedData(id?: string): object { + const persistedData: string = localStorage.getItem(id || this.dataSource.id); + return JSON.parse(persistedData); + } + + /** + * Set the queries to be maintained in the persisted state. + * @param {Event} e - The event parameter that triggers the setPersistData method. + * @param {string} id - The identifier of the persisted query to set. + * @param {object} persistData - The data to be persisted. + * @returns {void} . + */ + public setPersistData(e: Event, id?: string, persistData?:object): void { + localStorage.setItem(id || this.dataSource.id, JSON.stringify(persistData || this.persistQuery)); + } + + private setPersistQuery(query: Query): Query { + const persistedQuery: object = this.getPersistedData(); + if (this.isInitialLoad && persistedQuery && Object.keys(persistedQuery).length) { + this.persistQuery = persistedQuery; + (this.persistQuery as Query).queries = (this.persistQuery as Query).queries.filter((query: QueryOptions) => { + if (this.dataSource.ignoreOnPersist && this.dataSource.ignoreOnPersist.length) { + if (query.fn && this.dataSource.ignoreOnPersist.some((keyword: string) => query.fn === keyword)) { + return false; // Exclude the matching query + } + } + + if (query.fn === 'onWhere') { + const { e } = query; + if (e && e.isComplex && e.predicates instanceof Array) { + const allPredicates: Predicate[] = e.predicates.map((predicateObj: Predicate) => { + if (predicateObj.predicates && predicateObj.predicates instanceof Array) { + // Process nested predicate array + const nestedPredicates: Predicate[] = predicateObj.predicates.map((nestedPredicate: Predicate) => { + const { field, operator, value, ignoreCase, ignoreAccent, matchCase } = nestedPredicate; + return new Predicate(field, operator, value, ignoreCase, ignoreAccent, matchCase); + }); + return predicateObj.condition === 'and' ? Predicate.and(nestedPredicates) : Predicate.or(nestedPredicates); + } else { + // Process individual predicate + const { field, operator, value, ignoreCase, ignoreAccent, matchCase } = predicateObj; + return new Predicate(field, operator, value, ignoreCase, ignoreAccent, matchCase); + } + }); + query.e = new Predicate(allPredicates[0], e.condition, allPredicates.slice(1)); + } + } + return true; // Keep all other queries + }); + const newQuery: Query = extend(new Query(), this.persistQuery) as Query; + this.isInitialLoad = false; + return (newQuery); + } + else { + this.persistQuery = query; + this.isInitialLoad = false; + return query; + } + } + + /** + * Overrides DataManager's default query with given query. + * + * @param {Query} query - Defines the new default query. + */ + public setDefaultQuery(query: Query): DataManager { + this.defaultQuery = query; + return this; + } + + /** + * Executes the given query with local data source. + * + * @param {Query} query - Defines the query to retrieve data. + */ + public executeLocal(query?: Query): Object[] { + if (!this.defaultQuery && !(query instanceof Query)) { + DataUtil.throwError('DataManager - executeLocal() : A query is required to execute'); + } + + if (!this.dataSource.json) { + DataUtil.throwError('DataManager - executeLocal() : Json data is required to execute'); + } + + if (this.dataSource.enablePersistence && this.dataSource.id) { + query = this.setPersistQuery(query); + } + + query = query || this.defaultQuery; + + const result: ReturnOption = this.adaptor.processQuery(this, query); + + if (query.subQuery) { + const from: string = query.subQuery.fromTable; + const lookup: Object = query.subQuery.lookups; + const res: [{ [key: string]: Object[] }] = query.isCountRequired ? <[{ [key: string]: Object[] }]>result.result : + <[{ [key: string]: Object[] }]>result; + + if (lookup && lookup instanceof Array) { + DataUtil.buildHierarchy(query.subQuery.fKey, from, res as Group, lookup as Group, query.subQuery.key); + } + + for (let j: number = 0; j < res.length; j++) { + if (res[j][from] instanceof Array) { + res[j] = extend({}, {}, res[j]) as { [key: string]: Object[] }; + res[j][from] = this.adaptor.processResponse( + query.subQuery.using(new DataManager(res[j][from].slice(0) as JSON[])).executeLocal(), + this, query); + } + } + } + return this.adaptor.processResponse(result, this, query); + } + + /** + * Executes the given query with either local or remote data source. + * It will be executed as asynchronously and returns Promise object which will be resolved or rejected after action completed. + * + * @param {Query|Function} query - Defines the query to retrieve data. + * @param {Function} done - Defines the callback function and triggers when the Promise is resolved. + * @param {Function} fail - Defines the callback function and triggers when the Promise is rejected. + * @param {Function} always - Defines the callback function and triggers when the Promise is resolved or rejected. + */ + public executeQuery(query: Query | Function, done?: Function, fail?: Function, always?: Function): Promise { + const makeRequest: string = 'makeRequest'; + if (this.dataSource.enablePersistence && this.dataSource.id) { + query = this.setPersistQuery(query as Query); + } + if (typeof query === 'function') { + always = fail; + fail = done; + done = query; + query = null; + } + + if (!query) { + query = this.defaultQuery; + } + + if (!(query instanceof Query)) { + DataUtil.throwError('DataManager - executeQuery() : A query is required to execute'); + } + + const deffered: Deferred = new Deferred(); + let args: Object = { query: query }; + + if (!this.dataSource.offline && (this.dataSource.url !== undefined && this.dataSource.url !== '') + || (!isNullOrUndefined(this.adaptor[makeRequest])) || this.isCustomDataAdaptor(this.adaptor)) { + const result: ReturnOption = this.isEnableCache ? this.processQuery(query as Query) : this.adaptor.processQuery(this, query); + if (!isNullOrUndefined(this.adaptor[makeRequest])) { + this.adaptor[makeRequest](result, deffered, args, query); + } else if (!isNullOrUndefined(result.url) || this.isCustomDataAdaptor(this.adaptor)) { + this.requests = []; + this.makeRequest(result, deffered, args, query); + } else { + args = DataManager.getDeferedArgs(query, result as ReturnOption, args as ReturnOption); + deffered.resolve(args); + } + } else { + DataManager.nextTick( + () => { + const res: Object[] = this.executeLocal(query); + args = DataManager.getDeferedArgs(query, res as ReturnOption, args as ReturnOption); + deffered.resolve(args); + }); + } + if (done || fail) { + deffered.promise.then(<(value: Object) => Object> done, <(value: Object) => Object> fail); + } + if (always) { + deffered.promise.then(<(value: Object) => Object> always, <(value: Object) => Object> always); + } + + return deffered.promise as Promise; + } + + protected getQueryRequest(query: Query): Requests { + const req: Requests = { sorts: [], groups: [], filters: [], searches: [], aggregates: [] }; + req.sorts = Query.filterQueries(query.queries, 'onSortBy'); + req.groups = Query.filterQueries(query.queries, 'onGroup'); + req.filters = Query.filterQueries(query.queries, 'onWhere'); + req.searches = Query.filterQueries(query.queries, 'onSearch'); + req.aggregates = Query.filterQueries(query.queries, 'onAggregates'); + return req; + } + + + private generateKey(url: string, query: Query): string { + const queries: Requests = this.getQueryRequest(query); + const singles: Object = Query.filterQueryLists(query.queries, ['onSelect', 'onPage', 'onSkip', 'onTake', 'onRange']); + let key: string = url; + const page: string = 'onPage'; + queries.sorts.forEach((obj: QueryOptions) => { + key += obj.e.direction + obj.e.fieldName; + }); + queries.groups.forEach((obj: QueryOptions) => { + key += obj.e.fieldName; + }); + queries.searches.forEach((obj: QueryOptions) => { + key += obj.e.searchKey; + }); + + for (let filter: number = 0; filter < queries.filters.length; filter++) { + const currentFilter: QueryOptions = queries.filters[filter]; + if (currentFilter.e.isComplex) { + const newQuery: Query = query.clone(); + newQuery.queries = []; + for (let i: number = 0; i < currentFilter.e.predicates.length; i++) { + newQuery.queries.push({ fn: 'onWhere', e: currentFilter.e.predicates[i], filter: query.queries.filter }); + } + key += currentFilter.e.condition + this.generateKey(url, newQuery); + } else { + key += currentFilter.e.field + currentFilter.e.operator + currentFilter.e.value; + } + } + if (!isNullOrUndefined(this.previousCacheQuery) && this.previousCacheQuery !== key) { + const obj: Object = { keys: [], results: [] }; + window.localStorage.setItem(this.guidId, JSON.stringify(obj)); + } + this.previousCacheQuery = key; + if (page in singles) { + key += singles[page].pageIndex; + } + return key; + } + + private processQuery(query: Query): Object { + const key: string = this.generateKey(this.dataSource.url, query); + const cachedItems: DataResult = JSON.parse(window.localStorage.getItem(this.guidId)); + const data: DataResult = cachedItems ? cachedItems.results[cachedItems.keys.indexOf(key)] : null; + if (data != null) { + return DataUtil.parse.parseJson(data); + } + return this.adaptor.processQuery(this, query); + } + + private static getDeferedArgs(query: Query, result: ReturnOption, args?: ReturnOption): Object { + if (query.isCountRequired) { + args.result = result.result; + args.count = result.count; + args.aggregates = result.aggregates; + } else { + args.result = result; + } + return args; + } + private static nextTick(fn: Function): void { + /* eslint-disable @typescript-eslint/no-explicit-any */ + // tslint:disable-next-line:no-any + ((window).setImmediate || window.setTimeout)(fn, 0); + /* eslint-enable @typescript-eslint/no-explicit-any */ + } + private extendRequest(url: Object, fnSuccess: Function, fnFail: Function): Object { + return extend( + {}, { + type: 'GET', + dataType: this.dataSource.dataType, + crossDomain: this.dataSource.crossDomain, + jsonp: this.dataSource.jsonp, + cache: true, + processData: false, + onSuccess: fnSuccess, + onFailure: fnFail + }, + url); + } + // tslint:disable-next-line:max-func-body-length + private makeRequest(url: ReturnOption, deffered: Deferred, args?: RequestOptions, query?: Query): Object { + let isSelector: boolean = !!query.subQuerySelector; + const fnFail: Function = (e: string) => { + args.error = e; + deffered.reject(args); + }; + const process: Function = ( + data: Object, count: number, xhr: Request, request: IFetch, actual: Object, + aggregates: Aggregates, virtualSelectRecords?: Object) => { + args.xhr = xhr; + args.count = count ? parseInt(count.toString(), 10) : 0; + args.result = data; + args.request = request; + args.aggregates = aggregates; + args.actual = actual; + args.virtualSelectRecords = virtualSelectRecords; + deffered.resolve(args); + }; + const fnQueryChild: Function = (data: Object[], selector: Object) => { + const subDeffer: Deferred = new Deferred(); + const childArgs: Object = { parent: args }; + query.subQuery.isChild = true; + const subUrl: Object = this.adaptor.processQuery(this, query.subQuery, data ? this.adaptor.processResponse(data) : selector); + const childReq: Object = this.makeRequest(subUrl, subDeffer, childArgs, query.subQuery); + if (!isSelector) { + subDeffer.then( + (subData: { count: number, xhr: Request }) => { + if (data) { + DataUtil.buildHierarchy( + query.subQuery.fKey, query.subQuery.fromTable, data as Group, + subData as Group, query.subQuery.key); + process(data, subData.count, subData.xhr); + } + }, + fnFail); + } + return childReq; + }; + const fnSuccess: Function = (data: string | Object, request: IFetch) => { + if (this.isGraphQLAdaptor(this.adaptor)) { + // tslint:disable-next-line:no-string-literal + if (!isNullOrUndefined(data['errors'])) { + // tslint:disable-next-line:no-string-literal + return fnFail(data['errors'], request); + } + } + if (this.isCustomDataAdaptor(this.adaptor)) { + request = extend({}, this.fetchReqOption, request) as IFetch; + } + if (request.contentType.indexOf('xml') === -1 && this.dateParse && !this.isEnableCache) { + data = DataUtil.parse.parseJson(data); + } + let result: ReturnOption; + let promise: Promise = (>this.afterReponseRequest(data)); + promise.then((data: Object) => { + result = this.adaptor.processResponse(data, this, query, request.fetchRequest, request); + if (this.isEnableCache) { + /* eslint-enable prefer-spread */ + const key: string = query ? this.generateKey(this.dataSource.url, query) : this.dataSource.url; + let obj: DataResult = {}; + obj = JSON.parse(window.localStorage.getItem(this.guidId)); + const index: number = obj.keys.indexOf(key); + if (index !== -1) { + (obj.results).splice(index, 1); + obj.keys.splice(index, 1); + } + obj.results[obj.keys.push(key) - 1] = { keys: key, result: result.result, timeStamp: new Date(), count: result.count }; + window.localStorage.setItem(this.guidId, JSON.stringify(obj)); + } + if (request.contentType.indexOf('xml') === -1 && this.dateParse && this.isEnableCache) { + result = DataUtil.parse.parseJson(result); + } + let count: number = 0; + let aggregates: Aggregates = null; + const virtualSelectRecords: string = 'virtualSelectRecords'; + const virtualRecords: { virtualSelectRecords: Object } = + (<{ [key: string]: { virtualSelectRecords: Object } }>data)[virtualSelectRecords]; + if (query.isCountRequired) { + count = result.count; + aggregates = result.aggregates; + result = result.result; + } + if (!query.subQuery) { + process(result, count, request.fetchRequest, request.type, data, aggregates, virtualRecords); + return; + } + if (!isSelector) { + fnQueryChild(result, request); + }; + }).catch((e: Error) => this.dataManagerFailure(e, deffered, args)); + }; + let req: Object = this.extendRequest(url, fnSuccess, fnFail); + if (!this.isCustomDataAdaptor(this.adaptor)) { + let promise: Promise = (>this.useMiddleware(req)); + let fetch: IFetch; + promise.then((response: any) => { + fetch = Fetch(req); + fetch.beforeSend = () => { + this.beforeSend(fetch.fetchRequest, fetch, response); + }; + req = fetch.send(); + (>req).catch((e: Error) => true); + this.requests.push(fetch); + }).catch((e: Error) => this.dataManagerFailure(e, deffered, args)); + } else { + this.fetchReqOption = req as IFetch; + const request: FetchOption = req; + (<{ options?: RemoteOptions }>this.adaptor).options.getData({ + data: request.data, + onSuccess: request.onSuccess, onFailure: request.onFailure + }); + } + if (isSelector) { + let promise: Promise; + const res: Object[] = query.subQuerySelector.call(this, { query: query.subQuery, parent: query }); + if (res && res.length) { + promise = Promise.all([req, fnQueryChild(null, res)]); + promise.then((...args: Object[][]) => { + const result: Object[] = args[0]; + let pResult: ReturnOption = this.adaptor.processResponse( + result[0], this, query, this.requests[0].fetchRequest, this.requests[0]); + let count: number = 0; + if (query.isCountRequired) { + count = pResult.count; + pResult = pResult.result; + } + let cResult: ReturnOption = this.adaptor.processResponse( + result[1], this, query.subQuery, this.requests[1].fetchRequest, this.requests[1]); + count = 0; + if (query.subQuery.isCountRequired) { + count = cResult.count; + cResult = cResult.result; + } + DataUtil.buildHierarchy(query.subQuery.fKey, query.subQuery.fromTable, pResult, cResult, query.subQuery.key); + isSelector = false; + process(pResult, count, this.requests[0].fetchRequest); + }); + } else { + isSelector = false; + } + } + return req; + } + + /** + * @param {Error} e - specifies the string + * @param {Deferred} deffered - specifies the deffered + * @param {RequestOptions} args - specifies the RequestOptions + * @hidden + */ + public dataManagerFailure(e: Error, deffered: Deferred, args?: RequestOptions): void { + args.error = e; + deffered.reject(args); + } + + + + private async afterReponseRequest(data: string | Object): Promise { + const reponse: Object = await this.applyPostRequestMiddlewares(data); + const deffered: Deferred = new Deferred(); + deffered.resolve(reponse); + return deffered.promise as Promise; + } + + /** + * Processes the middleware stack after receiving the response. + * @param {Response} response - The response object. + * @returns {Response} - The potentially modified response. + */ + + public async applyPostRequestMiddlewares(response: string | Object): Promise { + return response; + } + + /** + * Registers a new middleware in the DataManager. + * @param {Middleware} middleware - The middleware instance to register. + * @returns {void} + */ + public async useMiddleware(request: Object): Promise { + const reponse: Object = await this.applyPreRequestMiddlewares(request); + const deffered: Deferred = new Deferred(); + deffered.resolve(reponse); + return deffered.promise as Promise; + } + + /** + * Processes the middleware stack before sending the request. + * @param {Request} request - The request object. + * @returns {Request} - The potentially modified request. + */ + public async applyPreRequestMiddlewares(request: Object): Promise { + return request; + } + + + private beforeSend(request: Request, settings?: IFetch, response?: Object): void { + this.adaptor.beforeSend(this, request, settings); + let headers: Object[] = []; + if (this.dataSource.headers) { + headers = headers.concat(this.dataSource.headers); + } + let props: Object[]; + if (response && (<{ headers: Object[] }>response).headers) { + headers = headers.concat((<{ headers: Object[] }>response).headers); + } + for (let i: number = 0; headers && i < headers.length; i++) { + props = []; + const keys: string[] = Object.keys(headers[i]); + for (const prop of keys) { + props.push(prop); + request.headers.set(prop, (<{ [key: string]: string }>headers[i])[prop]); + } + } + } + + /** + * Save bulk changes to the given table name. + * User can add a new record, edit an existing record, and delete a record at the same time. + * If the datasource from remote, then updated in a single post. + * + * @param {Object} changes - Defines the CrudOptions. + * @param {string} key - Defines the column field. + * @param {string|Query} tableName - Defines the table name. + * @param {Query} query - Sets default query for the DataManager. + * @param original + */ + public saveChanges( + changes: Object, key?: string, tableName?: string | Query, query?: Query, original?: Object): Promise | Object { + + if (tableName instanceof Query) { + query = tableName; + tableName = null; + } + + const args: Object = { + url: tableName, + key: key || this.dataSource.key + }; + + const req: Object = this.adaptor.batchRequest(this, changes, args, query || new Query(), original); + + const dofetchRequest: string = 'dofetchRequest'; + + if (this.dataSource.offline) { + return req; + } + + if (!isNullOrUndefined(this.adaptor[dofetchRequest])) { + return this.adaptor[dofetchRequest](req); + } else if (!this.isCustomDataAdaptor(this.adaptor)) { + const deff: Deferred = new Deferred(); + const fetch: IFetch = Fetch(req); + fetch.beforeSend = () => { + this.beforeSend(fetch.fetchRequest, fetch); + }; + fetch.onSuccess = (data: string | Object, request: IFetch) => { + if (this.isGraphQLAdaptor(this.adaptor)) { + // tslint:disable-next-line:no-string-literal + if (!isNullOrUndefined(data['errors'])) { + // tslint:disable-next-line:no-string-literal + fetch.onFailure(new Error(JSON.stringify(data['errors']))); + } + } + deff.resolve(this.adaptor.processResponse( + data, this, null, request.fetchRequest, request, changes, args)); + }; + fetch.onFailure = (e: Error) => { + deff.reject([{ error: e.message }]); + };; + (>fetch.send()).catch((e: Error) => true); // to handle the failure requests. + return deff.promise; + } else { + return this.dofetchRequest(req, (<{ options?: RemoteOptions }>this.adaptor).options.batchUpdate, changes); + } + } + + /** + * Inserts new record in the given table. + * + * @param {Object} data - Defines the data to insert. + * @param {string|Query} tableName - Defines the table name. + * @param {Query} query - Sets default query for the DataManager. + * @param position + */ + public insert(data: Object, tableName?: string | Query, query?: Query, position?: number): Object | Promise { + if (tableName instanceof Query) { + query = tableName; + tableName = null; + } + + const req: Object = this.adaptor.insert(this, data, tableName, query, position); + + const dofetchRequest: string = 'dofetchRequest'; + + if (this.dataSource.offline) { + return req; + } + + if (!isNullOrUndefined(this.adaptor[dofetchRequest])) { + return this.adaptor[dofetchRequest](req); + } else { + return this.dofetchRequest(req, (<{ options?: RemoteOptions }>this.adaptor).options.addRecord); + } + } + + /** + * Removes data from the table with the given key. + * + * @param {string} keyField - Defines the column field. + * @param {Object} value - Defines the value to find the data in the specified column. + * @param {string|Query} tableName - Defines the table name + * @param {Query} query - Sets default query for the DataManager. + */ + public remove(keyField: string, value: Object, tableName?: string | Query, query?: Query): Object | Promise { + if (typeof value === 'object') { + value = DataUtil.getObject(keyField, value); + } + + if (tableName instanceof Query) { + query = tableName; + tableName = null; + } + + const res: Object = this.adaptor.remove(this, keyField, value, tableName, query); + + const dofetchRequest: string = 'dofetchRequest'; + + if (this.dataSource.offline) { + return res; + } + + if (!isNullOrUndefined(this.adaptor[dofetchRequest])) { + return this.adaptor[dofetchRequest](res); + } else { + const remove: Function = (<{ options?: RemoteOptions }>this.adaptor).options.deleteRecord; + return this.dofetchRequest(res, remove); + } + } + /** + * Updates existing record in the given table. + * + * @param {string} keyField - Defines the column field. + * @param {Object} value - Defines the value to find the data in the specified column. + * @param {string|Query} tableName - Defines the table name + * @param {Query} query - Sets default query for the DataManager. + * @param original + */ + public update(keyField: string, value: Object, tableName?: string | Query, query?: Query, original?: Object): Object | Promise { + + if (tableName instanceof Query) { + query = tableName; + tableName = null; + } + + if (this.isEnableCache) { + this.cacheQuery = this.generateKey(this.dataSource.url, query); + } + + const res: Object = this.adaptor.update(this, keyField, value, tableName, query, original); + + const dofetchRequest: string = 'dofetchRequest'; + + if (this.dataSource.offline) { + return res; + } + + if (!isNullOrUndefined(this.adaptor[dofetchRequest])) { + return this.adaptor[dofetchRequest](res); + } else { + const update: Function = (<{ options?: RemoteOptions }>this.adaptor).options.updateRecord; + return this.dofetchRequest(res, update); + } + } + + private isCustomDataAdaptor(dataSource: AdaptorOptions): boolean { + return (<{ getModuleName?: Function }>this.adaptor).getModuleName && + (<{ getModuleName?: Function }>this.adaptor).getModuleName() === 'CustomDataAdaptor'; + } + + private isGraphQLAdaptor(dataSource: AdaptorOptions): boolean { + return (<{ getModuleName?: Function }>this.adaptor).getModuleName && + (<{ getModuleName?: Function }>this.adaptor).getModuleName() === 'GraphQLAdaptor'; + } + + private successFunc(record: string | Object, request: IFetch, changes?: Object): void { + if (this.isGraphQLAdaptor(this.adaptor)) { + const data: Object = typeof record === 'object' ? record : JSON.parse(record as string); + // tslint:disable-next-line:no-string-literal + if (!isNullOrUndefined(data['errors'])) { + // tslint:disable-next-line:no-string-literal + this.failureFunc(JSON.stringify(data['errors'])); + } + } + if (this.isCustomDataAdaptor(this.adaptor)) { + request = extend({}, this.fetchReqOption, request) as IFetch; + } + try { + DataUtil.parse.parseJson(record); + } catch (e) { + record = []; + } + + if (this.isEnableCache) { + const requests: { action: string, keyColumn: string, key: number | string, value: object } = JSON.parse((<{ data: string }>request).data); + if (requests.action === 'insert' || requests.action === 'remove') { + const obj: Object = { keys: [], results: [] }; + window.localStorage.setItem(this.guidId, JSON.stringify(obj)); + } else if (requests.action === 'update') { + const cachedItems: DataResult = JSON.parse(window.localStorage.getItem(this.guidId)); + const data: DataResult = cachedItems ? cachedItems.results[cachedItems.keys.indexOf(this.cacheQuery)] : null; + if (data && data.result) { + let cacheData: Object[] = <[{ [key: string]: Object[] }]>data.result; + for (let i: number = 0; i < cacheData.length; i++) { + if (cacheData[i][requests.keyColumn] === requests.key) { + cacheData[i] = requests.value; + window.localStorage.setItem(this.guidId, JSON.stringify(cachedItems)); + break; + } + } + } + } + } + + record = this.adaptor.processResponse(DataUtil.parse.parseJson(record), this, null, request.fetchRequest, request, changes); + this.fetchDeffered.resolve(record); + } + + private failureFunc (e: string): void { + if (this.isEnableCache) { + this.cacheQuery = ''; + } + this.fetchDeffered.reject([{ error: e }]); + } + + private dofetchRequest(res: Object, fetchFunc?: Function, changes?: Object): Promise { + + res = extend( + {}, { + type: 'POST', + contentType: 'application/json; charset=utf-8', + processData: false + }, + res); + this.fetchDeffered = new Deferred(); + + if (!this.isCustomDataAdaptor(this.adaptor)) { + const fetch: IFetch = Fetch(res); + + fetch.beforeSend = () => { + this.beforeSend(fetch.fetchRequest, fetch); + }; + fetch.onSuccess = this.successFunc.bind(this); + fetch.onFailure = this.failureFunc.bind(this); + (>fetch.send()).catch((e: Error) => true); // to handle the failure requests. + } else { + this.fetchReqOption = res as IFetch; + fetchFunc.call(this, { + data: (res as FetchOption).data, onSuccess: this.successFunc.bind(this), + onFailure: this.failureFunc.bind(this), + changes: changes + }); + } + return this.fetchDeffered.promise as Promise; + } + + public clearPersistence(): void { + window.removeEventListener('unload', this.setPersistData.bind(this)); + this.dataSource.enablePersistence = false; + this.persistQuery = {}; + window.localStorage.setItem(this.dataSource.id, '[]'); + } +} + +/** + * Deferred is used to handle asynchronous operation. + */ +export class Deferred { + /** + * Resolve a Deferred object and call doneCallbacks with the given args. + */ + public resolve: Function; + /** + * Reject a Deferred object and call failCallbacks with the given args. + */ + public reject: Function; + /** + * Promise is an object that represents a value that may not be available yet, but will be resolved at some point in the future. + */ + public promise: Promise = new Promise((resolve: Function, reject: Function) => { + this.resolve = resolve; + this.reject = reject; + }); + /** + * Defines the callback function triggers when the Deferred object is resolved. + */ + public then: Function = this.promise.then.bind(this.promise); + /** + * Defines the callback function triggers when the Deferred object is rejected. + */ + public catch: Function = this.promise.catch.bind(this.promise); +} + +/** + * @hidden + */ +export interface DataOptions { + url?: string; + adaptor?: AdaptorOptions; + insertUrl?: string; + removeUrl?: string; + updateUrl?: string; + crudUrl?: string; + batchUrl?: string; + json?: Object[]; + headers?: Object[]; + accept?: boolean; + data?: JSON; + enableCache?: boolean; + timeTillExpiration?: number; + cachingPageSize?: number; + enableCaching?: boolean; + requestType?: string; + key?: string; + crossDomain?: boolean; + jsonp?: string; + dataType?: string; + offline?: boolean; + requiresFormat?: boolean; + timeZoneHandling?: boolean; + id?: string; + enablePersistence?: boolean; + ignoreOnPersist?: string[]; +} + +/** + * @hidden + */ +export interface ReturnOption { + result?: ReturnOption; + count?: number; + url?: string; + aggregates?: Aggregates; +} + +/** + * @hidden + */ +export interface FetchOption { + onSuccess?: Function; + onFailure?: Function; + data?: string; +} + +/** + * @hidden + */ +export interface RequestOptions { + xhr?: Request; + count?: number; + result?: ReturnOption; + request?: IFetch; + aggregates?: Aggregates; + actual?: Object; + virtualSelectRecords?: Object; + error?: string | Error; +} + +/** + * @hidden + */ +export interface AdaptorOptions { + processQuery?: Function; + processResponse?: Function; + beforeSend?: Function; + batchRequest?: Function; + insert?: Function; + remove?: Function; + update?: Function; + key?: string; +} diff --git a/components/data/src/query.ts b/components/data/src/query.ts new file mode 100644 index 0000000..024259e --- /dev/null +++ b/components/data/src/query.ts @@ -0,0 +1,865 @@ +/* eslint-disable valid-jsdoc */ +/* eslint-disable security/detect-object-injection */ +import { DataUtil } from './util'; +import { DataManager } from './manager'; +import { NumberFormatOptions, DateFormatOptions } from '@syncfusion/react-base'; +import { isNullOrUndefined } from '@syncfusion/react-base'; +/** + * Query class is used to build query which is used by the DataManager to communicate with datasource. + */ +export class Query { + /** @hidden */ + public queries: QueryOptions[]; + /** @hidden */ + public key: string; + /** @hidden */ + public fKey: string; + /** @hidden */ + public fromTable: string; + /** @hidden */ + public lookups: string[]; + /** @hidden */ + public expands: Object[]; + /** @hidden */ + public sortedColumns: Object[]; + /** @hidden */ + public groupedColumns: Object[]; + /** @hidden */ + public subQuerySelector: Function; + /** @hidden */ + public subQuery: Query = null; + /** @hidden */ + public isChild: boolean = false; + /** @hidden */ + public params: ParamOption[]; + /** @hidden */ + public lazyLoad: { key: string, value: object | boolean }[]; + /** @hidden */ + public isCountRequired: boolean; + /** @hidden */ + public dataManager: DataManager; + /** @hidden */ + public distincts: string[] = []; + /** @hidden */ + public get moduleName(): string { return 'query' }; + + /** + * Constructor for Query class. + * + * @param {string|string[]} from? + * @param from + * @hidden + */ + constructor(from?: string | string[]) { + this.queries = []; + this.key = ''; + this.fKey = ''; + if (typeof from === 'string') { + this.fromTable = from; + } else if (from && from instanceof Array) { + this.lookups = from; + } + this.expands = []; + this.sortedColumns = []; + this.groupedColumns = []; + this.subQuery = null; + this.isChild = false; + this.params = []; + this.lazyLoad = []; + return this; + } + + /** + * Sets the primary key. + * + * @param {string} field - Defines the column field. + */ + public setKey(field: string): Query { + this.key = field; + return this; + } + + /** + * Sets default DataManager to execute query. + * + * @param {DataManager} dataManager - Defines the DataManager. + */ + public using(dataManager: DataManager): Query { + this.dataManager = dataManager; + return this; + } + + /** + * Executes query with the given DataManager. + * + * @param {DataManager} dataManager - Defines the DataManager. + * @param {Function} done - Defines the success callback. + * @param {Function} fail - Defines the failure callback. + * @param {Function} always - Defines the callback which will be invoked on either success or failure. + * + *
+     * let dataManager: DataManager = new DataManager([{ ID: '10' }, { ID: '2' }, { ID: '1' }, { ID: '20' }]);
+     * let query: Query = new Query();
+     * query.sortBy('ID', (x: string, y: string): number => { return parseInt(x, 10) - parseInt(y, 10) });
+     * let promise: Promise< Object > = query.execute(dataManager);
+     * promise.then((e: { result: Object }) => { });
+     * 
+ */ + public execute(dataManager?: DataManager, done?: Function, fail?: Function, always?: Function): Promise { + dataManager = dataManager || this.dataManager; + + if (dataManager) { + return dataManager.executeQuery(this, done, fail, always); + } + + return DataUtil.throwError( + 'Query - execute() : dataManager needs to be is set using "using" function or should be passed as argument' + ); + } + + /** + * Executes query with the local datasource. + * + * @param {DataManager} dataManager - Defines the DataManager. + */ + public executeLocal(dataManager?: DataManager): Object[] { + dataManager = dataManager || this.dataManager; + + if (dataManager) { + return dataManager.executeLocal(this); + } + + return DataUtil.throwError( + 'Query - executeLocal() : dataManager needs to be is set using "using" function or should be passed as argument' + ); + } + + /** + * Creates deep copy of the Query object. + */ + public clone(): Query { + const cloned: Query = new Query(); + cloned.queries = this.queries.slice(0); + cloned.key = this.key; + cloned.isChild = this.isChild; + cloned.dataManager = this.dataManager; + cloned.fromTable = this.fromTable; + cloned.params = this.params.slice(0); + cloned.expands = this.expands.slice(0); + cloned.sortedColumns = this.sortedColumns.slice(0); + cloned.groupedColumns = this.groupedColumns.slice(0); + cloned.subQuerySelector = this.subQuerySelector; + cloned.subQuery = this.subQuery; + cloned.fKey = this.fKey; + cloned.isCountRequired = this.isCountRequired; + cloned.distincts = this.distincts.slice(0); + cloned.lazyLoad = this.lazyLoad.slice(0); + return cloned; + } + + /** + * Specifies the name of table to retrieve data in query execution. + * + * @param {string} tableName - Defines the table name. + */ + public from(tableName: string): Query { + this.fromTable = tableName; + return this; + } + + /** + * Adds additional parameter which will be sent along with the request which will be generated while DataManager execute. + * + * @param {string} key - Defines the key of additional parameter. + * @param {Function|string} value - Defines the value for the key. + */ + public addParams(key: string, value: Function | string | null ): Query { + if (typeof value === 'function') { + this.params.push({ key: key, fn: value }); + } else { + this.params.push({ key: key, value: value }); + } + return this; + } + + /** + * @param fields + * @hidden + */ + public distinct(fields: string | string[]): Query { + if (typeof fields === 'string') { + this.distincts = [].slice.call([fields], 0); + } else { + this.distincts = fields.slice(0); + } + return this; + } + + /** + * Expands the related table. + * + * @param {string|Object[]} tables + */ + public expand(tables: string | Object[]): Query { + if (typeof tables === 'string') { + this.expands = [].slice.call([tables], 0); + } else { + this.expands = tables.slice(0); + } + return this; + } + + /** + * Filter data with given filter criteria. + * + * @param {string|Predicate} fieldName - Defines the column field or Predicate. + * @param {string} operator - Defines the operator how to filter data. + * @param {string|number|boolean} value - Defines the values to match with data. + * @param {boolean} ignoreCase - If ignore case set to false, then filter data with exact match or else + * filter data with case insensitive. + * @param ignoreAccent + * @param matchCase + */ + public where( + fieldName: string | Predicate | Predicate[], operator?: string, + value?: string | Date | number | boolean | null, ignoreCase?: boolean, ignoreAccent?: boolean, matchCase?: boolean): Query { + + operator = operator ? (operator).toLowerCase() : null; + let predicate: Predicate | QueryOptions = null; + if (typeof fieldName === 'string') { + predicate = new Predicate(fieldName, operator, value, ignoreCase, ignoreAccent, matchCase); + } else if (fieldName instanceof Predicate) { + predicate = fieldName; + } + this.queries.push({ + fn: 'onWhere', + e: predicate + }); + return this; + } + + /** + * Search data with given search criteria. + * + * @param {string|number|boolean} searchKey - Defines the search key. + * @param {string|string[]} fieldNames - Defines the collection of column fields. + * @param {string} operator - Defines the operator how to search data. + * @param {boolean} ignoreCase - If ignore case set to false, then filter data with exact match or else + * filter data with case insensitive. + * @param ignoreAccent + */ + public search( + searchKey: string | number | boolean, fieldNames?: string | string[], operator?: string, ignoreCase?: boolean, + ignoreAccent?: boolean): Query { + if (typeof fieldNames === 'string') { + fieldNames = [(fieldNames as string)]; + } + if (!operator || operator === 'none') { + operator = 'contains'; + } + const comparer: Function = (<{ [key: string]: Function }>DataUtil.fnOperators)[operator]; + this.queries.push({ + fn: 'onSearch', + e: { + fieldNames: fieldNames, + operator: operator, + searchKey: searchKey, + ignoreCase: ignoreCase, + ignoreAccent: ignoreAccent, + comparer: comparer + } + }); + return this; + } + + /** + * Sort the data with given sort criteria. + * By default, sort direction is ascending. + * + * @param {string|string[]} fieldName - Defines the single or collection of column fields. + * @param {string|Function} comparer - Defines the sort direction or custom sort comparer function. + * @param isFromGroup + */ + public sortBy(fieldName: string | string[], comparer?: string | Function, isFromGroup?: boolean): Query { + return this.sortByForeignKey(fieldName, comparer, isFromGroup); + } + + /** + * Sort the data with given sort criteria. + * By default, sort direction is ascending. + * + * @param {string|string[]} fieldName - Defines the single or collection of column fields. + * @param {string|Function} comparer - Defines the sort direction or custom sort comparer function. + * @param isFromGroup + * @param {string} direction - Defines the sort direction . + */ + public sortByForeignKey(fieldName: string | string[], comparer?: string | Function, isFromGroup?: boolean, direction?: string): Query { + let order: string = !isNullOrUndefined(direction) ? direction : 'ascending'; + let sorts: Object[]; + let temp: string | string[]; + + if (typeof fieldName === 'string' && DataUtil.endsWith((fieldName as string).toLowerCase(), ' desc')) { + fieldName = (fieldName as string).replace(/ desc$/i, ''); + comparer = 'descending'; + } + + if (!comparer || typeof comparer === 'string') { + order = comparer ? (comparer).toLowerCase() : 'ascending'; + comparer = DataUtil.fnSort(comparer); + } + + if (isFromGroup) { + sorts = Query.filterQueries(this.queries, 'onSortBy'); + + for (let i: number = 0; i < sorts.length; i++) { + temp = (sorts[i]).e.fieldName; + if (typeof temp === 'string') { + if (temp === fieldName) { return this; } + } else if (temp instanceof Array) { + for (let j: number = 0; j < (temp as string[]).length; j++) { + if (temp[j] === fieldName || (fieldName as string).toLowerCase() === temp[j] + ' desc') { + return this; + } + } + } + } + } + + this.queries.push({ + fn: 'onSortBy', + e: { + fieldName: (fieldName), + comparer: comparer, + direction: order + } + }); + + return this; + } + + /** + * Sorts data in descending order. + * + * @param {string} fieldName - Defines the column field. + */ + public sortByDesc(fieldName: string): Query { + return this.sortBy(fieldName, 'descending'); + } + + /** + * Groups data with the given field name. + * + * @param {string} fieldName - Defines the column field. + * @param fn + * @param format + */ + public group(fieldName: string, fn?: Function, format?: string | NumberFormatOptions | DateFormatOptions): Query { + this.sortBy(fieldName, null, true); + this.queries.push({ + fn: 'onGroup', + e: { + fieldName: fieldName, + comparer: fn ? fn : null, + format: format ? format : null + } + }); + return this; + } + + /** + * Gets data based on the given page index and size. + * + * @param {number} pageIndex - Defines the current page index. + * @param {number} pageSize - Defines the no of records per page. + */ + public page(pageIndex: number, pageSize: number): Query { + this.queries.push({ + fn: 'onPage', + e: { + pageIndex: pageIndex, + pageSize: pageSize + } + }); + return this; + } + + /** + * Gets data based on the given start and end index. + * + * @param {number} start - Defines the start index of the datasource. + * @param {number} end - Defines the end index of the datasource. + */ + public range(start: number, end: number): Query { + this.queries.push({ + fn: 'onRange', + e: { + start: start, + end: end + } + }); + return this; + } + + /** + * Gets data from the top of the data source based on given number of records count. + * + * @param {number} nos - Defines the no of records to retrieve from datasource. + */ + public take(nos: number): Query { + this.queries.push({ + fn: 'onTake', + e: { + nos: nos + } + }); + return this; + } + + /** + * Skips data with given number of records count from the top of the data source. + * + * @param {number} nos - Defines the no of records skip in the datasource. + */ + public skip(nos: number): Query { + this.queries.push({ + fn: 'onSkip', + e: { nos: nos } + }); + return this; + } + + /** + * Selects specified columns from the data source. + * + * @param {string|string[]} fieldNames - Defines the collection of column fields. + */ + public select(fieldNames: string | string[]): Query { + if (typeof fieldNames === 'string') { + fieldNames = [].slice.call([fieldNames], 0); + } + this.queries.push({ + fn: 'onSelect', + e: { fieldNames: fieldNames } + }); + return this; + } + + /** + * Gets the records in hierarchical relationship from two tables. It requires the foreign key to relate two tables. + * + * @param {Query} query - Defines the query to relate two tables. + * @param {Function} selectorFn - Defines the custom function to select records. + */ + public hierarchy(query: Query, selectorFn: Function): Query { + this.subQuerySelector = selectorFn; + this.subQuery = query; + return this; + } + + /** + * Sets the foreign key which is used to get data from the related table. + * + * @param {string} key - Defines the foreign key. + */ + public foreignKey(key: string): Query { + this.fKey = key; + return this; + } + + /** + * It is used to get total number of records in the DataManager execution result. + */ + public requiresCount(): Query { + this.isCountRequired = true; + return this; + } + + //type - sum, avg, min, max + /** + * Aggregate the data with given type and field name. + * + * @param {string} type - Defines the aggregate type. + * @param {string} field - Defines the column field to aggregate. + */ + public aggregate(type: string, field: string): Query { + this.queries.push({ + fn: 'onAggregates', + e: { field: field, type: type } + }); + return this; + } + + /** + * Pass array of filterColumn query for performing filter operation. + * + * @param {QueryOptions[]} queries + * @param {string} name + * @hidden + */ + public static filterQueries(queries: QueryOptions[], name: string): QueryOptions[] { + return queries.filter((q: QueryOptions): boolean => { + return q.fn === name; + }); + } + /** + * To get the list of queries which is already filtered in current data source. + * + * @param {Object[]} queries + * @param {string[]} singles + * @hidden + */ + public static filterQueryLists(queries: Object[], singles: string[]): Object { + const filtered: QueryOptions[] = queries.filter((q: QueryOptions) => { + return singles.indexOf(q.fn) !== -1; + }); + const res: { [key: string]: Object } = {}; + for (let i: number = 0; i < filtered.length; i++) { + if (!res[filtered[i].fn]) { + res[filtered[i].fn] = filtered[i].e; + } + } + return res; + } +} + +/** + * Predicate class is used to generate complex filter criteria. + * This will be used by DataManager to perform multiple filtering operation. + */ +export class Predicate { + /** @hidden */ + public field: string; + /** @hidden */ + public operator: string; + /** @hidden */ + public value: string | number | Date | boolean | Predicate | Predicate[] | (string | number | boolean | Date)[] | null; + /** @hidden */ + public condition: string; + /** @hidden */ + public ignoreCase: boolean; + /** @hidden */ + public matchCase: boolean; + /** @hidden */ + public ignoreAccent: boolean = false; + /** @hidden */ + public isComplex: boolean = false; + /** @hidden */ + public predicates: Predicate[]; + /** @hidden */ + public comparer: Function; + [x: string]: string | number | Date | boolean | Predicate | Predicate[] | Function | (string | number | boolean | Date)[] | null; + + /** + * Constructor for Predicate class. + * + * @param {string|Predicate} field + * @param {string} operator + * @param {string | number | Date | boolean | Predicate | Predicate[] | (string | number | boolean | Date)[] | null} value + * @param {boolean=false} ignoreCase + * @param ignoreAccent + * @param {boolean} matchCase + * @hidden + */ + constructor( + field: string | Predicate, operator: string, value: string | number | Date | boolean | Predicate | Predicate[] | (string | number | boolean | Date)[] | null, + ignoreCase: boolean = false, ignoreAccent?: boolean, matchCase?: boolean) { + if (typeof field === 'string') { + this.field = field; + this.operator = operator.toLowerCase(); + this.value = value; + this.matchCase = matchCase; + this.ignoreCase = ignoreCase; + this.ignoreAccent = ignoreAccent; + this.isComplex = false; + this.comparer = DataUtil.fnOperators.processOperator(this.operator); + } else if (field instanceof Predicate && value instanceof Predicate || value instanceof Array) { + this.isComplex = true; + this.condition = operator.toLowerCase(); + this.predicates = [field]; + this.matchCase = field.matchCase; + this.ignoreCase = field.ignoreCase; + this.ignoreAccent = field.ignoreAccent; + if (value instanceof Array) { + [].push.apply(this.predicates, value); + } else { + this.predicates.push(value); + } + } + return this; + } + + /** + * Adds n-number of new predicates on existing predicate with “and” condition. + * + * @param {Object[]} args - Defines the collection of predicates. + */ + public static and(...args: Object[]): Predicate { + return Predicate.combinePredicates([].slice.call(args, 0), 'and'); + } + + /** + * Adds new predicate on existing predicate with “and” condition. + * + * @param {string} field - Defines the column field. + * @param {string} operator - Defines the operator how to filter data. + * @param {string} value - Defines the values to match with data. + * @param {boolean} ignoreCase? - If ignore case set to false, then filter data with exact match or else + * filter data with case insensitive. + * @param ignoreCase + * @param ignoreAccent + */ + public and( + field: string | Predicate, operator?: string, value?: string | number | Date | boolean | null, + ignoreCase?: boolean, ignoreAccent?: boolean): Predicate { + return Predicate.combine(this, field, operator, value, 'and', ignoreCase, ignoreAccent); + } + + /** + * Adds n-number of new predicates on existing predicate with “or” condition. + * + * @param {Object[]} args - Defines the collection of predicates. + */ + public static or(...args: Object[]): Predicate { + return Predicate.combinePredicates([].slice.call(args, 0), 'or'); + } + + /** + * Adds new predicate on existing predicate with “or” condition. + * + * @param {string} field - Defines the column field. + * @param {string} operator - Defines the operator how to filter data. + * @param {string} value - Defines the values to match with data. + * @param {boolean} ignoreCase? - If ignore case set to false, then filter data with exact match or else + * filter data with case insensitive. + * @param ignoreCase + * @param ignoreAccent + */ + public or( + field: string | Predicate, operator?: string, value?: string | number | Date | boolean | null, ignoreCase?: boolean, + ignoreAccent?: boolean): Predicate { + return Predicate.combine(this, field, operator, value, 'or', ignoreCase, ignoreAccent); + } + + /** + * Adds n-number of new predicates on existing predicate with “and not” condition. + * + * @param {Object[]} args - Defines the collection of predicates. + */ + public static ornot(...args: Object[]): Predicate { + return Predicate.combinePredicates([].slice.call(args, 0), 'or not'); + } + + /** + * Adds new predicate on existing predicate with “and not” condition. + * + * @param {string} field - Defines the column field. + * @param {string} operator - Defines the operator how to filter data. + * @param {string} value - Defines the values to match with data. + * @param {boolean} ignoreCase? - If ignore case set to false, then filter data with exact match or else + * filter data with case insensitive. + * @param ignoreCase + * @param ignoreAccent + */ + public ornot( + field: string | Predicate, operator?: string, value?: string | number | Date | boolean | null, ignoreCase?: boolean, + ignoreAccent?: boolean): Predicate { + return Predicate.combine(this, field, operator, value, 'ornot', ignoreCase, ignoreAccent); + } + + /** + * Adds n-number of new predicates on existing predicate with “and not” condition. + * + * @param {Object[]} args - Defines the collection of predicates. + */ + public static andnot(...args: Object[]): Predicate { + return Predicate.combinePredicates([].slice.call(args, 0), 'and not'); + } + + /** + * Adds new predicate on existing predicate with “and not” condition. + * + * @param {string} field - Defines the column field. + * @param {string} operator - Defines the operator how to filter data. + * @param {string} value - Defines the values to match with data. + * @param {boolean} ignoreCase? - If ignore case set to false, then filter data with exact match or else + * filter data with case insensitive. + * @param ignoreCase + * @param ignoreAccent + */ + public andnot( + field: string | Predicate, operator?: string, value?: string | number | Date | boolean | null, ignoreCase?: boolean, + ignoreAccent?: boolean): Predicate { + return Predicate.combine(this, field, operator, value, 'andnot', ignoreCase, ignoreAccent); + } + + /** + * Converts plain JavaScript object to Predicate object. + * + * @param {Predicate[]|Predicate} json - Defines single or collection of Predicate. + */ + public static fromJson(json: Predicate[] | Predicate): Predicate[] { + if (json instanceof Array) { + const res: Predicate[] = []; + for (let i: number = 0, len: number = json.length; i < len; i++) { + res.push(this.fromJSONData(json[i])); + } + return res; + } + const pred: Predicate = json; + return this.fromJSONData(pred); + } + + /** + * Validate the record based on the predicates. + * + * @param {Object} record - Defines the datasource record. + */ + public validate(record: Object): boolean { + const predicate: Predicate[] = this.predicates ? this.predicates : []; + let ret: boolean; + let isAnd: boolean; + + if (!this.isComplex && this.comparer) { + if (this.condition && this.condition.indexOf('not') !== -1) { + this.condition = this.condition.split('not')[0] === '' ? undefined : this.condition.split('not')[0]; + return !this.comparer.call(this, DataUtil.getObject(this.field, record), this.value, this.ignoreCase, this.ignoreAccent); + } + else { + return this.comparer.call(this, DataUtil.getObject(this.field, record), this.value, this.ignoreCase, this.ignoreAccent); + } + } + if (this.condition && this.condition.indexOf('not') !== -1) { + isAnd = this.condition.indexOf('and') !== -1; + } + else { + isAnd = this.condition === 'and'; + } + + for (let i: number = 0; i < predicate.length; i++) { + if (i > 0 && this.condition && this.condition.indexOf('not') !== -1) { + predicate[i].condition = predicate[i].condition ? predicate[i].condition + 'not' : 'not'; + } + ret = predicate[i].validate(record); + if (isAnd) { + if (!ret) { return false; } + } else { + if (ret) { return true; } + } + } + return isAnd; + } + + /** + * Converts predicates to plain JavaScript. + * This method is uses Json stringify when serializing Predicate object. + */ + public toJson(): Object { + let predicates: Object[]; + let p: Predicate[]; + if (this.isComplex) { + predicates = []; + p = this.predicates; + for (let i: number = 0; i < p.length; i++) { + predicates.push(p[i].toJson()); + } + } + return { + isComplex: this.isComplex, + field: this.field, + operator: this.operator, + value: this.value, + ignoreCase: this.ignoreCase, + ignoreAccent: this.ignoreAccent, + condition: this.condition, + predicates: predicates, + matchCase: this.matchCase + }; + } + + private static combinePredicates(predicates: Predicate[] | Predicate[][], operator: string): Predicate { + if (predicates.length === 1) { + if (!(predicates[0] instanceof Array)) { + return predicates[0] as Predicate; + } + predicates = predicates[0] as Predicate[]; + } + return new Predicate(predicates[0] as Predicate, operator, (predicates as Predicate[]).slice(1)); + } + + private static combine( + pred: Predicate, field: string | Predicate, operator: string, value: string | number | Date | boolean | null, + condition: string, ignoreCase?: boolean, ignoreAccent?: boolean): Predicate { + if (field instanceof Predicate) { + return Predicate[condition](pred, field); + } + if (typeof field === 'string') { + return Predicate[condition](pred, new Predicate(field, operator, value, ignoreCase, ignoreAccent)); + } + return DataUtil.throwError('Predicate - ' + condition + ' : invalid arguments'); + } + private static fromJSONData(json: Predicate): Predicate { + const preds: Predicate[] = json.predicates || []; + const len: number = preds.length; + const predicates: Predicate[] = []; + let result: Predicate; + + for (let i: number = 0; i < len; i++) { + predicates.push(this.fromJSONData(preds[i])); + } + if (!json.isComplex) { + result = new Predicate(json.field, json.operator, json.value, json.ignoreCase, json.ignoreAccent); + } else { + result = new Predicate(predicates[0], json.condition, predicates.slice(1)); + } + + return result; + } +} +/** + * @hidden + */ +export interface QueryOptions { + fn?: string; + e?: QueryOptions; + fieldNames?: string | string[]; + operator?: string; + searchKey?: string | number | boolean; + ignoreCase?: boolean; + ignoreAccent?: boolean; + comparer?: string | Function; + format?: string| NumberFormatOptions | DateFormatOptions; + direction?: string; + pageIndex?: number; + pageSize?: number; + start?: number; + end?: number; + nos?: number; + field?: string; + fieldName?: string; + type?: Object; + name?: string | string[]; + filter?: Object; + key?: string; + value?: string | number | Date | boolean | Predicate | Predicate[] | (string | number | boolean | Date)[]; + isComplex?: boolean; + predicates?: Predicate[]; + condition?: string; +} +/** + * @hidden + */ +export interface QueryList { + onSelect?: QueryOptions; + onPage?: QueryOptions; + onSkip?: QueryOptions; + onTake?: QueryOptions; + onRange?: QueryOptions; +} +/** + * @hidden + */ +export interface ParamOption { + key: string; + value?: string | null; + fn?: Function; +} diff --git a/components/data/src/schema.ts b/components/data/src/schema.ts new file mode 100644 index 0000000..10131f2 --- /dev/null +++ b/components/data/src/schema.ts @@ -0,0 +1,27 @@ +/* tslint:disable:typedef */ +// tslint:disable-next-line:missing-jsdoc +export const schema = +// tslint:disable-next-line:no-multiline-string +`input Sort { + name: String! + direction: String! +} + +input Aggregate { + field: String! + type: String! +} + +input DataManager { + skip: Int + take: Int + sorted: [Sort] + group: [String] + table: String + select: [String] + where: String + search: String + requiresCounts: Boolean, + aggregates: [Aggregate], + params: String +}`; diff --git a/components/data/src/util.ts b/components/data/src/util.ts new file mode 100644 index 0000000..98e10b5 --- /dev/null +++ b/components/data/src/util.ts @@ -0,0 +1,2582 @@ +/* eslint-disable valid-jsdoc */ +/* eslint-disable security/detect-object-injection */ +import { isNullOrUndefined } from '@syncfusion/react-base'; +import { DataManager } from './manager'; +import { Query, Predicate } from './query'; +import { ReturnType } from './adaptors'; +const consts: { [key: string]: string } = { GroupGuid: '{271bbba0-1ee7}' }; + +/** + * Data manager common utility methods. + * + * @hidden + */ +export class DataUtil { + /** + * Specifies the value which will be used to adjust the date value to server timezone. + * + * @default null + */ + public static serverTimezoneOffset: number = null; + + /** + * Species whether are not to be parsed with serverTimezoneOffset value. + * + * @hidden + */ + public static timeZoneHandling: boolean = true; + + /** + * Returns the value by invoking the provided parameter function. + * If the paramater is not of type function then it will be returned as it is. + * + * @param {Function|string|string[]|number} value + * @param {Object} inst? + * @param inst + * @hidden + */ + public static getValue(value: T | Function, inst?: Object): T { + if (typeof value === 'function') { + return (value).call(inst || {}); + } + return value; + } + + /** + * Returns true if the input string ends with given string. + * + * @param {string} input + * @param {string} substr + */ + public static endsWith(input: string, substr: string): boolean { + return input.slice && input.slice(-substr.length) === substr; + } + + /** + * Returns true if the input string not ends with given string. + * + * @param {string} input + * @param {string} substr + */ + public static notEndsWith(input: string, substr: string): boolean { + return input.slice && input.slice(-substr.length) !== substr; + } + + /** + * Returns true if the input string starts with given string. + * + * @param {string} str + * @param {string} startstr + * @param input + * @param start + */ + public static startsWith(input: string, start: string): boolean { + return input.slice(0, start.length) === start; + } + + /** + * Returns true if the input string not starts with given string. + * + * @param {string} str + * @param {string} startstr + * @param input + * @param start + */ + public static notStartsWith(input: string, start: string): boolean { + return input.slice(0, start.length) !== start; + } + + /** + * Returns true if the input string pattern(wildcard) matches with given string. + * + * @param {string} str + * @param {string} startstr + * @param input + * @param pattern + */ + public static wildCard(input: string, pattern: string): boolean { + let asteriskSplit: string[]; + let optionalSplit: string[]; + // special character allowed search + if (pattern.indexOf('[') !== -1) { + pattern = pattern.split('[').join('[[]'); + } + if (pattern.indexOf('(') !== -1) { + pattern = pattern.split('(').join('[(]'); + } + if (pattern.indexOf(')') !== -1) { + pattern = pattern.split(')').join('[)]'); + } + if (pattern.indexOf('\\') !== -1) { + pattern = pattern.split('\\').join('[\\\\]'); + } + if (pattern.indexOf('*') !== -1) { + if (pattern.charAt(0) !== '*') { + pattern = '^' + pattern; + } + if (pattern.charAt(pattern.length - 1) !== '*') { + pattern = pattern + '$'; + } + asteriskSplit = pattern.split('*'); + for (let i: number = 0; i < asteriskSplit.length; i++) { + if (asteriskSplit[i].indexOf('.') === -1) { + asteriskSplit[i] = asteriskSplit[i] + '.*'; + } + else { + asteriskSplit[i] = asteriskSplit[i] + '*'; + } + } + pattern = asteriskSplit.join(''); + } + if (pattern.indexOf('%3f') !== -1 || pattern.indexOf('?') !== -1) { + optionalSplit = pattern.indexOf('%3f') !== -1 ? pattern.split('%3f') : pattern.split('?'); + pattern = optionalSplit.join('.'); + } + // eslint-disable-next-line security/detect-non-literal-regexp + const regexPattern: RegExp = new RegExp(pattern, 'g'); + return regexPattern.test(input); + } + + /** + * Returns true if the input string pattern(like) matches with given string. + * + * @param {string} str + * @param {string} startstr + * @param input + * @param pattern + */ + public static like(input: string, pattern: string): boolean { + if (pattern.indexOf('%') !== -1) { + if (pattern.charAt(0) === '%' && pattern.lastIndexOf('%') < 2) { + pattern = pattern.substring(1, pattern.length); + return DataUtil.startsWith(DataUtil.toLowerCase(input), DataUtil.toLowerCase(pattern)); + } + else if (pattern.charAt(pattern.length - 1) === '%' && pattern.indexOf('%') > pattern.length - 3) { + pattern = pattern.substring(0, pattern.length - 1); + return DataUtil.endsWith(DataUtil.toLowerCase(input), DataUtil.toLowerCase(pattern)); + } + else if (pattern.lastIndexOf('%') !== pattern.indexOf('%') && pattern.lastIndexOf('%') > pattern.indexOf('%') + 1) { + pattern = pattern.substring(pattern.indexOf('%') + 1, pattern.lastIndexOf('%')); + return input.indexOf(pattern) !== -1; + } + else { + return input.indexOf(pattern) !== -1; + } + } + else { + return false; + } + } + + /** + * To return the sorting function based on the string. + * + * @param {string} order + * @hidden + */ + public static fnSort(order: string): Function { + order = order ? DataUtil.toLowerCase(order) : 'ascending'; + if (order === 'ascending') { + return this.fnAscending; + } + return this.fnDescending; + } + + /** + * Comparer function which is used to sort the data in ascending order. + * + * @param {string|number} x + * @param {string|number} y + * @returns number + */ + public static fnAscending(x: string | number, y: string | number) + : number { + if (isNullOrUndefined(x) && isNullOrUndefined(y)) { + return 0; + } + if (y === null || y === undefined) { + return -1; + } + if (typeof x === 'string') { + return x.localeCompare(y); + } + if (x === null || x === undefined) { + return 1; + } + return x - y; + } + /** + * Comparer function which is used to sort the data in descending order. + * + * @param {string|number} x + * @param {string|number} y + * @returns number + */ + public static fnDescending(x: string | number, y: string | number) + : number { + if (isNullOrUndefined(x) && isNullOrUndefined(y)) { + return 0; + } + if (y === null || y === undefined) { + return 1; + } + if (typeof x === 'string') { + return x.localeCompare(y as string) * -1; + } + if (x === null || x === undefined) { + return -1; + } + + return y - x; + } + private static extractFields(obj: Object, fields: string[]): Object { + let newObj: { [key: string]: Object } | Object = {}; + + for (let i: number = 0; i < fields.length; i++) { + newObj = this.setValue(fields[i], this.getObject(fields[i], obj), newObj); + } + + return newObj; + } + + /** + * Select objects by given fields from jsonArray. + * + * @param {Object[]} jsonArray + * @param {string[]} fields + */ + public static select(jsonArray: Object[], fields: string[]): Object[] { + const newData: Object[] = []; + + for (let i: number = 0; i < jsonArray.length; i++) { + newData.push(this.extractFields(jsonArray[i], fields)); + } + return newData; + } + + /** + * Group the input data based on the field name. + * It also performs aggregation of the grouped records based on the aggregates paramater. + * + * @param {Object[]} jsonArray + * @param {string} field? + * @param {Object[]} agg? + * @param {number} level? + * @param {Object[]} groupDs? + * @param field + * @param aggregates + * @param level + * @param groupDs + * @param format + * @param isLazyLoad + */ + public static group( + jsonArray: Object[], field?: string, aggregates?: Object[], level?: number, + groupDs?: Object[], format?: Function, isLazyLoad?: boolean + ): Object[] { + level = level || 1; + const jsonData: Group[] = jsonArray; + const guid: string = 'GroupGuid'; + if ((jsonData).GroupGuid === consts[guid]) { + for (let j: number = 0; j < jsonData.length; j++) { + if (!isNullOrUndefined(groupDs)) { + let indx: number = -1; + const temp: Group[] = groupDs.filter((e: { key: string }) => { return e.key === jsonData[j].key; }); + indx = groupDs.indexOf(temp[0]); + jsonData[j].items = this.group( + jsonData[j].items, field, aggregates, (jsonData as Group).level + 1, + (groupDs as Group[])[indx].items, format, isLazyLoad + ); + jsonData[j].count = (groupDs as Group[])[indx].count; + } else { + jsonData[j].items = this.group( + jsonData[j].items, field, aggregates, (jsonData as Group).level + 1, null, format, isLazyLoad + ); + jsonData[j].count = jsonData[j].items.length; + } + } + + (jsonData as Group).childLevels += 1; + return jsonData; + } + + const grouped: { [key: string]: Group } = {}; + const groupedArray: Group[] = []; + + (groupedArray as Group).GroupGuid = consts[guid]; + (groupedArray as Group).level = level; + (groupedArray as Group).childLevels = 0; + (groupedArray as Group).records = jsonData; + + for (let i: number = 0; i < jsonData.length; i++) { + let val: string = (this.getVal(jsonData, i, field)); + if (!isNullOrUndefined(format)) { + val = format(val, field); + } + if (!grouped[val]) { + grouped[val] = { + key: val, + count: 0, + items: [], + aggregates: {}, + field: field + }; + groupedArray.push(grouped[val]); + if (!isNullOrUndefined(groupDs)) { + const tempObj: Group[] = groupDs.filter((e: { key: string }) => { return e.key === grouped[val].key; }); + grouped[val].count = tempObj[0].count; + } + } + + grouped[val].count = !isNullOrUndefined(groupDs) ? grouped[val].count : grouped[val].count += 1; + if (!isLazyLoad || (isLazyLoad && aggregates.length)) { + grouped[val].items.push(jsonData[i]); + } + } + if (aggregates && aggregates.length) { + + for (let i: number = 0; i < groupedArray.length; i++) { + const res: { [key: string]: Object } = {}; + let fn: Function; + const aggs: Aggregates[] = aggregates as Aggregates[]; + for (let j: number = 0; j < aggregates.length; j++) { + fn = DataUtil.aggregates[(aggregates[j] as Aggregates).type]; + if (!isNullOrUndefined(groupDs)) { + const temp: Group[] = groupDs.filter((e: { key: string }) => { return e.key === groupedArray[i].key; }); + if (fn) { + res[aggs[j].field + ' - ' + aggs[j].type] = fn(temp[0].items, aggs[j].field); + } + } else { + if (fn) { + res[aggs[j].field + ' - ' + aggs[j].type] = fn(groupedArray[i].items, aggs[j].field); + } + } + } + groupedArray[i].aggregates = res; + } + } + if (isLazyLoad && groupedArray.length && aggregates.length) { + for (let i: number = 0; i < groupedArray.length; i++) { + groupedArray[i].items = []; + } + } + return jsonData.length && groupedArray || jsonData; + } + + /** + * It is used to categorize the multiple items based on a specific field in jsonArray. + * The hierarchical queries are commonly required when you use foreign key binding. + * + * @param {string} fKey + * @param {string} from + * @param {Object[]} source + * @param {Group} lookup? + * @param {string} pKey? + * @param lookup + * @param pKey + * @hidden + */ + public static buildHierarchy(fKey: string, from: string, source: Group, lookup?: Group, pKey?: string): void { + let i: number; + const grp: { [key: string]: Object[] } = {}; + let temp: Object[]; + if (lookup.result) { lookup = lookup.result; } + + if (lookup.GroupGuid) { + this.throwError('DataManager: Do not have support Grouping in hierarchy'); + } + + for (i = 0; i < (lookup).length; i++) { + const fKeyData: number = (this.getObject(fKey, (lookup)[i])); + temp = grp[fKeyData] || (grp[fKeyData] = []); + temp.push((lookup)[i]); + } + + for (i = 0; i < (source).length; i++) { + const fKeyData: number = (this.getObject(pKey || fKey, (source)[i])); + (source)[i][from] = grp[fKeyData]; + } + } + + /** + * Throw error with the given string as message. + * + * @param {string} er + * @param error + */ + public static throwError: Function = (error: string) => { + try { + throw new Error(error); + } catch (e) { + // eslint-disable-next-line no-throw-literal + throw e.message + '\n' + e.stack; + } + } + + public static aggregates: Aggregates = { + /** + * Calculate sum of the given field in the data. + * + * @param {Object[]} ds + * @param {string} field + */ + sum: (ds: Object[], field: string): number => { + let result: number = 0; + let val: number; + const castRequired: boolean = typeof DataUtil.getVal(ds, 0, field) !== 'number'; + + for (let i: number = 0; i < ds.length; i++) { + val = DataUtil.getVal(ds, i, field) as number; + if (!isNaN(val) && val !== null) { + if (castRequired) { + val = +val; + } + result += val; + } + } + return result; + }, + /** + * Calculate average value of the given field in the data. + * + * @param {Object[]} ds + * @param {string} field + */ + average: (ds: Object[], field: string): number => { + return DataUtil.aggregates.sum(ds, field) / ds.length; + }, + /** + * Returns the min value of the data based on the field. + * + * @param {Object[]} ds + * @param {string|Function} field + */ + min: (ds: Object[], field: string | Function) => { + let comparer: Function; + if (typeof field === 'function') { + comparer = field; + field = null; + } + return DataUtil.getObject(field, DataUtil.getItemFromComparer(ds, field, comparer || DataUtil.fnAscending)); + }, + /** + * Returns the max value of the data based on the field. + * + * @param {Object[]} ds + * @param {string} field + * @returns number + */ + max: (ds: Object[], field: string | Function) => { + let comparer: Function; + if (typeof field === 'function') { + comparer = field; + field = null; + } + return DataUtil.getObject(field, DataUtil.getItemFromComparer(ds, field, comparer || DataUtil.fnDescending)); + }, + /** + * Returns the total number of true value present in the data based on the given boolean field name. + * + * @param {Object[]} ds + * @param {string} field + */ + truecount: (ds: Object[], field: string): number => { + return new DataManager(ds as JSON[]).executeLocal(new Query().where(field, 'equal', true, true)).length; + }, + /** + * Returns the total number of false value present in the data based on the given boolean field name. + * + * @param {Object[]} ds + * @param {string} field + */ + falsecount: (ds: Object[], field: string): number => { + return new DataManager(ds as JSON[]).executeLocal(new Query().where(field, 'equal', false, true)).length; + }, + /** + * Returns the length of the given data. + * + * @param {Object[]} ds + * @param {string} field? + * @param field + * @returns number + */ + count: (ds: Object[], field?: string): number => { + return ds.length; + } + }; + + /** + * The method used to get the field names which started with specified characters. + * + * @param {Object} obj + * @param {string[]} fields? + * @param {string} prefix? + * @param fields + * @param prefix + * @hidden + */ + public static getFieldList(obj: Object, fields?: string[], prefix?: string): string[] { + if (prefix === undefined) { + prefix = ''; + } + if (fields === undefined || fields === null) { + return this.getFieldList(obj, [], prefix); + } + const copyObj: { [key: string]: Object } = (obj as { [key: string]: Object }); + const keys: string[] = Object.keys(obj); + for (const prop of keys) { + if (typeof copyObj[prop] === 'object' && !(copyObj[prop] instanceof Array)) { + this.getFieldList((copyObj[prop] as Object), fields, prefix + prop + '.'); + } else { + fields.push(prefix + prop); + } + } + return fields; + } + + /** + * Gets the value of the property in the given object. + * The complex object can be accessed by providing the field names concatenated with dot(.). + * + * @param {string} nameSpace - The name of the property to be accessed. + * @param {Object} from - Defines the source object. + */ + public static getObject(nameSpace: string, from: Object): Object { + if (!nameSpace) { + return from; + } + if (!from) { + return undefined; + } + if (nameSpace.indexOf('.') === -1) { + if (!isNullOrUndefined(from[nameSpace])) { + return from[nameSpace]; + } + else { + const lowerCaseNameSpace: string = nameSpace.charAt(0).toLowerCase() + nameSpace.slice(1); + const upperCaseNameSpace: string = nameSpace.charAt(0).toUpperCase() + nameSpace.slice(1); + if (!isNullOrUndefined(from[lowerCaseNameSpace])) { + return from[lowerCaseNameSpace]; + } else if (!isNullOrUndefined(from[upperCaseNameSpace])) { + return from[upperCaseNameSpace]; + } else { + return null; + } + } + } + let value: Object = from; + const splits: string[] = nameSpace.split('.'); + for (let i: number = 0; i < splits.length; i++) { + if (value == null) { + break; + } + value = value[splits[i]]; + if (value === undefined) { + const casing: string = splits[i].charAt(0).toUpperCase() + splits[i].slice(1); + value = from[casing] || from[casing.charAt(0).toLowerCase() + casing.slice(1)] || null; + } + from = value; + } + return value; + } + + /** + * To set value for the nameSpace in desired object. + * + * @param {string} nameSpace - String value to the get the inner object. + * @param {Object} value - Value that you need to set. + * @param {Object} obj - Object to get the inner object value. + * @return { [key: string]: Object; } | Object + * @hidden + */ + public static setValue(nameSpace: string, value: Object | null, obj: Object): { [key: string]: Object; } | Object { + const keys: string[] = nameSpace.toString().split('.'); + const start: Object = obj || {}; + let fromObj: Object = start; + let i: number; + const length: number = keys.length; + let key: string; + for (i = 0; i < length; i++) { + key = keys[i]; + if (i + 1 === length) { + fromObj[key] = value === undefined ? undefined : value; + } else if (isNullOrUndefined(fromObj[key])) { + fromObj[key] = {}; + } + fromObj = fromObj[key]; + } + return start; + } + + /** + * Sort the given data based on the field and comparer. + * + * @param {Object[]} dataSource - Defines the input data. + * @param {string} field - Defines the field to be sorted. + * @param {Function} comparer - Defines the comparer function used to sort the records. + */ + public static sort(dataSource: Object[], field: string, comparer: Function): Object[] { + if (dataSource.length <= 1) { + return dataSource; + } + return dataSource.slice() + .sort((a: Object, b: Object) => (comparer)(this.getVal([a], 0, field), this.getVal([b], 0, field), a, b)); + } + public static ignoreDiacritics(value: string | number | Date | boolean): string | Object { + if (typeof value !== 'string') { + return value; + } + const result: string[] = value.split(''); + const newValue: string[] = result.map((temp: string) => temp in DataUtil.diacritics ? DataUtil.diacritics[temp] : temp); + return newValue.join(''); + } + public static ignoreDiacriticsForArrays(valueArray: Array): Array { + if (!Array.isArray(valueArray)) { + return []; + } + return valueArray.map(item => { + return DataUtil.ignoreDiacritics(item) as string | number; + }); + } + + public static merge(left: Object[], right: Object[], fieldName: string, comparer: Function): Object[] { + const result: Object[] = []; + let current: Object[]; + + while (left.length > 0 || right.length > 0) { + if (left.length > 0 && right.length > 0) { + if (comparer) { + current = (comparer)(this.getVal(left, 0, fieldName), this.getVal(right, 0, fieldName), + left[0], right[0]) <= 0 ? left : right; + } else { + current = left[0][fieldName] < left[0][fieldName] ? left : right; + } + } else { + current = left.length > 0 ? left : right; + } + result.push(current.shift()); + } + return result; + } + private static getVal(array: Object[], index: number, field?: string): Object { + return field ? this.getObject(field, array[index]) : array[index]; + } + private static toLowerCase(val: string | number | boolean | Date): string { + if (isNullOrUndefined(val)) return ''; + if (typeof val === 'string') return val.toLowerCase(); + if (val instanceof Date) return val.toString().toLowerCase(); + return val.toString(); + } + /** + * Specifies the Object with filter operators. + */ + public static operatorSymbols: { [key: string]: string } = { + '<': 'lessthan', + '>': 'greaterthan', + '<=': 'lessthanorequal', + '>=': 'greaterthanorequal', + '==': 'equal', + '!=': 'notequal', + '*=': 'contains', + '$=': 'endswith', + '^=': 'startswith' + }; + /** + * Specifies the Object with filter operators which will be used for OData filter query generation. + * * It will be used for date/number type filter query. + */ + public static odBiOperator: { [key: string]: string } = { + '<': ' lt ', + '>': ' gt ', + '<=': ' le ', + '>=': ' ge ', + '==': ' eq ', + '!=': ' ne ', + 'lessthan': ' lt ', + 'lessthanorequal': ' le ', + 'greaterthan': ' gt ', + 'greaterthanorequal': ' ge ', + 'equal': ' eq ', + 'notequal': ' ne ' + }; + /** + * Specifies the Object with filter operators which will be used for OData filter query generation. + * It will be used for string type filter query. + */ + public static odUniOperator: { [key: string]: string } = { + '$=': 'endswith', + '^=': 'startswith', + '*=': 'substringof', + 'endswith': 'endswith', + 'startswith': 'startswith', + 'contains': 'substringof', + 'doesNotEndWith': 'not endswith', + 'doesNotStartWith': 'not startswith', + 'doesNotcontain': 'not substringof', + 'wildcard': 'wildcard', + 'like': 'like' + }; + + /** + * Specifies the Object with filter operators which will be used for ODataV4 filter query generation. + * It will be used for string type filter query. + */ + public static odv4UniOperator: { [key: string]: string } = { + '$=': 'endswith', + '^=': 'startswith', + '*=': 'contains', + 'endswith': 'endswith', + 'startswith': 'startswith', + 'contains': 'contains', + 'doesNotEndWith': 'not endswith', + 'doesNotStartWith': 'not startswith', + 'doesNotcontain': 'not contains', + 'wildcard': 'wildcard', + 'like': 'like' + }; + public static diacritics: { [key: string]: string } = { + '\u24B6': 'A', + '\uFF21': 'A', + '\u00C0': 'A', + '\u00C1': 'A', + '\u00C2': 'A', + '\u1EA6': 'A', + '\u1EA4': 'A', + '\u1EAA': 'A', + '\u1EA8': 'A', + '\u00C3': 'A', + '\u0100': 'A', + '\u0102': 'A', + '\u1EB0': 'A', + '\u1EAE': 'A', + '\u1EB4': 'A', + '\u1EB2': 'A', + '\u0226': 'A', + '\u01E0': 'A', + '\u00C4': 'A', + '\u01DE': 'A', + '\u1EA2': 'A', + '\u00C5': 'A', + '\u01FA': 'A', + '\u01CD': 'A', + '\u0200': 'A', + '\u0202': 'A', + '\u1EA0': 'A', + '\u1EAC': 'A', + '\u1EB6': 'A', + '\u1E00': 'A', + '\u0104': 'A', + '\u023A': 'A', + '\u2C6F': 'A', + '\uA732': 'AA', + '\u00C6': 'AE', + '\u01FC': 'AE', + '\u01E2': 'AE', + '\uA734': 'AO', + '\uA736': 'AU', + '\uA738': 'AV', + '\uA73A': 'AV', + '\uA73C': 'AY', + '\u24B7': 'B', + '\uFF22': 'B', + '\u1E02': 'B', + '\u1E04': 'B', + '\u1E06': 'B', + '\u0243': 'B', + '\u0182': 'B', + '\u0181': 'B', + '\u24B8': 'C', + '\uFF23': 'C', + '\u0106': 'C', + '\u0108': 'C', + '\u010A': 'C', + '\u010C': 'C', + '\u00C7': 'C', + '\u1E08': 'C', + '\u0187': 'C', + '\u023B': 'C', + '\uA73E': 'C', + '\u24B9': 'D', + '\uFF24': 'D', + '\u1E0A': 'D', + '\u010E': 'D', + '\u1E0C': 'D', + '\u1E10': 'D', + '\u1E12': 'D', + '\u1E0E': 'D', + '\u0110': 'D', + '\u018B': 'D', + '\u018A': 'D', + '\u0189': 'D', + '\uA779': 'D', + '\u01F1': 'DZ', + '\u01C4': 'DZ', + '\u01F2': 'Dz', + '\u01C5': 'Dz', + '\u24BA': 'E', + '\uFF25': 'E', + '\u00C8': 'E', + '\u00C9': 'E', + '\u00CA': 'E', + '\u1EC0': 'E', + '\u1EBE': 'E', + '\u1EC4': 'E', + '\u1EC2': 'E', + '\u1EBC': 'E', + '\u0112': 'E', + '\u1E14': 'E', + '\u1E16': 'E', + '\u0114': 'E', + '\u0116': 'E', + '\u00CB': 'E', + '\u1EBA': 'E', + '\u011A': 'E', + '\u0204': 'E', + '\u0206': 'E', + '\u1EB8': 'E', + '\u1EC6': 'E', + '\u0228': 'E', + '\u1E1C': 'E', + '\u0118': 'E', + '\u1E18': 'E', + '\u1E1A': 'E', + '\u0190': 'E', + '\u018E': 'E', + '\u24BB': 'F', + '\uFF26': 'F', + '\u1E1E': 'F', + '\u0191': 'F', + '\uA77B': 'F', + '\u24BC': 'G', + '\uFF27': 'G', + '\u01F4': 'G', + '\u011C': 'G', + '\u1E20': 'G', + '\u011E': 'G', + '\u0120': 'G', + '\u01E6': 'G', + '\u0122': 'G', + '\u01E4': 'G', + '\u0193': 'G', + '\uA7A0': 'G', + '\uA77D': 'G', + '\uA77E': 'G', + '\u24BD': 'H', + '\uFF28': 'H', + '\u0124': 'H', + '\u1E22': 'H', + '\u1E26': 'H', + '\u021E': 'H', + '\u1E24': 'H', + '\u1E28': 'H', + '\u1E2A': 'H', + '\u0126': 'H', + '\u2C67': 'H', + '\u2C75': 'H', + '\uA78D': 'H', + '\u24BE': 'I', + '\uFF29': 'I', + '\u00CC': 'I', + '\u00CD': 'I', + '\u00CE': 'I', + '\u0128': 'I', + '\u012A': 'I', + '\u012C': 'I', + '\u0130': 'I', + '\u00CF': 'I', + '\u1E2E': 'I', + '\u1EC8': 'I', + '\u01CF': 'I', + '\u0208': 'I', + '\u020A': 'I', + '\u1ECA': 'I', + '\u012E': 'I', + '\u1E2C': 'I', + '\u0197': 'I', + '\u24BF': 'J', + '\uFF2A': 'J', + '\u0134': 'J', + '\u0248': 'J', + '\u24C0': 'K', + '\uFF2B': 'K', + '\u1E30': 'K', + '\u01E8': 'K', + '\u1E32': 'K', + '\u0136': 'K', + '\u1E34': 'K', + '\u0198': 'K', + '\u2C69': 'K', + '\uA740': 'K', + '\uA742': 'K', + '\uA744': 'K', + '\uA7A2': 'K', + '\u24C1': 'L', + '\uFF2C': 'L', + '\u013F': 'L', + '\u0139': 'L', + '\u013D': 'L', + '\u1E36': 'L', + '\u1E38': 'L', + '\u013B': 'L', + '\u1E3C': 'L', + '\u1E3A': 'L', + '\u0141': 'L', + '\u023D': 'L', + '\u2C62': 'L', + '\u2C60': 'L', + '\uA748': 'L', + '\uA746': 'L', + '\uA780': 'L', + '\u01C7': 'LJ', + '\u01C8': 'Lj', + '\u24C2': 'M', + '\uFF2D': 'M', + '\u1E3E': 'M', + '\u1E40': 'M', + '\u1E42': 'M', + '\u2C6E': 'M', + '\u019C': 'M', + '\u24C3': 'N', + '\uFF2E': 'N', + '\u01F8': 'N', + '\u0143': 'N', + '\u00D1': 'N', + '\u1E44': 'N', + '\u0147': 'N', + '\u1E46': 'N', + '\u0145': 'N', + '\u1E4A': 'N', + '\u1E48': 'N', + '\u0220': 'N', + '\u019D': 'N', + '\uA790': 'N', + '\uA7A4': 'N', + '\u01CA': 'NJ', + '\u01CB': 'Nj', + '\u24C4': 'O', + '\uFF2F': 'O', + '\u00D2': 'O', + '\u00D3': 'O', + '\u00D4': 'O', + '\u1ED2': 'O', + '\u1ED0': 'O', + '\u1ED6': 'O', + '\u1ED4': 'O', + '\u00D5': 'O', + '\u1E4C': 'O', + '\u022C': 'O', + '\u1E4E': 'O', + '\u014C': 'O', + '\u1E50': 'O', + '\u1E52': 'O', + '\u014E': 'O', + '\u022E': 'O', + '\u0230': 'O', + '\u00D6': 'O', + '\u022A': 'O', + '\u1ECE': 'O', + '\u0150': 'O', + '\u01D1': 'O', + '\u020C': 'O', + '\u020E': 'O', + '\u01A0': 'O', + '\u1EDC': 'O', + '\u1EDA': 'O', + '\u1EE0': 'O', + '\u1EDE': 'O', + '\u1EE2': 'O', + '\u1ECC': 'O', + '\u1ED8': 'O', + '\u01EA': 'O', + '\u01EC': 'O', + '\u00D8': 'O', + '\u01FE': 'O', + '\u0186': 'O', + '\u019F': 'O', + '\uA74A': 'O', + '\uA74C': 'O', + '\u01A2': 'OI', + '\uA74E': 'OO', + '\u0222': 'OU', + '\u24C5': 'P', + '\uFF30': 'P', + '\u1E54': 'P', + '\u1E56': 'P', + '\u01A4': 'P', + '\u2C63': 'P', + '\uA750': 'P', + '\uA752': 'P', + '\uA754': 'P', + '\u24C6': 'Q', + '\uFF31': 'Q', + '\uA756': 'Q', + '\uA758': 'Q', + '\u024A': 'Q', + '\u24C7': 'R', + '\uFF32': 'R', + '\u0154': 'R', + '\u1E58': 'R', + '\u0158': 'R', + '\u0210': 'R', + '\u0212': 'R', + '\u1E5A': 'R', + '\u1E5C': 'R', + '\u0156': 'R', + '\u1E5E': 'R', + '\u024C': 'R', + '\u2C64': 'R', + '\uA75A': 'R', + '\uA7A6': 'R', + '\uA782': 'R', + '\u24C8': 'S', + '\uFF33': 'S', + '\u1E9E': 'S', + '\u015A': 'S', + '\u1E64': 'S', + '\u015C': 'S', + '\u1E60': 'S', + '\u0160': 'S', + '\u1E66': 'S', + '\u1E62': 'S', + '\u1E68': 'S', + '\u0218': 'S', + '\u015E': 'S', + '\u2C7E': 'S', + '\uA7A8': 'S', + '\uA784': 'S', + '\u24C9': 'T', + '\uFF34': 'T', + '\u1E6A': 'T', + '\u0164': 'T', + '\u1E6C': 'T', + '\u021A': 'T', + '\u0162': 'T', + '\u1E70': 'T', + '\u1E6E': 'T', + '\u0166': 'T', + '\u01AC': 'T', + '\u01AE': 'T', + '\u023E': 'T', + '\uA786': 'T', + '\uA728': 'TZ', + '\u24CA': 'U', + '\uFF35': 'U', + '\u00D9': 'U', + '\u00DA': 'U', + '\u00DB': 'U', + '\u0168': 'U', + '\u1E78': 'U', + '\u016A': 'U', + '\u1E7A': 'U', + '\u016C': 'U', + '\u00DC': 'U', + '\u01DB': 'U', + '\u01D7': 'U', + '\u01D5': 'U', + '\u01D9': 'U', + '\u1EE6': 'U', + '\u016E': 'U', + '\u0170': 'U', + '\u01D3': 'U', + '\u0214': 'U', + '\u0216': 'U', + '\u01AF': 'U', + '\u1EEA': 'U', + '\u1EE8': 'U', + '\u1EEE': 'U', + '\u1EEC': 'U', + '\u1EF0': 'U', + '\u1EE4': 'U', + '\u1E72': 'U', + '\u0172': 'U', + '\u1E76': 'U', + '\u1E74': 'U', + '\u0244': 'U', + '\u24CB': 'V', + '\uFF36': 'V', + '\u1E7C': 'V', + '\u1E7E': 'V', + '\u01B2': 'V', + '\uA75E': 'V', + '\u0245': 'V', + '\uA760': 'VY', + '\u24CC': 'W', + '\uFF37': 'W', + '\u1E80': 'W', + '\u1E82': 'W', + '\u0174': 'W', + '\u1E86': 'W', + '\u1E84': 'W', + '\u1E88': 'W', + '\u2C72': 'W', + '\u24CD': 'X', + '\uFF38': 'X', + '\u1E8A': 'X', + '\u1E8C': 'X', + '\u24CE': 'Y', + '\uFF39': 'Y', + '\u1EF2': 'Y', + '\u00DD': 'Y', + '\u0176': 'Y', + '\u1EF8': 'Y', + '\u0232': 'Y', + '\u1E8E': 'Y', + '\u0178': 'Y', + '\u1EF6': 'Y', + '\u1EF4': 'Y', + '\u01B3': 'Y', + '\u024E': 'Y', + '\u1EFE': 'Y', + '\u24CF': 'Z', + '\uFF3A': 'Z', + '\u0179': 'Z', + '\u1E90': 'Z', + '\u017B': 'Z', + '\u017D': 'Z', + '\u1E92': 'Z', + '\u1E94': 'Z', + '\u01B5': 'Z', + '\u0224': 'Z', + '\u2C7F': 'Z', + '\u2C6B': 'Z', + '\uA762': 'Z', + '\u24D0': 'a', + '\uFF41': 'a', + '\u1E9A': 'a', + '\u00E0': 'a', + '\u00E1': 'a', + '\u00E2': 'a', + '\u1EA7': 'a', + '\u1EA5': 'a', + '\u1EAB': 'a', + '\u1EA9': 'a', + '\u00E3': 'a', + '\u0101': 'a', + '\u0103': 'a', + '\u1EB1': 'a', + '\u1EAF': 'a', + '\u1EB5': 'a', + '\u1EB3': 'a', + '\u0227': 'a', + '\u01E1': 'a', + '\u00E4': 'a', + '\u01DF': 'a', + '\u1EA3': 'a', + '\u00E5': 'a', + '\u01FB': 'a', + '\u01CE': 'a', + '\u0201': 'a', + '\u0203': 'a', + '\u1EA1': 'a', + '\u1EAD': 'a', + '\u1EB7': 'a', + '\u1E01': 'a', + '\u0105': 'a', + '\u2C65': 'a', + '\u0250': 'a', + '\uA733': 'aa', + '\u00E6': 'ae', + '\u01FD': 'ae', + '\u01E3': 'ae', + '\uA735': 'ao', + '\uA737': 'au', + '\uA739': 'av', + '\uA73B': 'av', + '\uA73D': 'ay', + '\u24D1': 'b', + '\uFF42': 'b', + '\u1E03': 'b', + '\u1E05': 'b', + '\u1E07': 'b', + '\u0180': 'b', + '\u0183': 'b', + '\u0253': 'b', + '\u24D2': 'c', + '\uFF43': 'c', + '\u0107': 'c', + '\u0109': 'c', + '\u010B': 'c', + '\u010D': 'c', + '\u00E7': 'c', + '\u1E09': 'c', + '\u0188': 'c', + '\u023C': 'c', + '\uA73F': 'c', + '\u2184': 'c', + '\u24D3': 'd', + '\uFF44': 'd', + '\u1E0B': 'd', + '\u010F': 'd', + '\u1E0D': 'd', + '\u1E11': 'd', + '\u1E13': 'd', + '\u1E0F': 'd', + '\u0111': 'd', + '\u018C': 'd', + '\u0256': 'd', + '\u0257': 'd', + '\uA77A': 'd', + '\u01F3': 'dz', + '\u01C6': 'dz', + '\u24D4': 'e', + '\uFF45': 'e', + '\u00E8': 'e', + '\u00E9': 'e', + '\u00EA': 'e', + '\u1EC1': 'e', + '\u1EBF': 'e', + '\u1EC5': 'e', + '\u1EC3': 'e', + '\u1EBD': 'e', + '\u0113': 'e', + '\u1E15': 'e', + '\u1E17': 'e', + '\u0115': 'e', + '\u0117': 'e', + '\u00EB': 'e', + '\u1EBB': 'e', + '\u011B': 'e', + '\u0205': 'e', + '\u0207': 'e', + '\u1EB9': 'e', + '\u1EC7': 'e', + '\u0229': 'e', + '\u1E1D': 'e', + '\u0119': 'e', + '\u1E19': 'e', + '\u1E1B': 'e', + '\u0247': 'e', + '\u025B': 'e', + '\u01DD': 'e', + '\u24D5': 'f', + '\uFF46': 'f', + '\u1E1F': 'f', + '\u0192': 'f', + '\uA77C': 'f', + '\u24D6': 'g', + '\uFF47': 'g', + '\u01F5': 'g', + '\u011D': 'g', + '\u1E21': 'g', + '\u011F': 'g', + '\u0121': 'g', + '\u01E7': 'g', + '\u0123': 'g', + '\u01E5': 'g', + '\u0260': 'g', + '\uA7A1': 'g', + '\u1D79': 'g', + '\uA77F': 'g', + '\u24D7': 'h', + '\uFF48': 'h', + '\u0125': 'h', + '\u1E23': 'h', + '\u1E27': 'h', + '\u021F': 'h', + '\u1E25': 'h', + '\u1E29': 'h', + '\u1E2B': 'h', + '\u1E96': 'h', + '\u0127': 'h', + '\u2C68': 'h', + '\u2C76': 'h', + '\u0265': 'h', + '\u0195': 'hv', + '\u24D8': 'i', + '\uFF49': 'i', + '\u00EC': 'i', + '\u00ED': 'i', + '\u00EE': 'i', + '\u0129': 'i', + '\u012B': 'i', + '\u012D': 'i', + '\u00EF': 'i', + '\u1E2F': 'i', + '\u1EC9': 'i', + '\u01D0': 'i', + '\u0209': 'i', + '\u020B': 'i', + '\u1ECB': 'i', + '\u012F': 'i', + '\u1E2D': 'i', + '\u0268': 'i', + '\u0131': 'i', + '\u24D9': 'j', + '\uFF4A': 'j', + '\u0135': 'j', + '\u01F0': 'j', + '\u0249': 'j', + '\u24DA': 'k', + '\uFF4B': 'k', + '\u1E31': 'k', + '\u01E9': 'k', + '\u1E33': 'k', + '\u0137': 'k', + '\u1E35': 'k', + '\u0199': 'k', + '\u2C6A': 'k', + '\uA741': 'k', + '\uA743': 'k', + '\uA745': 'k', + '\uA7A3': 'k', + '\u24DB': 'l', + '\uFF4C': 'l', + '\u0140': 'l', + '\u013A': 'l', + '\u013E': 'l', + '\u1E37': 'l', + '\u1E39': 'l', + '\u013C': 'l', + '\u1E3D': 'l', + '\u1E3B': 'l', + '\u017F': 'l', + '\u0142': 'l', + '\u019A': 'l', + '\u026B': 'l', + '\u2C61': 'l', + '\uA749': 'l', + '\uA781': 'l', + '\uA747': 'l', + '\u01C9': 'lj', + '\u24DC': 'm', + '\uFF4D': 'm', + '\u1E3F': 'm', + '\u1E41': 'm', + '\u1E43': 'm', + '\u0271': 'm', + '\u026F': 'm', + '\u24DD': 'n', + '\uFF4E': 'n', + '\u01F9': 'n', + '\u0144': 'n', + '\u00F1': 'n', + '\u1E45': 'n', + '\u0148': 'n', + '\u1E47': 'n', + '\u0146': 'n', + '\u1E4B': 'n', + '\u1E49': 'n', + '\u019E': 'n', + '\u0272': 'n', + '\u0149': 'n', + '\uA791': 'n', + '\uA7A5': 'n', + '\u01CC': 'nj', + '\u24DE': 'o', + '\uFF4F': 'o', + '\u00F2': 'o', + '\u00F3': 'o', + '\u00F4': 'o', + '\u1ED3': 'o', + '\u1ED1': 'o', + '\u1ED7': 'o', + '\u1ED5': 'o', + '\u00F5': 'o', + '\u1E4D': 'o', + '\u022D': 'o', + '\u1E4F': 'o', + '\u014D': 'o', + '\u1E51': 'o', + '\u1E53': 'o', + '\u014F': 'o', + '\u022F': 'o', + '\u0231': 'o', + '\u00F6': 'o', + '\u022B': 'o', + '\u1ECF': 'o', + '\u0151': 'o', + '\u01D2': 'o', + '\u020D': 'o', + '\u020F': 'o', + '\u01A1': 'o', + '\u1EDD': 'o', + '\u1EDB': 'o', + '\u1EE1': 'o', + '\u1EDF': 'o', + '\u1EE3': 'o', + '\u1ECD': 'o', + '\u1ED9': 'o', + '\u01EB': 'o', + '\u01ED': 'o', + '\u00F8': 'o', + '\u01FF': 'o', + '\u0254': 'o', + '\uA74B': 'o', + '\uA74D': 'o', + '\u0275': 'o', + '\u01A3': 'oi', + '\u0223': 'ou', + '\uA74F': 'oo', + '\u24DF': 'p', + '\uFF50': 'p', + '\u1E55': 'p', + '\u1E57': 'p', + '\u01A5': 'p', + '\u1D7D': 'p', + '\uA751': 'p', + '\uA753': 'p', + '\uA755': 'p', + '\u24E0': 'q', + '\uFF51': 'q', + '\u024B': 'q', + '\uA757': 'q', + '\uA759': 'q', + '\u24E1': 'r', + '\uFF52': 'r', + '\u0155': 'r', + '\u1E59': 'r', + '\u0159': 'r', + '\u0211': 'r', + '\u0213': 'r', + '\u1E5B': 'r', + '\u1E5D': 'r', + '\u0157': 'r', + '\u1E5F': 'r', + '\u024D': 'r', + '\u027D': 'r', + '\uA75B': 'r', + '\uA7A7': 'r', + '\uA783': 'r', + '\u24E2': 's', + '\uFF53': 's', + '\u00DF': 's', + '\u015B': 's', + '\u1E65': 's', + '\u015D': 's', + '\u1E61': 's', + '\u0161': 's', + '\u1E67': 's', + '\u1E63': 's', + '\u1E69': 's', + '\u0219': 's', + '\u015F': 's', + '\u023F': 's', + '\uA7A9': 's', + '\uA785': 's', + '\u1E9B': 's', + '\u24E3': 't', + '\uFF54': 't', + '\u1E6B': 't', + '\u1E97': 't', + '\u0165': 't', + '\u1E6D': 't', + '\u021B': 't', + '\u0163': 't', + '\u1E71': 't', + '\u1E6F': 't', + '\u0167': 't', + '\u01AD': 't', + '\u0288': 't', + '\u2C66': 't', + '\uA787': 't', + '\uA729': 'tz', + '\u24E4': 'u', + '\uFF55': 'u', + '\u00F9': 'u', + '\u00FA': 'u', + '\u00FB': 'u', + '\u0169': 'u', + '\u1E79': 'u', + '\u016B': 'u', + '\u1E7B': 'u', + '\u016D': 'u', + '\u00FC': 'u', + '\u01DC': 'u', + '\u01D8': 'u', + '\u01D6': 'u', + '\u01DA': 'u', + '\u1EE7': 'u', + '\u016F': 'u', + '\u0171': 'u', + '\u01D4': 'u', + '\u0215': 'u', + '\u0217': 'u', + '\u01B0': 'u', + '\u1EEB': 'u', + '\u1EE9': 'u', + '\u1EEF': 'u', + '\u1EED': 'u', + '\u1EF1': 'u', + '\u1EE5': 'u', + '\u1E73': 'u', + '\u0173': 'u', + '\u1E77': 'u', + '\u1E75': 'u', + '\u0289': 'u', + '\u24E5': 'v', + '\uFF56': 'v', + '\u1E7D': 'v', + '\u1E7F': 'v', + '\u028B': 'v', + '\uA75F': 'v', + '\u028C': 'v', + '\uA761': 'vy', + '\u24E6': 'w', + '\uFF57': 'w', + '\u1E81': 'w', + '\u1E83': 'w', + '\u0175': 'w', + '\u1E87': 'w', + '\u1E85': 'w', + '\u1E98': 'w', + '\u1E89': 'w', + '\u2C73': 'w', + '\u24E7': 'x', + '\uFF58': 'x', + '\u1E8B': 'x', + '\u1E8D': 'x', + '\u24E8': 'y', + '\uFF59': 'y', + '\u1EF3': 'y', + '\u00FD': 'y', + '\u0177': 'y', + '\u1EF9': 'y', + '\u0233': 'y', + '\u1E8F': 'y', + '\u00FF': 'y', + '\u1EF7': 'y', + '\u1E99': 'y', + '\u1EF5': 'y', + '\u01B4': 'y', + '\u024F': 'y', + '\u1EFF': 'y', + '\u24E9': 'z', + '\uFF5A': 'z', + '\u017A': 'z', + '\u1E91': 'z', + '\u017C': 'z', + '\u017E': 'z', + '\u1E93': 'z', + '\u1E95': 'z', + '\u01B6': 'z', + '\u0225': 'z', + '\u0240': 'z', + '\u2C6C': 'z', + '\uA763': 'z', + '\u0386': '\u0391', + '\u0388': '\u0395', + '\u0389': '\u0397', + '\u038A': '\u0399', + '\u03AA': '\u0399', + '\u038C': '\u039F', + '\u038E': '\u03A5', + '\u03AB': '\u03A5', + '\u038F': '\u03A9', + '\u03AC': '\u03B1', + '\u03AD': '\u03B5', + '\u03AE': '\u03B7', + '\u03AF': '\u03B9', + '\u03CA': '\u03B9', + '\u0390': '\u03B9', + '\u03CC': '\u03BF', + '\u03CD': '\u03C5', + '\u03CB': '\u03C5', + '\u03B0': '\u03C5', + '\u03C9': '\u03C9', + '\u03C2': '\u03C3' + }; + + public static fnOperators: Operators = { + /** + * Returns true when the actual input is equal to the given input. + * + * @param {string|number|boolean} actual + * @param {string|number|boolean} expected + * @param {boolean} ignoreCase? + * @param {boolean} ignoreAccent? + * @param ignoreCase + * @param ignoreAccent + */ + equal: (actual: string | number | boolean, expected: string | number | boolean, ignoreCase?: boolean, + ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + if (ignoreCase) { + return DataUtil.toLowerCase(actual) === DataUtil.toLowerCase(expected); + } + return actual === expected; + }, + /** + * Returns true when the actual input is not equal to the given input. + * + * @param {string|number|boolean} actual + * @param {string|number|boolean} expected + * @param {boolean} ignoreCase? + * @param ignoreCase + * @param ignoreAccent + */ + notequal: (actual: string | number | boolean, expected: string | number | boolean, ignoreCase?: boolean, + ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + return !DataUtil.fnOperators.equal(actual, expected, ignoreCase); + }, + /** + * Returns true when the actual input is less than to the given input. + * + * @param {string|number|boolean} actual + * @param {string|number|boolean} expected + * @param {boolean} ignoreCase? + * @param ignoreCase + */ + lessthan: (actual: string | number | boolean, expected: string | number | boolean, ignoreCase?: boolean): boolean => { + if (ignoreCase) { + return DataUtil.toLowerCase(actual) < DataUtil.toLowerCase(expected); + } + if (isNullOrUndefined(actual)) { + actual = undefined; + } + return actual < expected; + }, + /** + * Returns true when the actual input is greater than to the given input. + * + * @param {string|number|boolean} actual + * @param {string|number|boolean} expected + * @param {boolean} ignoreCase? + * @param ignoreCase + */ + greaterthan: (actual: string | number | boolean, expected: string | number | boolean, ignoreCase?: boolean): boolean => { + if (ignoreCase) { + return DataUtil.toLowerCase(actual) > DataUtil.toLowerCase(expected); + } + return actual > expected; + }, + /** + * Returns true when the actual input is less than or equal to the given input. + * + * @param {string|number|boolean} actual + * @param {string|number|boolean} expected + * @param {boolean} ignoreCase? + * @param ignoreCase + */ + lessthanorequal: (actual: string | number | boolean, expected: string | number | boolean, ignoreCase?: boolean): boolean => { + if (ignoreCase) { + return DataUtil.toLowerCase(actual) <= DataUtil.toLowerCase(expected); + } + if (isNullOrUndefined(actual)) { + actual = undefined; + } + return actual <= expected; + }, + /** + * Returns true when the actual input is greater than or equal to the given input. + * + * @param {string|number|boolean} actual + * @param {string|number|boolean} expected + * @param {boolean} ignoreCase? + * @param ignoreCase + */ + greaterthanorequal: (actual: string | number | boolean, expected: string | number | boolean, ignoreCase?: boolean): boolean => { + if (ignoreCase) { + return DataUtil.toLowerCase(actual) >= DataUtil.toLowerCase(expected); + } + if (isNullOrUndefined(actual)) { + actual = undefined; + } + return actual >= expected; + }, + /** + * Returns true when the actual input contains the given string. + * + * @param {string|number} actual + * @param {string|number} expected + * @param {boolean} ignoreCase? + * @param ignoreCase + * @param ignoreAccent + */ + contains: (actual: string | number, expected: string | number, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + if (ignoreCase) { + return !isNullOrUndefined(actual) && !isNullOrUndefined(expected) && + DataUtil.toLowerCase(actual).indexOf(DataUtil.toLowerCase(expected)) !== -1; + } + return !isNullOrUndefined(actual) && !isNullOrUndefined(expected) && + actual.toString().indexOf(expected as string) !== -1; + }, + /** + * Returns true when the actual input not contains the given string. + * + * @param {string|number} actual + * @param {string|number} expected + * @param {boolean} ignoreCase? + */ + doesNotcontain: (actual: string | number, expected: string | number, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + if (ignoreCase) { + return !isNullOrUndefined(actual) && !isNullOrUndefined(expected) && + DataUtil.toLowerCase(actual).indexOf(DataUtil.toLowerCase(expected)) === -1; + } + return !isNullOrUndefined(actual) && !isNullOrUndefined(expected) && + actual.toString().indexOf(expected as string) === -1; + }, + /** + * Returns true when the given input value is not null. + * + * @param {string|number} actual + * @returns boolean + */ + isNotNull: (actual: string | number): boolean => { + return actual !== null && actual !== undefined; + }, + /** + * Returns true when the given input value is null. + * + * @param {string|number} actual + * @returns boolean + */ + isNull: (actual: string | number): boolean => { + return actual === null || actual === undefined; + }, + /** + * Returns true when the actual input starts with the given string + * + * @param {string} actual + * @param {string} expected + * @param {boolean} ignoreCase? + * @param ignoreCase + * @param ignoreAccent + */ + startswith: (actual: string, expected: string, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + if (ignoreCase) { + return actual && expected && DataUtil.startsWith(DataUtil.toLowerCase(actual), DataUtil.toLowerCase(expected)); + } + return actual && expected && DataUtil.startsWith(actual, expected); + }, + /** + * Returns true when the actual input not starts with the given string + * + * @param {string} actual + * @param {string} expected + * @param {boolean} ignoreCase? + */ + doesNotStartWith: (actual: string, expected: string, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + if (ignoreCase) { + return actual && expected && DataUtil.notStartsWith(DataUtil.toLowerCase(actual), DataUtil.toLowerCase(expected)); + } + return actual && expected && DataUtil.notStartsWith(actual, expected); + }, + /** + * Returns true when the actual input like with the given string. + * + * @param {string} actual + * @param {string} expected + * @param {boolean} ignoreCase? + */ + like: (actual: string, expected: string, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + if (ignoreCase) { + return actual && expected && DataUtil.like(DataUtil.toLowerCase(actual), DataUtil.toLowerCase(expected)); + } + return actual && expected && DataUtil.like(actual, expected); + }, + /** + * Returns true when the given input value is empty. + * + * @param {string|number} actual + * @returns boolean + */ + isEmpty: (actual: string): boolean => { + return actual === undefined || actual === ''; + }, + /** + * Returns true when the given input value is not empty. + * + * @param {string|number} actual + * @returns boolean + */ + isNotEmpty: (actual: string): boolean => { + return actual !== undefined && actual !== ''; + }, + /** + * Returns true when the actual input pattern(wildcard) matches with the given string. + * + * @param {string|Date} actual + * @param {string} expected + * @param {boolean} ignoreCase? + */ + wildcard: (actual: string, expected: string, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + if (ignoreCase) { + return (actual || typeof actual === 'boolean') && expected && typeof actual !== 'object' && + DataUtil.wildCard(DataUtil.toLowerCase(actual), DataUtil.toLowerCase(expected)); + } + return (actual || typeof actual === 'boolean') && expected && DataUtil.wildCard(actual, expected); + }, + /** + * Returns true when the actual input ends with the given string. + * + * @param {string} actual + * @param {string} expected + * @param {boolean} ignoreCase? + * @param ignoreCase + * @param ignoreAccent + */ + endswith: (actual: string, expected: string, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + if (ignoreCase) { + return actual && expected && DataUtil.endsWith(DataUtil.toLowerCase(actual), DataUtil.toLowerCase(expected)); + } + return actual && expected && DataUtil.endsWith(actual, expected); + }, + /** + * Returns true when the actual input not ends with the given string. + * + * @param {string} actual + * @param {string} expected + * @param {boolean} ignoreCase? + */ + doesNotEndWith: (actual: string, expected: string, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expected = DataUtil.ignoreDiacritics(expected); + } + if (ignoreCase) { + return actual && expected && DataUtil.notEndsWith(DataUtil.toLowerCase(actual), DataUtil.toLowerCase(expected)); + } + return actual && expected && DataUtil.notEndsWith(actual, expected); + }, + /** + * It will return the filter operator based on the filter symbol. + * + * @param {string} operator + * @hidden + */ + processSymbols: (operator: string): string => { + const fnName: string = DataUtil.operatorSymbols[operator]; + if (fnName) { + const fn: string = DataUtil.fnOperators[fnName]; + return fn; + } + return DataUtil.throwError('Query - Process Operator : Invalid operator'); + }, + /** + * It will return the valid filter operator based on the specified operators. + * + * @param {string} operator + * @hidden + */ + processOperator: (operator: string): string => { + const fn: string = DataUtil.fnOperators[operator]; + if (fn) { return fn; } + return DataUtil.fnOperators.processSymbols(operator); + }, + /** + * Checks if the specified value exists in the given array, with optional case and accent insensitivity. + * + * @param {string | number} actual - The value to check. + * @param {Array} expectedArray - The array to search within. + * @param {boolean} [ignoreCase] - Whether to perform a case-insensitive comparison. + * @param {boolean} [ignoreAccent] - Whether to ignore accents/diacritics. + * @returns {boolean} `true` if the value is found, otherwise `false`. + */ + in: (actual: string | number | Date, expectedArray: Array, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expectedArray = >DataUtil.ignoreDiacriticsForArrays(expectedArray); + } + if (ignoreCase) { + return !isNullOrUndefined(actual) && expectedArray && expectedArray.length > 0 && expectedArray + .map(item => DataUtil.toLowerCase(item)).indexOf(DataUtil.toLowerCase(actual)) > -1; + } + if (actual instanceof Date) { + return !isNullOrUndefined(actual) && expectedArray && expectedArray.length > 0 && Array.isArray(expectedArray) && + expectedArray.some(item => item instanceof Date && (item as Date).getTime() === (actual as Date).getTime()); + } + return !isNullOrUndefined(actual) && expectedArray && expectedArray.length > 0 && expectedArray.indexOf(actual) > -1; + }, + /** + * Checks if the specified value is not present in the given array, with optional case and accent insensitivity. + * + * @param {string | number} actual - The value to check. + * @param {Array} expectedArray - The array to search within. + * @param {boolean} [ignoreCase] - Whether to perform a case-insensitive comparison. + * @param {boolean} [ignoreAccent] - Whether to ignore accents/diacritics. + * @returns {boolean} `true` if the value is not found, otherwise `false`. + */ + notin: (actual: string | number | Date, expectedArray: Array, ignoreCase?: boolean, ignoreAccent?: boolean): boolean => { + if (ignoreAccent) { + actual = DataUtil.ignoreDiacritics(actual); + expectedArray = >DataUtil.ignoreDiacriticsForArrays(expectedArray); + } + if (ignoreCase) { + return !isNullOrUndefined(actual) && expectedArray && expectedArray.length > 0 && expectedArray + .map(item => DataUtil.toLowerCase(item)).indexOf(DataUtil.toLowerCase(actual)) === -1; + } + if (actual instanceof Date) { + return !isNullOrUndefined(actual) && expectedArray && expectedArray.length > 0 && Array.isArray(expectedArray) && + expectedArray.every(item => !(item instanceof Date) || (item as Date).getTime() !== (actual as Date).getTime()); + } + return !isNullOrUndefined(actual) && expectedArray && expectedArray.length > 0 && expectedArray.indexOf(actual) === -1; + } + }; + + /** + * To perform the filter operation with specified adaptor and returns the result. + * + * @param {Object} adaptor + * @param {string} fnName + * @param {Object} param1? + * @param {Object} param2? + * @param param1 + * @param param2 + * @hidden + */ + public static callAdaptorFunction(adaptor: Object, fnName: string, param1?: Object, param2?: Object): Object { + if (fnName in adaptor) { + const res: Query = adaptor[fnName](param1, param2); + if (!isNullOrUndefined(res)) { param1 = res; } + } + return param1; + } + + public static getAddParams(adp: Object, dm: DataManager, query: Query): Object { + const req: Object = {}; + DataUtil.callAdaptorFunction(adp, 'addParams', { + dm: dm, + query: query, + params: query ? query.params : [], + reqParams: req + }); + return req; + } + /** + * To perform the parse operation on JSON data, like convert to string from JSON or convert to JSON from string. + */ + public static parse: ParseOption = { + /** + * Parse the given string to the plain JavaScript object. + * + * @param {string|Object|Object[]} jsonText + */ + parseJson: (jsonText: string | Object | Object[]): Object => { + if (typeof jsonText === 'string' && (/^[\s]*\[|^[\s]*\{(.)+:/g.test(jsonText) || jsonText.indexOf('"') === -1)) { + jsonText = JSON.parse(jsonText, DataUtil.parse.jsonReviver); + } else if (jsonText instanceof Array) { + DataUtil.parse.iterateAndReviveArray(jsonText); + } else if (typeof jsonText === 'object' && jsonText !== null) { + DataUtil.parse.iterateAndReviveJson(jsonText); + } + return jsonText; + }, + /** + * It will perform on array of values. + * + * @param {string[]|Object[]} array + * @hidden + */ + iterateAndReviveArray: (array: string[] | Object[]): void => { + for (let i: number = 0; i < array.length; i++) { + if (typeof array[i] === 'object' && array[i] !== null) { + DataUtil.parse.iterateAndReviveJson(array[i]); + // eslint-disable-next-line no-useless-escape + } else if (typeof array[i] === 'string' && (!/^[\s]*\[|^[\s]*\{(.)+:|\"/g.test(array[i]) || + array[i].toString().indexOf('"') === -1)) { + array[i] = DataUtil.parse.jsonReviver('', array[i]); + } else { + array[i] = DataUtil.parse.parseJson(array[i]); + } + } + }, + /** + * It will perform on JSON values + * + * @param {JSON} json + * @hidden + */ + iterateAndReviveJson: (json: JSON): void => { + let value: Object | string; + + const keys: string[] = Object.keys(json); + for (const prop of keys) { + if (DataUtil.startsWith(prop, '__')) { + continue; + } + + value = json[prop]; + if (typeof value === 'object') { + if (value instanceof Array) { + DataUtil.parse.iterateAndReviveArray(value); + } else if (value) { + DataUtil.parse.iterateAndReviveJson(value); + } + } else { + json[prop] = DataUtil.parse.jsonReviver(json[prop], value); + } + } + }, + /** + * It will perform on JSON values + * + * @param {string} field + * @param {string|Date} value + * @hidden + */ + jsonReviver: (field: string, value: string | Date): string | Date => { + if (typeof value === 'string') { + // eslint-disable-next-line security/detect-unsafe-regex + const ms: string[] = /^\/Date\(([+-]?[0-9]+)([+-][0-9]{4})?\)\/$/.exec(value); + const offSet: number = DataUtil.timeZoneHandling ? DataUtil.serverTimezoneOffset : null; + if (ms) { + return DataUtil.dateParse.toTimeZone(new Date(parseInt(ms[1], 10)), offSet, true); + // eslint-disable-next-line no-useless-escape, security/detect-unsafe-regex + } else if (/^(\d{4}\-\d\d\-\d\d([tT][\d:\.]*){1})([zZ]|([+\-])(\d\d):?(\d\d))?$/.test(value)) { + const isUTC: boolean = value.indexOf('Z') > -1 || value.indexOf('z') > -1; + const arr: string[] = (value).split(/[^0-9.]/); + if (isUTC) { + if (arr[5].indexOf('.') > -1) { + const secondsMs: string[] = arr[5].split('.'); + arr[5] = secondsMs[0]; + arr[6] = new Date(value).getUTCMilliseconds().toString(); + } else { + arr[6] = '00'; + } + value = DataUtil.dateParse + .toTimeZone(new Date( + parseInt(arr[0], 10), + parseInt(arr[1], 10) - 1, + parseInt(arr[2], 10), + parseInt(arr[3], 10), parseInt(arr[4], 10), parseInt(arr[5] ? arr[5] : '00', 10), parseInt(arr[6], 10)), + DataUtil.serverTimezoneOffset, false); + } else { + const utcFormat: Date = new Date( + parseInt(arr[0], 10), + parseInt(arr[1], 10) - 1, + parseInt(arr[2], 10), + parseInt(arr[3], 10), parseInt(arr[4], 10), parseInt(arr[5] ? arr[5] : '00', 10)); + const hrs: number = parseInt(arr[6], 10); + const mins: number = parseInt(arr[7], 10); + if (isNaN(hrs) && isNaN(mins)) { + return utcFormat; + } + if (value.indexOf('+') > -1) { + utcFormat.setHours(utcFormat.getHours() - hrs, utcFormat.getMinutes() - mins); + } else { + utcFormat.setHours(utcFormat.getHours() + hrs, utcFormat.getMinutes() + mins); + } + value = DataUtil.dateParse + .toTimeZone(utcFormat, DataUtil.serverTimezoneOffset, false); + } + if (DataUtil.serverTimezoneOffset == null) { + value = DataUtil.dateParse.addSelfOffset(value); + } + } + } + return value; + }, + /** + * Check wheather the given value is JSON or not. + * + * @param {Object[]} jsonData + */ + isJson: (jsonData: Object[]): Object => { + if (typeof jsonData[0] === 'string') { + return jsonData; + } + return DataUtil.parse.parseJson(jsonData); + }, + /** + * Checks wheather the given value is GUID or not. + * + * @param {string} value + */ + isGuid: (value: string): boolean => { + // eslint-disable-next-line security/detect-unsafe-regex + const regex: RegExp = /[A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12}/i; + const match: RegExpExecArray = regex.exec(value); + return match != null; + }, + /** + * The method used to replace the value based on the type. + * + * @param {Object} value + * @param {boolean} stringify + * @hidden + */ + replacer: (value: Object, stringify?: boolean): Object => { + + if (DataUtil.isPlainObject(value)) { + return DataUtil.parse.jsonReplacer(value, stringify); + } + + if (value instanceof Array) { + return DataUtil.parse.arrayReplacer(value); + } + + if (value instanceof Date) { + return DataUtil.parse.jsonReplacer({ val: value }, stringify).val; + } + return value; + }, + /** + * It will replace the JSON value. + * + * @param {string} key + * @param {Object} val + * @param stringify + * @hidden + */ + jsonReplacer: (val: Object, stringify: boolean): Object => { + let value: Date; + const keys: string[] = Object.keys(val); + for (const prop of keys) { + value = val[prop]; + + if (!(value instanceof Date)) { + continue; + } + let d: Date = value; + if (DataUtil.serverTimezoneOffset == null) { + val[prop] = DataUtil.dateParse.toTimeZone(d, null).toJSON(); + } else { + d = new Date(+d + DataUtil.serverTimezoneOffset * 3600000); + val[prop] = DataUtil.dateParse.toTimeZone(DataUtil.dateParse.addSelfOffset(d), null).toJSON(); + } + } + + return val; + }, + /** + * It will replace the Array of value. + * + * @param {string} key + * @param {Object[]} val + * @hidden + */ + arrayReplacer: (val: Object[]): Object => { + + for (let i: number = 0; i < val.length; i++) { + if (DataUtil.isPlainObject(val[i])) { + val[i] = DataUtil.parse.jsonReplacer(val[i]); + } else if (val[i] instanceof Date) { + val[i] = DataUtil.parse.jsonReplacer({ date: val[i] }).date; + } + } + + return val; + }, + /** + * It will replace the Date object with respective to UTC format value. + * + * @param {string} key + * @param {any} value + * @hidden + */ + /* eslint-disable @typescript-eslint/no-explicit-any */ + /* tslint:disable-next-line:no-any */ + jsonDateReplacer: (key: string, value: any): any => { + /* eslint-enable @typescript-eslint/no-explicit-any */ + if (key === 'value' && value) { + if (typeof value === 'string') { + // eslint-disable-next-line security/detect-unsafe-regex + const ms: string[] = /^\/Date\(([+-]?[0-9]+)([+-][0-9]{4})?\)\/$/.exec(value); + if (ms) { + value = DataUtil.dateParse.toTimeZone(new Date(parseInt(ms[1], 10)), null, true); + // eslint-disable-next-line no-useless-escape, security/detect-unsafe-regex + } else if (/^(\d{4}\-\d\d\-\d\d([tT][\d:\.]*){1})([zZ]|([+\-])(\d\d):?(\d\d))?$/.test(value)) { + const arr: string[] = (value).split(/[^0-9]/); + value = DataUtil.dateParse + .toTimeZone(new Date( + parseInt(arr[0], 10), + parseInt(arr[1], 10) - 1, + parseInt(arr[2], 10), + parseInt(arr[3], 10), parseInt(arr[4], 10), parseInt(arr[5], 10)), + null, true); + } + } + if (value instanceof Date) { + value = DataUtil.dateParse.addSelfOffset(value); + if (DataUtil.serverTimezoneOffset === null) { + return DataUtil.dateParse.toTimeZone(DataUtil.dateParse.addSelfOffset(value), null).toJSON(); + } else { + value = DataUtil.dateParse.toTimeZone(value, ((value.getTimezoneOffset() / 60) + - DataUtil.serverTimezoneOffset ), + false); + return value.toJSON(); + } + } + } + return value; + } + }; + + /** + * Checks wheather the given input is a plain object or not. + * + * @param {Object|Object[]} obj + */ + public static isPlainObject(obj: Object | Object[]): boolean { + return (!!obj) && (obj.constructor === Object); + } + + /** + * Returns true when the browser cross origin request. + */ + public static isCors(): boolean { + let xhr: XMLHttpRequest = null; + const request: string = 'XMLHttpRequest'; + try { + xhr = new window[request](); + } catch (e) { + // No exception handling + } + return !!xhr && ('withCredentials' in xhr); + } + /** + * Generate random GUID value which will be prefixed with the given value. + * + * @param {string} prefix + */ + public static getGuid(prefix: string): string { + const hexs: string = '0123456789abcdef'; + let rand: number; + return (prefix || '') + '00000000-0000-4000-0000-000000000000'.replace(/0/g, (val: string, i: number) => { + if ('crypto' in window && 'getRandomValues' in crypto) { + const arr: Uint8Array = new Uint8Array(1); + window.crypto.getRandomValues(arr); + rand = arr[0] % 16 | 0; + } else { + rand = Math.random() * 16 | 0; + } + return hexs[i === 19 ? rand & 0x3 | 0x8 : rand]; + }); + } + /** + * Checks wheather the given value is null or not. + * + * @param {string|Object} val + * @returns boolean + */ + public static isNull(val: string | Object): boolean { + return val === undefined || val === null; + } + /** + * To get the required items from collection of objects. + * + * @param {Object[]} array + * @param {string} field + * @param {Function} comparer + * @returns Object + * @hidden + */ + public static getItemFromComparer(array: Object[], field: string, comparer: Function): Object { + let keyVal: Object; + let current: Object; + let key: Object; + let i: number = 0; + const castRequired: boolean = typeof DataUtil.getVal(array, 0, field) === 'string'; + if (array.length) { + while (isNullOrUndefined(keyVal) && i < array.length) { + keyVal = DataUtil.getVal(array, i, field); + key = array[i++]; + } + } + for (; i < array.length; i++) { + current = DataUtil.getVal(array, i, field); + if (isNullOrUndefined(current)) { + continue; + } + if (castRequired) { + keyVal = +keyVal; + current = +current; + } + if (comparer(keyVal, current) > 0) { + keyVal = current; + key = array[i]; + } + } + return key; + } + + /** + * To get distinct values of Array or Array of Objects. + * + * @param {Object[]} json + * @param {string} field + * @param fieldName + * @param {boolean} requiresCompleteRecord + * @returns Object[] + * * distinct array of objects is return when requiresCompleteRecord set as true. + * @hidden + */ + public static distinct(json: Object[], fieldName: string, requiresCompleteRecord?: boolean): Object[] { + requiresCompleteRecord = isNullOrUndefined(requiresCompleteRecord) ? false : requiresCompleteRecord; + const result: Object[] = []; + let val: string; + const tmp: Object = {}; + json.forEach((data: Object, index: number) => { + val = typeof(json[index]) === 'object' ? DataUtil.getVal(json, index, fieldName) as string : json[index] as string; + if (!(val in tmp)) { + result.push(!requiresCompleteRecord ? val : json[index]); + tmp[val] = 1; + } + }); + return result; + } + /** + * @hidden + */ + public static dateParse: DateParseOption = { + addSelfOffset: (input: Date) => { + return new Date(+input - (input.getTimezoneOffset() * 60000)); + }, + toUTC: (input: Date) => { + return new Date(+input + (input.getTimezoneOffset() * 60000)); + }, + toTimeZone: (input: Date, offset?: number, utc?: boolean) => { + if (offset === null) { return input; } + const unix: Date = utc ? DataUtil.dateParse.toUTC(input) : input; + return new Date(+unix - (offset * 3600000)); + }, + toLocalTime: (input: Date) => { + const datefn: Date = input; + const timeZone: number = -datefn.getTimezoneOffset(); + const differenceString: string = timeZone >= 0 ? '+' : '-'; + const localtimefn: Function = (num: number) => { + const norm: number = Math.floor(Math.abs(num)); + return (norm < 10 ? '0' : '') + norm; + }; + const val: string = datefn.getFullYear() + '-' + localtimefn(datefn.getMonth() + 1) + '-' + localtimefn(datefn.getDate()) + + 'T' + localtimefn(datefn.getHours()) + + ':' + localtimefn(datefn.getMinutes()) + + ':' + localtimefn(datefn.getSeconds()) + + differenceString + localtimefn(timeZone / 60) + + ':' + localtimefn(timeZone % 60); + return val; + } + }; + /** + * Process the given records based on the datamanager string. + * + * @param {string} datamanager + * @param dm + * @param {Object[]} records + */ + public static processData(dm: GraphQLParams, records: Object[]): ReturnType { + const query: Query = this.prepareQuery(dm); + const sampledata: DataManager = new DataManager(records); + if (dm.requiresCounts) { + query.requiresCount(); + } + /* eslint-disable @typescript-eslint/no-explicit-any */ + // tslint:disable-next-line:no-any + const result: ReturnType | any = sampledata.executeLocal(query); + /* eslint-enable @typescript-eslint/no-explicit-any */ + const returnValue: ReturnType = { + result: dm.requiresCounts ? result.result : result, + count: result.count, + aggregates: JSON.stringify(result.aggregates) + }; + return dm.requiresCounts ? returnValue : result; + } + + private static prepareQuery(dm: GraphQLParams): Query { + const query: Query = new Query(); + + if (dm.select) { + query.select(dm.select as string[]); + } + + if (dm.where) { + const where: Predicate[] = DataUtil.parse.parseJson(dm.where); + where.filter((pred: Predicate) => { + if (isNullOrUndefined(pred.condition)) { + query.where(pred.field, pred.operator, (pred.value as string | number | boolean | Date), pred.ignoreCase, + pred.ignoreAccent); + } else { + let predicateList: Predicate[] = []; + if ((pred as Predicate).field) { + predicateList.push(new Predicate(pred.field, pred.operator, pred.value, pred.ignoreCase, + pred.ignoreAccent)); + } else { + predicateList = predicateList.concat(this.getPredicate(pred.predicates)); + } + if (pred.condition === 'or') { + query.where(Predicate.or(predicateList)); + } else if (pred.condition === 'and') { + query.where(Predicate.and(predicateList)); + } + } + }); + + } + + if (dm.search) { + const search: object[] = DataUtil.parse.parseJson(dm.search); + // tslint:disable-next-line:no-string-literal + search.filter((e: { key: string, fields: string[] }) => query.search(e.key, e.fields, e['operator'], + // tslint:disable-next-line:no-string-literal + e['ignoreCase'], e['ignoreAccent'])); + } + if (dm.aggregates) { + dm.aggregates.filter((e: { type: string, field: string }) => query.aggregate(e.type, e.field)); + } + + if (dm.sorted) { + dm.sorted.filter((e: { name: string, direction: string }) => query.sortBy(e.name, e.direction)); + } + if (dm.skip) { + query.skip(dm.skip); + } + if (dm.take) { + query.take(dm.take); + } + + if (dm.group) { + dm.group.filter((grp: string) => query.group(grp)); + } + return query; + } + + private static getPredicate(pred: Predicate[]): Predicate[] { + const mainPred: Predicate[] = []; + for (let i: number = 0; i < pred.length; i++) { + const e: Predicate = pred[i]; + if (e.field) { + mainPred.push(new Predicate(e.field, e.operator, e.value, e.ignoreCase, + e.ignoreAccent)); + } else { + const childPred: Predicate[] = []; + // tslint:disable-next-line:typedef + const cpre = this.getPredicate(e.predicates); + for (const prop of Object.keys(cpre)) { + childPred.push(cpre[prop]); + } + mainPred.push(e.condition === 'or' ? Predicate.or(childPred) : Predicate.and(childPred)); + } + } + return mainPred; + } +} + +/** + * @hidden + */ +export interface GraphQLParams { + skip?: number; + take?: number; + sorted?: {name: string, direction: string}[]; + group?: string[]; + table?: string; + select?: string[]; + where?: string; + search?: string; + requiresCounts?: boolean; + aggregates?: Aggregates[]; + params?: string; +} + +/** + * @hidden + */ +export interface Aggregates { + sum?: Function; + average?: Function; + min?: Function; + max?: Function; + truecount?: Function; + falsecount?: Function; + count?: Function; + type?: string; + field?: string; +} + +/** + * @hidden + */ +export interface Operators { + equal?: Function; + notequal?: Function; + lessthan?: Function; + greaterthan?: Function; + lessthanorequal?: Function; + greaterthanorequal?: Function; + contains?: Function; + doesNotcontain?: Function; + isNotNull?: Function; + isNull?: Function; + startswith?: Function; + doesNotStartWith?: Function; + like?: Function; + isEmpty?: Function; + isNotEmpty?: Function; + wildcard?: Function; + endswith?: Function; + doesNotEndWith?: Function; + processSymbols?: Function; + processOperator?: Function; + in?: Function; + notin?: Function; +} + +/** + * @hidden + */ +export interface Group { + GroupGuid?: string; + level?: number; + childLevels?: number; + records?: Object[]; + key?: string; + count?: number; + items?: Object[]; + aggregates?: Object; + field?: string; + result?: Object; + +} + +/** + * @hidden + */ +export interface ParseOption { + parseJson?: Function; + iterateAndReviveArray?: Function; + iterateAndReviveJson?: Function; + jsonReviver?: (key: string, value: Object) => Object; + isJson?: Function; + isGuid?: Function; + replacer?: Function; + jsonReplacer?: Function; + arrayReplacer?: Function; + /* eslint-disable @typescript-eslint/no-explicit-any */ + /* tslint:disable-next-line:no-any */ + jsonDateReplacer?: (key: string, value: any) => any; + /* eslint-enable @typescript-eslint/no-explicit-any */ +} +/** + * @hidden + */ +export interface DateParseOption { + addSelfOffset?: (input: Date) => Date; + toUTC?: (input: Date) => Date; + toTimeZone?: (input: Date, offset?: number, utc?: boolean) => Date; + toLocalTime?: (input: Date) => string; +} diff --git a/components/popups/tsconfig.json b/components/data/tsconfig.json similarity index 100% rename from components/popups/tsconfig.json rename to components/data/tsconfig.json diff --git a/components/dropdowns/CHANGELOG.md b/components/dropdowns/CHANGELOG.md new file mode 100644 index 0000000..30d5cee --- /dev/null +++ b/components/dropdowns/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## [Unreleased] \ No newline at end of file diff --git a/components/dropdowns/README.md b/components/dropdowns/README.md new file mode 100644 index 0000000..3fd51c7 --- /dev/null +++ b/components/dropdowns/README.md @@ -0,0 +1,63 @@ +# React Dropdown Components + +## What's Included in the React Dropdown Package + +The React Dropdown package includes the following list of components. + +### React Dropdown List + +The Dropdown List component provides a user-friendly interface for selecting a single option from a list of predefined values. It supports rich customization, filtering, and templating features, making it ideal for forms, data filtering, and guided user selections. + +**Key features** + +- **Primitive Data Binding:** Bind the Dropdown List to simple arrays of strings or numbers for straightforward use cases without complex data structures. + +- **Grouping:** Organize Dropdown List items into logical categories using the groupBy property, improving navigation and discoverability in large datasets. + +- **Filtering:** Enable the filterable property to allow users to search and narrow down options dynamically, with real-time updates to the list as they type. + +- **Grouping with Filtering:** Combine grouping and filtering to enhance usability, especially when dealing with extensive or categorized data. + +- **Templates:** Customize the Dropdown List appearance using various template options: + +- **Item Template:** Use itemTemplate to style individual items or display additional information. + - **Header Template:** Add custom content at the top of the Dropdown List using headerTemplate. + - **Footer Template:** Include extra information or actions at the bottom using footerTemplate. + - **Value Template:** Style the selected value using valueTemplate for a personalized display. + - **Label Mode:** Control how labels or placeholders appear with the labelMode property. Available modes include Never, Always, and Auto. + +

+Trusted by the world's leading companies + + Syncfusion logo + +

+ +## Setup + +To install `dropdowns` and its dependent packages, use the following command, + +```sh +npm install @syncfusion/react-dropdowns +``` + +## Support + +Product support is available through following mediums. + +* [Support ticket](https://support.syncfusion.com/support/tickets/create) - Guaranteed Response in 24 hours | Unlimited tickets | Holiday support +* Live chat + +## Changelog +Check the changelog [here](https://github.com/syncfusion/react-ui-components/blob/master/components/dropdowns/CHANGELOG.md). Get minor improvements and bug fixes every week to stay up to date with frequent updates. + +## License and copyright + +> This is a commercial product and requires a paid license for possession or use. Syncfusion’s licensed software, including this component, is subject to the terms and conditions of Syncfusion's [EULA](https://www.syncfusion.com/eula/es/). To acquire a license for [React UI components](https://www.syncfusion.com/react-components), you can [purchase](https://www.syncfusion.com/sales/products) or [start a free 30-day trial](https://www.syncfusion.com/account/manage-trials/start-trials). + +> A [free community license](https://www.syncfusion.com/products/communitylicense) is also available for companies and individuals whose organizations have less than $1 million USD in annual gross revenue and five or fewer developers. + +See [LICENSE FILE](https://github.com/syncfusion/react-ui-components/blob/master/license?utm_source=npm&utm_campaign=notification) for more info. + +© Copyright 2025 Syncfusion, Inc. All Rights Reserved. The Syncfusion Essential Studio license and copyright applies to this distribution. + diff --git a/components/inputs/gulpfile.js b/components/dropdowns/gulpfile.js similarity index 100% rename from components/inputs/gulpfile.js rename to components/dropdowns/gulpfile.js diff --git a/components/navigations/license b/components/dropdowns/license similarity index 100% rename from components/navigations/license rename to components/dropdowns/license diff --git a/components/splitbuttons/package.json b/components/dropdowns/package.json similarity index 65% rename from components/splitbuttons/package.json rename to components/dropdowns/package.json index 0641d1c..f68eb5e 100644 --- a/components/splitbuttons/package.json +++ b/components/dropdowns/package.json @@ -1,7 +1,7 @@ { - "name": "@syncfusion/react-splitbuttons", - "version": "30.1.37", - "description": "A package of feature-rich Pure React components such as DropDownButton, SplitButton, ProgressButton and ButtonGroup.", + "name": "@syncfusion/react-dropdowns", + "version": "31.1.17", + "description": "A package of React Dropdown components", "author": "Syncfusion Inc.", "license": "SEE LICENSE IN license", "keywords": [ @@ -9,7 +9,8 @@ "web-components", "react", "syncfusion-react", - "splitbuttons-components" + "dropdownlist", + "react-dropdownlist" ], "repository": { "type": "git", @@ -19,9 +20,12 @@ "module": "./index.js", "readme": "README.md", "dependencies": { - "@syncfusion/react-base": "~30.1.37", - "@syncfusion/react-buttons": "~30.1.37", - "@syncfusion/react-popups": "~30.1.37" + "@syncfusion/react-base": "~31.1.17", + "@syncfusion/react-buttons": "~31.1.17", + "@syncfusion/react-data": "~31.1.17", + "@syncfusion/react-inputs": "~31.1.17", + "@syncfusion/react-lists": "~31.1.17", + "@syncfusion/react-popups": "~31.1.17" }, "devDependencies": { "gulp": "4.0.2", diff --git a/components/dropdowns/src/common/drop-down-base.tsx b/components/dropdowns/src/common/drop-down-base.tsx new file mode 100644 index 0000000..d346b25 --- /dev/null +++ b/components/dropdowns/src/common/drop-down-base.tsx @@ -0,0 +1,948 @@ +import * as React from 'react'; +import { useState, useEffect, useRef, useImperativeHandle, forwardRef, useMemo, useCallback, Ref } from 'react'; +import { DataManager, Query, DataOptions } from '@syncfusion/react-data'; +import { EmitType, getUniqueID, getValue, IL10n, isNullOrUndefined, L10n, useProviderContext } from '@syncfusion/react-base'; +import { generateUL, ListItem, ListBaseOptions, groupDataSource, defaultListBaseOptions, getDataSource, addSorting, SortOrder } + from '@syncfusion/react-lists'; +import { TooltipAnimationOptions, IPopup } from '@syncfusion/react-popups'; +import { InputBase, renderClearButton } from '@syncfusion/react-inputs'; + +export const dropDownBaseClasses: DropDownBaseClassList = { + root: 'sf-dropdownbase', + content: 'sf-content', + selected: 'sf-active', + focus: 'sf-item-focus', + li: 'sf-list-item', + disabled: 'sf-disabled', + grouping: 'sf-dd-group', + hover: 'sf-hover' +}; + +export interface DropDownBaseClassList { + root: string; + content: string; + selected: string; + focus: string; + li: string; + disabled: string; + grouping: string; + hover: string; +} + +export interface FieldSettingsModel { + text?: string; + value?: string; + groupBy?: string; + disabled?: string; + htmlAttributes?: string; + icon?: string; +} + +export interface DataEventArgs { + cancel?: boolean; + data: { [key: string]: object }[] | DataManager | string[] | number[] | boolean[]; + query?: Query; +} + +export interface PopupEventArgs { + animation?: TooltipAnimationOptions; + event?: React.MouseEvent | React.KeyboardEvent | React.TouchEvent | object; + popup: IPopup; +} + +export interface FocusEventArgs { + event?: React.MouseEvent | React.FocusEvent | React.TouchEvent | React.KeyboardEvent; +} + +export interface SelectEventArgs { + item: HTMLLIElement; + itemData: FieldSettingsModel; + e: React.MouseEvent | React.KeyboardEvent | React.TouchEvent; +} + +export const defaultMappedFields: FieldSettingsModel = { + text: 'text', + value: 'value', + disabled: 'disabled', + groupBy: 'undefined', + htmlAttributes: 'htmlAttributes', + icon: 'icon' +}; + +export interface FilteringEventArgs { + preventDefaultAction: boolean; + baseEventArgs: object; + text: string; + updateData: (dataSource: DataManager | string[] | number[] | boolean[], query?: Query, fields?: FieldSettingsModel) => void; +} + +export type FilterType = 'StartsWith' | 'EndsWith' | 'Contains'; + +export interface DropDownBaseProps { + /** + * Specifies the data source for the dropdown items + */ + dataSource: { [key: string]: object }[] | DataManager | string[] | number[] | boolean[]; + + /** + * Defines mapping fields for the data items + */ + fields: FieldSettingsModel; + + /** + * Specifies the selected value + */ + value?: number | string | boolean | object | null; + + /** + * Defines the query to retrieve specific data from the data source + */ + query?: Query; + + /** + * Specifies the sort order for the data items + */ + sortOrder?: SortOrder; + + /** + * Specifies whether to ignore case while filtering or selecting items. + * + * @default true + */ + ignoreCase?: boolean; + + /** + * Specifies whether to ignore diacritics while filtering or selecting items. + * + * @default false + */ + ignoreAccent?: boolean; + + /** + * Specifies whether filtering is enabled in the dropdown base. + * When enabled, a search box appears at the top of the popup that allows users to filter items. + * + * @default false + * @private + */ + isDropdownFiltering?: boolean; + + /** + * Specifies the placeholder text for the filter input field. + * This is displayed when the filter input is empty. + * + * @default '' + */ + filterPlaceholder?: string; + + /** + * Defines the filtering type to apply when searching for items. + * Possible values are 'StartsWith', 'EndsWith', and 'Contains'. + * + * @default 'StartsWith' + */ + filterType?: FilterType; + + /** + * Defines template for rendering individual items + */ + itemTemplate?: Function | React.ReactNode; + + /** + * Defines template for rendering group headers + */ + groupTemplate?: Function | React.ReactNode; + + /** + * Defines template for header section + */ + headerTemplate?: Function | React.ReactNode; + + /** + * Defines template for footer section + */ + footerTemplate?: Function | React.ReactNode; + + /** + * Defines template when no data is available + */ + noRecordsTemplate?: Function | React.ReactNode; + + /** + * Specifies the height of the popup + */ + popupHeight?: string | number; + + /** + * Triggers when data fetching fails + */ + actionFailure?: (e: object) => void; + + /** + * Triggers when an item is clicked + */ + onItemClick?: (e: React.MouseEvent, index: number) => void; + + /** + * Handles keyboard actions + */ + keyActionHandler?: (e: React.KeyboardEvent) => void; + + /** + * Triggers on typing a character in the filter bar when the filtering is enabled. + */ + onFilterChange?: EmitType; + + /** + * Callback that triggers when remote data loading completes + * + * @private + */ + onDataLoaded?: () => void; + + /** + * Triggers after data is fetched successfully from the remote server. + */ + actionComplete?: EmitType; +} + +export interface IDropDownBase { + /** + * Gets formatted value based on type + */ + getFormattedValue(value: string | number | boolean): string | number | boolean | []; + + /** + * Gets data object by value + */ + getDataByValue(value: string | number | boolean): { [key: string]: object } | string | number | boolean | undefined; + + /** + * Gets index by value + */ + getIndexByValue(value: string | number | boolean): number; + + /** + * Gets text by value + */ + getTextByValue(value: string | number | boolean): string; + + /** + * Gets all list items + */ + getListItems(): HTMLLIElement[]; + + /** + * To filter the data from given data source by using query + */ + filter(dataSource: { [key: string]: Object }[] | DataManager | string[] | number[] | boolean[], + query?: Query, fields?: FieldSettingsModel): void; +} + +type IDropDownBaseProps = DropDownBaseProps & Omit, keyof DropDownBaseProps>; + +/** + * DropDownBase provides core functionality for dropdown-type components + */ +export const DropDownBase: React.ForwardRefExoticComponent> = + forwardRef((props: IDropDownBaseProps, ref: Ref) => { + const { + dataSource = [], + fields = defaultMappedFields, + value, + query = new Query(), + isDropdownFiltering = false, + filterPlaceholder = '', + ignoreCase, + ignoreAccent, + filterType = 'StartsWith', + itemTemplate, + groupTemplate, + headerTemplate, + footerTemplate, + sortOrder = SortOrder.None, + noRecordsTemplate, + popupHeight, + id = getUniqueID('dropdownlist'), + actionFailure, + onItemClick, + onFilterChange, + onDataLoaded, + actionComplete + } = props; + + const [listData, setListDatas] = useState<{ [key: string]: object }[] | boolean[] | string[] | number[]>([]); + const [isRequesting, setIsRequesting] = useState(false); + const [isDataInitialized, setIsDataInitialized] = useState(false); + const [, setListBaseOptions] = useState(defaultListBaseOptions); + const { locale } = useProviderContext(); + const localeStrings: Record = { noRecordsTemplate: 'No Records Found' }; + const l10nInstance: IL10n = L10n('drop-down-list', localeStrings, locale || 'en-US'); + const [selectedItemIndex, setSelectedItemIndex] = useState(null); + const [focusedItemIndex, setFocusedItemIndex] = useState(null); + const listItemsRef: React.RefObject<(HTMLLIElement | null)[]> = useRef<(HTMLLIElement | null)[]>([]); + const filterInputElementRef: React.RefObject = useRef(null); + const [typedString, setTypedString] = useState(''); + const [, setIsDataFetched] = useState(false); + const [isCustomFilter, setIsCustomFilter] = useState(false); + const typedStringRef: React.RefObject = useRef(typedString); + const containerRef: React.RefObject = useRef(null); + + useEffect(() => { + if (value != null && listData.length > 0) { + const itemIndex: number = listData.findIndex((item: string | number | boolean | {[key: string]: object}) => { + if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') { + return String(item) === String(value); + } else { + return String(getValue(fields.value ?? '', item)) === String(value); + } + }); + + if (itemIndex !== -1) { + setSelectedItemIndex(itemIndex); + } + } + }, [value, listData, fields]); + + useEffect(() => { + if (typedString !== null) { + typedStringRef.current = typedString; + setListData(dataSource, query); + } + }, [typedString]); + + useEffect(() => { + if (isDropdownFiltering && filterInputElementRef.current) { + filterInputElementRef.current.focus(); + } + }, [isDropdownFiltering, filterInputElementRef.current]); + + const typeOfData: (items: (string | number | boolean | { [key: string]: object })[]) => + { typeof: string | null, item: string | number | boolean | { [key: string]: object } | null } = + useCallback((items: (string | number | boolean | { [key: string]: object })[]) => { + let item: { + typeof: string | null; + item: string | number | boolean | { [key: string]: object } | null; + } = { typeof: null, item: null }; + if (!Array.isArray(items) || items.length === 0) { + return item; + } + for (let i: number = 0; i < items.length; i++) { + if (items[i as number] !== null && items[i as number] !== undefined) { + const listDataType: boolean = typeof items[i as number] === 'string' || + typeof items[i as number] === 'number' || typeof items[i as number] === 'boolean'; + const isNullData: boolean = listDataType ? isNullOrUndefined(items[i as number]) : + isNullOrUndefined(getValue((fields.value as string), items[i as number])); + if (!isNullData) { + return item = { typeof: typeof items[i as number], item: items[i as number] }; + } + } + } + return item; + }, [fields]); + + const getFormattedValue: (value: string | number | boolean | []) => string | number | boolean = + useCallback((value: string | number | boolean | []): string | number | boolean => { + if (listData && listData.length) { + const item: { [key: string]: object } & + { typeof?: string; item?: string | number | boolean | { [key: string]: object } | null } = + typeOfData(listData) as { [key: string]: object } & + { typeof?: string; item?: string | number | boolean | { [key: string]: object } | null }; + if (typeof getValue((fields.value as string), item.item as { [key: string]: object }) === 'number' || + item.typeof === 'number') { + return parseFloat(value as string); + } + if (typeof getValue((fields.value as string), item.item as { [key: string]: object }) === 'boolean' || + item.typeof === 'boolean') { + return ((value === 'true') || ('' + value === 'true')); + } + } + return value as string | number | boolean; + }, [listData, fields, typeOfData]); + + const getDataByValue: (value: string | number | boolean) => { [key: string]: Object } | string | number | boolean | undefined = + useCallback((value: string | number | boolean) => { + if (!isNullOrUndefined(listData)) { + const type: string = typeOfData(listData).typeof as string; + if (type === 'string' || type === 'number' || type === 'boolean') { + for (let i: number = 0; i < listData.length; i++) { + const item: { [key: string]: Object } | string | number | boolean = listData[i as number]; + if (isNullOrUndefined(item)) {continue; } + if (String(item) === String(value)) { + return item; + } + } + } else { + for (let i: number = 0; i < listData.length; i++) { + const item: { [key: string]: Object } | string | number | boolean = listData[i as number]; + if (isNullOrUndefined(item)) {continue; } + if (String(getValue((fields.value as string), item)) === String(value)) { + return item; + } + } + } + } + return undefined; + }, [listData, fields, typeOfData]); + + const checkIgnoreCase: (item: string, text: string) => boolean = useCallback((item: string, text: string): boolean => { + if (isNullOrUndefined(item)) { + return false; + } + return String(item).toLowerCase() === text?.toString().toLowerCase(); + }, []); + + const checkValueCase: ( + text: string, + ignoreCase: boolean, + isTextByValue?: boolean + ) => string | number | null = useCallback(( + text: string, + ignoreCase: boolean, + isTextByValue?: boolean + ): string | number | null => { + let value: string | number | null = null; + if (isTextByValue) { + value = text; + } + if (!isNullOrUndefined(listData)) { + const dataSource: { [key: string]: object }[] = listData as { [key: string]: object }[]; + if (ignoreCase) { + dataSource.filter((item: { [key: string]: object }) => { + const itemValue: string | number = getValue(fields.value as string, item); + if (!isNullOrUndefined(itemValue) && + checkIgnoreCase(getValue(fields.text || fields.value || '', item).toString(), text)) { + value = getValue(fields.value as string, item) as string; + } + }); + } else { + if (isTextByValue) { + const compareValue: string | number | null = value; + dataSource.filter((item: { [key: string]: object }) => { + const itemValue: string | number = getValue(fields.value as string, item); + if (!isNullOrUndefined(itemValue) && !isNullOrUndefined(value) && + compareValue != null && itemValue.toString() === compareValue.toString()) { + value = getValue(fields.text as string, item) as string; + } + }); + } + } + } + return value; + }, [ + listData, + fields, + typeOfData + ]); + + const getTextByValue: (value: string | number | boolean) => string = useCallback((value: string | number | boolean): string => { + const dataType: string = typeOfData(dataSource as (string | number | boolean | { [key: string]: object })[]).typeof as string; + if (dataType === 'string' || dataType === 'number' || dataType === 'boolean') { + return value.toString(); + } + + return checkValueCase(value as string, ignoreCase ? true : false, true) as string || ''; + }, [checkValueCase, typeOfData, dataSource]); + + const getQuery: (newQuery?: Query) => Query = useCallback((newQuery?: Query): Query => { + let filterQuery: Query; + if (!isCustomFilter && isDropdownFiltering && ((typedStringRef.current) || filterInputElementRef.current)) { + filterQuery = (newQuery as Query).clone(); + const filteringType: string = typedStringRef.current === '' ? 'contains' : filterType; + const dataType: string | null = typeOfData(dataSource as (string | number | boolean | { [key: string]: object })[]).typeof; + if (!(dataSource instanceof DataManager) && (dataType === 'string' || dataType === 'number')) { + filterQuery.where('', filteringType, typedStringRef.current, ignoreCase, ignoreAccent); + } else if ((isDropdownFiltering && typedStringRef.current !== '')) { + const field: string = (fields?.text) ? fields.text : ''; + filterQuery.where(field, filteringType, typedStringRef.current, ignoreCase, ignoreAccent); + } + } else { + filterQuery = (newQuery as Query).clone(); + } + return filterQuery.clone(); + }, [query, isCustomFilter, isDropdownFiltering, filterType, ignoreCase, ignoreAccent, fields]); + + const handleItemClick: (e: React.MouseEvent, index: number) => void = + useCallback((e: React.MouseEvent, index: number): void => { + const value: string | null = e.currentTarget.getAttribute('data-value'); + if (value) { + let itemData: { [key: string]: Object } | string | number | boolean | null | undefined; + const dataType: string = typeOfData(dataSource as (string | number | boolean | { [key: string]: object })[]) + .typeof as string; + if (dataType === 'string' || dataType === 'number' || dataType === 'boolean') { + itemData = value; + } else { + itemData = getDataByValue(getFormattedValue(value)); + } + if (itemData) { + getTextByValue(value); + } + } + setSelectedItemIndex(index); + setFocusedItemIndex(null); + onItemClick?.(e, index); + }, [getDataByValue, getFormattedValue, getTextByValue, onItemClick, typeOfData, dataSource]); + + const setItemFocus: () => void = useCallback((): void => { + if (listData.length > 0) { + let isSelectedItem: boolean = false; + + if (value != null) { + const itemIndex: number = listData.findIndex((item: string | number | boolean | { + [key: string]: object + }) => + getValue(fields.value ?? '', item) === value); + if (itemIndex !== -1) { + setSelectedItemIndex(itemIndex); + isSelectedItem = true; + setFocusedItemIndex(null); + } + } + + if (!isSelectedItem) { + const firstEnabledIndex: number = listData.findIndex((item: string | number | boolean | { + [key: string]: object + }) => + !getValue(fields.disabled ?? '', item)); + if (firstEnabledIndex !== -1) { + setFocusedItemIndex(firstEnabledIndex); + } + } + } + }, [value, fields, listData]); + + const setListData: ( + dataSource: { [key: string]: object }[] | string[] | number[] | DataManager | boolean[], + query?: Query + ) => void = useCallback(( + dataSource: { [key: string]: object }[] | string[] | number[] | DataManager | boolean[], + query?: Query + ): void => { + let filteredDataSource: typeof dataSource; + if (Array.isArray(dataSource)) { + filteredDataSource = dataSource.filter((item: string | number | boolean | {[key: string]: object}) => + item !== null && item !== undefined) as typeof dataSource; + } else { + filteredDataSource = dataSource; + } + const eventArgs: DataEventArgs = { cancel: false, data: filteredDataSource, query: query }; + if (!isRequesting && !eventArgs.cancel) { + setIsRequesting(true); + if (dataSource instanceof DataManager) { + (eventArgs.data as DataManager).executeQuery(getQuery(eventArgs.query || new Query())) + .then((e: Record) => { + if (!e.cancel) { + const completeEventArgs: { result: Array<{ [key: string]: object } | string | number | boolean>; + query: Query } = { + result: e.result, + query: getQuery(eventArgs.query || new Query()) + }; + actionComplete?.(completeEventArgs); + processDataResult(completeEventArgs.result); + } + setIsRequesting(false); + onDataLoaded?.(); + }) + .catch((e: object) => { + setIsRequesting(false); + actionFailure?.(e); + }); + } else { + const dataManager: DataManager = new DataManager(eventArgs.data as DataOptions | JSON[]); + const listItems: { [key: string]: object }[] = + getQuery(eventArgs.query || new Query()).executeLocal(dataManager) as { [key: string]: object }[]; + actionComplete?.({ result: listItems, query: getQuery(eventArgs.query || new Query()) }); + processDataResult(listItems); + setIsRequesting(false); + } + } + }, [ + isRequesting, + getQuery, + sortOrder, + actionFailure, + onDataLoaded, + actionComplete + ]); + + const listbaseClasses: string = useMemo(() => { + const isPrimitiveArray: boolean = Array.isArray(listData) && listData.length > 0 && + (typeof listData[0] === 'string' || + typeof listData[0] === 'number' || + typeof listData[0] === 'boolean'); + return [ + dropDownBaseClasses.content, + dropDownBaseClasses.root, + (!isPrimitiveArray && fields.groupBy) ? dropDownBaseClasses.grouping : '', + listData.length === 0 ? 'sf-nodata' : '' + ].filter(Boolean).join(' '); + }, [fields.groupBy, listData]); + + useEffect(() => { + const updatedOptions: ListBaseOptions = { + fields: fields, + parentClass: listbaseClasses, + hoverIndex: -1, + itemTemplate: itemTemplate, + groupTemplate: groupTemplate, + ariaAttributes: { + itemRole: 'listitem', + listRole: 'list', + itemText: '', + groupItemRole: 'presentation', + wrapperRole: 'presentation' + } + }; + setListBaseOptions(updatedOptions); + }, [fields, itemTemplate, groupTemplate, sortOrder, listbaseClasses]); + + useEffect(() => { + if (!isDataInitialized) { + setListData(dataSource, query); + } + }, [dataSource, fields, query, isDataInitialized]); + + useEffect(() => { + setItemFocus(); + }, [setItemFocus]); + + const processDataResult: (result: Array<{ [key: string]: object } | string | number | boolean>) => + void = useCallback((result: Array<{ [key: string]: object } | string | number | boolean>): void => { + if (!isDataInitialized) { + setIsDataInitialized(true); + } + const isPrimitiveArray: boolean = result.length > 0 && + (typeof result[0] === 'string' || typeof result[0] === 'number' || typeof result[0] === 'boolean'); + if (isPrimitiveArray || !fields.groupBy) { + if (isPrimitiveArray && sortOrder !== SortOrder.None) { + const sortedResult: (string | number | boolean | {[key: string]: object})[] = [...result]; + if (sortOrder === SortOrder.Ascending) { + sortedResult.sort((a: string | number | boolean | { [key: string]: object }, + b: string | number | boolean | { [key: string]: object }) => + String(a).localeCompare(String(b)) + ); + } else if (sortOrder === SortOrder.Descending) { + sortedResult.sort((a: string | number | boolean | { [key: string]: object }, + b: string | number | boolean | { [key: string]: object }) => + String(b).localeCompare(String(a)) + ); + } + setListDatas(sortedResult as typeof listData); + } else { + const sortQuery: Query = addSorting(sortOrder, fields?.text || 'text', query); + setListDatas(getDataSource(result as { [key: string]: object }[], sortQuery)); + } + } else { + setListDatas(groupDataSource(result as { [key: string]: object }[], fields, sortOrder)); + } + }, [sortOrder, fields, isDataInitialized, setIsDataInitialized, query]); + + const listBaseOptions: ListBaseOptions = useMemo(() => ({ + fields: fields, + parentClass: listbaseClasses, + hoverIndex: -1, + itemTemplate: itemTemplate, + groupTemplate: groupTemplate, + itemClick: handleItemClick, + sortOrder: sortOrder, + ariaAttributes: { + itemRole: 'option', + listRole: 'listbox', + itemText: '', + groupItemRole: 'presentation', + wrapperRole: 'presentation' + } + }), [fields, listbaseClasses, itemTemplate, groupTemplate, handleItemClick, sortOrder]); + + + const listItems: React.ReactElement[] = useMemo(() => { + listItemsRef.current = new Array(listData.length).fill(null); + const activeValues: Set = new Set(); + return (listData as ({ [key: string]: object } | string | number)[]).map( + (item: { [key: string]: object } | string | number, index: number) => { + const actualIndex: number = index; + let isSelected: boolean = false; + let itemValue: string = ''; + if (value !== null) { + if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') { + itemValue = String(item); + isSelected = String(item) === String(value); + } else { + itemValue = String(getValue(fields.value || '', item)); + isSelected = itemValue === String(value); + } + if (isSelected && activeValues.has(itemValue)) { + isSelected = false; + } + + if (isSelected) { + activeValues.add(itemValue); + } + } + const isActive: boolean = isSelected || index === selectedItemIndex; + const isFocused: boolean = index === focusedItemIndex && index !== selectedItemIndex; + const isDisabled: string | boolean | undefined = fields.disabled && + typeof item === 'object' && + getValue(fields.disabled, item) === true; + + const itemClassName: string = [ + isActive ? dropDownBaseClasses.selected : '', + isFocused ? dropDownBaseClasses.focus : '', + isDisabled ? dropDownBaseClasses.disabled : '' + ].filter(Boolean).join(' '); + + return ( + ) => listBaseOptions.itemClick?.(e, index)} + onKeyDown={(e: React.KeyboardEvent) => + listBaseOptions.itemKeyDown?.(e, index)} + aria-selected={isActive ? 'true' : 'false'} + aria-disabled={isDisabled ? 'true' : 'false'} + data-id={getUniqueID()} + listItemRef={(el: HTMLLIElement | null) => { listItemsRef.current[index as number] = el; + }} + /> + ); + } + ); + }, [ + listData, + listBaseOptions, + selectedItemIndex, + focusedItemIndex, + value, + fields, + dropDownBaseClasses + ]); + + const getListItems: () => HTMLLIElement[] = useCallback((): HTMLLIElement[] => { + if ((fields.groupBy || itemTemplate) && containerRef.current) { + return Array.from(containerRef.current.getElementsByClassName(dropDownBaseClasses.li)) as HTMLLIElement[]; + } + return listItemsRef.current.filter(Boolean) as HTMLLIElement[]; + }, [fields.groupBy, itemTemplate, dropDownBaseClasses.li]); + + const getIndexByValue: (value: string | number | boolean) => number = useCallback((value: string | number | boolean): number => { + let index: number = -1; + const listItems: HTMLLIElement[] = getListItems(); + for (let i: number = 0; i < listItems.length; i++) { + const itemValue: string | null = listItems[i as number].getAttribute('data-value'); + if (!isNullOrUndefined(value) && !isNullOrUndefined(itemValue) && + String(itemValue) === String(value)) { + index = i; + break; + } + } + return index; + }, [getListItems]); + + const ulListContainer: React.ReactElement = useMemo(() => { + return generateUL(listItems, listBaseOptions); + }, [listItems, listBaseOptions]); + + const renderHeader: React.ReactNode = useMemo(() => { + if (React.isValidElement(headerTemplate)) { + return headerTemplate; + } + if (typeof headerTemplate === 'function') { + return (headerTemplate as () => React.ReactNode)(); + } + return null; + }, [headerTemplate]); + + const renderFooter: React.ReactNode = useMemo(() => { + if (React.isValidElement(footerTemplate)) { + return footerTemplate; + } + if (typeof footerTemplate === 'function') { + return (footerTemplate as () => React.ReactNode)(); + } + return null; + }, [footerTemplate]); + + const renderNoRecords: React.ReactNode = useMemo(() => { + if (noRecordsTemplate === null) { + return null; + } + if (React.isValidElement(noRecordsTemplate)) { + return noRecordsTemplate; + } + if (typeof noRecordsTemplate === 'function') { + return (noRecordsTemplate as () => React.ReactNode)(); + } + return ( +
+ {l10nInstance.getConstant('noRecordsTemplate')} +
+ ); + }, [noRecordsTemplate, l10nInstance]); + + useImperativeHandle(ref, () => ({ + getFormattedValue, + getDataByValue, + getIndexByValue, + getTextByValue, + getListItems, + filterType, + filter + } as IDropDownBase), [ + getFormattedValue, + getDataByValue, + getIndexByValue, + getTextByValue, + getListItems, + listData + ]); + + const filterInputAttributes: { [key: string]: string } = { + autoComplete: 'off', + autoCapitalize: 'off', + spellCheck: 'false', + 'aria-expanded': 'false', + 'aria-disabled': 'false', + role: 'combobox' + }; + + const onFilterUp: (e: React.KeyboardEvent) => + void = (e: React.KeyboardEvent) => { + if (props.keyActionHandler) {props.keyActionHandler(e); } + }; + + const filter: ( + dataSource: { [key: string]: Object }[] | DataManager | string[] | number[] | boolean[], + query?: Query, + fields?: FieldSettingsModel + ) => void = ( + dataSource: { [key: string]: Object }[] | DataManager | string[] | number[] | boolean[], + query?: Query, + fields?: FieldSettingsModel + ): void => { + setIsCustomFilter(true); + const currentValue: string = filterInputElementRef.current?.value || typedString; + if (currentValue) { + const filterQuery: Query = query || new Query(); + if (!query) { + filterQuery.where( + fields?.text || 'text', + 'contains', + currentValue, + ignoreCase + ); + } + filteringAction(dataSource, filterQuery); + } else { + filteringAction(dataSource, query); + } + }; + + + const filteringAction: ( + dataSource: { [key: string]: Object }[] | DataManager | string[] | number[] | boolean[], + query?: Query | null + ) => void = ( + dataSource: { [key: string]: Object }[] | DataManager | string[] | number[] | boolean[], + query?: Query | null + ): void => { + if (filterInputElementRef.current != null && filterInputElementRef.current.value.trim() !== '') { + const filterQuery: Query = query || new Query(); + if (!query) { + const isPrimitive: boolean = Array.isArray(dataSource) && dataSource.length > 0 && + (typeof dataSource[0] === 'string' || typeof dataSource[0] === 'number' || typeof dataSource[0] === 'boolean'); + if (isPrimitive) { + filterQuery.where('', filterType, filterInputElementRef.current.value, ignoreCase, ignoreAccent); + } else { + filterQuery.where( + fields.text || 'text', + filterType, + filterInputElementRef.current.value, + ignoreCase, + ignoreAccent + ); + } + } + setListData(dataSource, filterQuery); + } else { + setListData(dataSource, query || new Query()); + } + }; + + const searchLists: (e: KeyboardEvent | MouseEvent, filterValue?: string) => void = + (e: KeyboardEvent | MouseEvent, filterValue?: string): void => { + setIsDataFetched(false); + if (isDropdownFiltering && (filterInputElementRef.current != null)) { + setIsRequesting(false); + const eventArgs: FilteringEventArgs = { + preventDefaultAction: false, + text: filterValue as string, + updateData: (dataSource: DataManager | string[] | number[] | boolean[], query?: Query) => { + setIsCustomFilter(true); + filteringAction(dataSource, query); + }, + baseEventArgs: e + }; + if (onFilterChange) { + onFilterChange(eventArgs); + } + if (!isCustomFilter && !eventArgs.preventDefaultAction) { + filteringAction(dataSource, null); + } + } + }; + + const clearText: () => void = (): void => { + setTypedString(''); + }; + + return ( + <> + {isDropdownFiltering && !isRequesting && ( + + + } + type='text' + value={typedString} + className={'sf-input-filter'} + placeholder={filterPlaceholder} + aria-controls={`${id}_filter`} + aria-autocomplete="list" + onChange={(e: React.ChangeEvent) => { + setTypedString(e.target.value); + searchLists(e as unknown as KeyboardEvent, e.target.value); + }} + onKeyUp={(e: React.KeyboardEvent) => onFilterUp(e)} + {...filterInputAttributes} + /> + {renderClearButton(typedString, () => clearText())} + + + )} + {!isRequesting && ( +
+
+ {renderHeader} +
+ {(listData.length === 0 && !(dataSource instanceof DataManager)) ? renderNoRecords : ulListContainer} +
+ {renderFooter} +
+
+ )} + + ); + }); + +export default React.memo(DropDownBase); diff --git a/components/dropdowns/src/common/index.ts b/components/dropdowns/src/common/index.ts new file mode 100644 index 0000000..981ffa7 --- /dev/null +++ b/components/dropdowns/src/common/index.ts @@ -0,0 +1,4 @@ +/** + * dropdown base modules + */ +export * from './drop-down-base'; diff --git a/components/dropdowns/src/drop-down-list/drop-down-list.tsx b/components/dropdowns/src/drop-down-list/drop-down-list.tsx new file mode 100644 index 0000000..cb6d286 --- /dev/null +++ b/components/dropdowns/src/drop-down-list/drop-down-list.tsx @@ -0,0 +1,1265 @@ +import { useState, useEffect, useRef, forwardRef, useMemo, useCallback, useImperativeHandle, JSX } from 'react'; +import * as React from 'react'; +import { createPortal } from 'react-dom'; +import { Popup, CollisionType, IPopup, Spinner, SpinnerType } from '@syncfusion/react-popups'; +import { CLASS_NAMES, LabelMode, InputBase, renderClearButton, renderFloatLabelElement, + validationProps } from '@syncfusion/react-inputs'; +import { DataManager, Query } from '@syncfusion/react-data'; +import { Browser, EmitType, formatUnit, getUniqueID, getValue, IL10n, isNullOrUndefined, L10n, preRender, useProviderContext } from '@syncfusion/react-base'; +import { ChevronDownFillIcon } from '@syncfusion/react-icons'; +import { DropDownBase, IDropDownBase, SelectEventArgs, PopupEventArgs, + FieldSettingsModel, FilterType, FilteringEventArgs} from '../common/drop-down-base'; +import { SortOrder } from '@syncfusion/react-lists'; +export { LabelMode }; + +export interface ChangeEventArgs{ + value: number | string | boolean | object | null; + previousItemData: FieldSettingsModel | string | number | boolean | { [key: string]: unknown } | null; + event: React.MouseEvent | React.KeyboardEvent; +} + +export interface DropDownListProps extends validationProps { + /** + * Specifies whether to show a clear button in the DropDownList component. When enabled, a clear icon appears when a value is selected, allowing users to clear the selection. + * + * @default false + */ + clearButton?: boolean | React.ReactNode; + + /** + * Defines the width of the dropdown popup list. The width can be specified in pixels or percentage. + * + * @default '100%' + */ + popupWidth?: string; + + /** + * Defines the height of the dropdown popup list. The height can be specified in pixels or as 'auto' to adjust based on content. + * + * @default 'auto' + */ + popupHeight?: string; + + /** + * Sets the placeholder text that appears in the DropDownList when no item is selected. + * + * @default - + */ + placeholder?: string; + + /** + * Specifies the query to retrieve data from the data source. This is useful when working with DataManager for complex data operations. + * + * @default null + */ + query?: Query; + + /** + * Specifies the value to be selected in the DropDownList component. This can be a primitive value or an object based on the configured data binding. + * + * @default null + */ + value?: number | string | boolean | object | null; + + /** + * Provides the data source for populating the dropdown items. Accepts various data formats including array of objects, primitive arrays, or DataManager. + * + * @default [] + */ + dataSource?: { [key: string]: object }[] | DataManager | string[] | number[] | boolean[]; + + /** + * Configures the mapping fields for text and value properties in the data source objects. Helps in binding complex data structures to the dropdown. + * + * @default { text: 'text', value: 'value' } + */ + fields?: FieldSettingsModel; + + /** + * Sets the z-index value for the dropdown popup, controlling its stacking order relative to other elements on the page. + * + * @default 1000 + */ + zIndex?: number; + + /** + * Enables binding of complex objects as values instead of primitive values. When enabled, the entire object can be accessed in events. + * + * @default false + */ + allowObjectBinding?: boolean; + + /** + * Specifies whether the dropdown popup is open or closed. + * + * @default false + */ + open?: boolean; + + /** + * Sets the default value of the DropDownList. Similar to the native select HTML element. + * + * @default null + */ + defaultValue?: number | string | boolean | object | null; + + /** + * Determines whether disabled items in the DropDownList should be skipped during keyboard navigation. When set to true, + * keyboard navigation will bypass disabled items, moving to the next enabled item in the list. + * + * @default true + */ + skipDisabledItems?: boolean; + + /** + * Specifies the behavior of the floating label associated with the DropDownList input. Determines when and how the label appears. + * + * @default 'Never' + */ + labelMode?: LabelMode; + + /** + * Specifies whether the DropDownList should ignore case while filtering or selecting items. + * + * @default true + */ + ignoreCase?: boolean; + + /** + * Specifies whether to ignore diacritics while filtering or selecting items. + * + * @default false + */ + ignoreAccent?: boolean; + + /** + * Specifies whether filtering should be allowed in the DropDownList. + * + * @default false + */ + filterable?: boolean; + + /** + * Specifies the type of filtering to be applied. + * + * @default 'StartsWith' + */ + filterType?: FilterType; + + /** + * Specifies the placeholder text to be shown in the filter bar of the DropDownList. + * + * @default null + */ + filterPlaceholder?: string; + + /** + * Specifies the sort order for the DropDownList items. + * + * @default 'None' + */ + sortOrder?: SortOrder; + + /** + * Specifies whether the component is in loading state. + * When true, a spinner icon replaces the default caret icon. + * + * @default false + */ + loading?: boolean; + + /** + * Provides a custom template for rendering each item in the dropdown list, allowing for customized appearance of list items. + * + * @default null + */ + itemTemplate?: Function | React.ReactNode; + + /** + * Provides a custom template for rendering the header section of the dropdown popup, enabling additional content above the item list. + * + * @default null + */ + headerTemplate?: Function | React.ReactNode; + + /** + * Provides a custom template for rendering the footer section of the dropdown popup, enabling additional content below the item list. + * + * @default null + */ + footerTemplate?: Function | React.ReactNode; + + /** + * Provides a custom template for rendering group header sections when items are categorized into groups in the dropdown list. + * + * @default null + */ + groupTemplate?: Function | React.ReactNode; + + /** + * Provides a custom template for rendering the selected value in the input element, allowing for customized appearance of the selection. + * + * @default null + */ + valueTemplate?: Function | React.ReactNode; + + /** + * Provides a custom template for the message displayed when no items match the search criteria or when the data source is empty. + * + * @default 'No Records Found' + */ + noRecordsTemplate?: Function | React.ReactNode; + + /** + * Triggers when an item in the dropdown list is selected, providing details about the selected item. + * + * @event select + */ + onSelect?: EmitType; + + /** + * Triggers when the selected value of the DropDownList changes, providing details about the new and previous values. + * + * @event change + */ + onChange?: EmitType; + + /** + * Triggers when the dropdown popup opens, allowing for custom actions to be performed at that moment. + * + * @event open + */ + onOpen?: EmitType; + + /** + * Triggers when the dropdown popup closes, allowing for custom actions to be performed at that moment. + * + * @event close + */ + onClose?: EmitType; + + /** + * Triggers when data fetching fails + */ + actionFailure?: EmitType; + + /** + * Triggers on typing a character in the filter bar when the filtering is enabled. + */ + onFilterChange?: EmitType; + + /** + * Triggers after data is fetched successfully from the remote server. + */ + actionComplete?: EmitType; +} + +export interface IDropDownList extends DropDownListProps { + /** + * Gets all list items from the dropdown list. + * + * @returns Array of HTMLLIElement + * @private + */ + getItems(): HTMLLIElement[]; + + /** + * Specifies the DOM element of the component. + * + * @private + */ + element?: HTMLElement | null; + + /** + * To filter the data from given data source by using query + */ + filter(dataSource: { [key: string]: Object }[] | DataManager | string[] | number[] | boolean[], + query?: Query, fields?: FieldSettingsModel): void; +} + +type IDropDownListProps = DropDownListProps & Omit, keyof DropDownListProps>; + +export const DropDownList: React.ForwardRefExoticComponent> = + forwardRef((props: IDropDownListProps, ref: React.Ref) => { + const { + dataSource = [], + query, + fields = { text: 'text', value: 'value', groupBy: 'groupBy', disabled: 'disabled' }, + value = null, + placeholder = '', + id = getUniqueID('dropdownlist'), + zIndex = 1000, + disabled = false, + readOnly = false, + popupWidth = '100%', + popupHeight = '300px', + allowObjectBinding = false, + labelMode = 'Never', + open, + skipDisabledItems = true, + defaultValue = null, + ignoreCase = true, + ignoreAccent = false, + filterable = false, + filterType = 'StartsWith', + filterPlaceholder = '', + sortOrder = SortOrder.None, + loading = false, + itemTemplate, + headerTemplate, + footerTemplate, + groupTemplate, + valueTemplate, + noRecordsTemplate, + clearButton = false, + valid, + validationMessage = '', + validityStyles = true, + required, + className, + onSelect, + onChange, + onOpen, + onClose, + actionFailure, + onFilterChange, + actionComplete, + ...otherProps + } = props; + + const [isPopupOpen, setIsPopupOpen] = useState(false); + const [dropdownValue, setDropdownValue] = useState(value ?? defaultValue ?? null); + const [textValue, setTextValue] = useState(''); + const [isSpanFocused, setIsSpanFocused] = useState(false); + const [, setSelectedLI] = useState(null); + const [, setPreviousValue] = useState(value); + const [itemData, setItemData] = useState(null); + const [previousItemData, setPreviousItemData] = useState(null); + const [activeIndex, setActiveIndex] = useState(null); + const [changeEvent, setChangeEvent] = useState | React.KeyboardEvent | null>(null); + const [isSelected] = useState(false); + const [isTyped, setIsTyped] = useState(false); + const [ariaExpanded, setAriaExpanded] = useState(false); + const [isFullPagePopup, setIsFullPagePopup] = useState(false); + const [isInputValid, setIsInputValid] = useState((valid !== undefined) ? valid : (required) ? dropdownValue != null : true); + const isOpenControlled: boolean = open !== undefined; + const [isLoading, setIsLoading] = useState(loading); + const [isDataLoading, setIsDataLoading] = useState(false); + const [shouldShowPopup, setShouldShowPopup] = useState(false); + const [listKey, setListKey] = useState(0); + + const spanElementRef: React.RefObject = useRef(null); + const inputElementRef: React.RefObject = useRef(null); + const popupRef: React.RefObject = useRef(null); + const dropdownbaseRef: React.RefObject = useRef(null); + const spinnerTargetRef: React.RefObject = useRef(null); + const { dir, locale } = useProviderContext(); + const spinnerId: string = `${id.replace(/[,]/g, '_')}_spinner`; + + useEffect(() => { + const isPrimitive: (val: string | number | boolean | object | null) => boolean = + (val: string | number | boolean | object | null) => { + return typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean'; + }; + const setFallback: (val: string | number | boolean | object | null) => void = + (val: string | number | boolean | object | null) => { + setTextValue(isPrimitive(val) ? String(val) : ''); + return; + }; + const handleArrayDataSource: ( + source: Array, + val: string | number | boolean | object | null + ) => boolean = (source: Array, + val: string | number | boolean | object | null) => { + const isPrimitiveArray: boolean = source.length > 0 && isPrimitive(source[0]); + const idx: number = source.findIndex((item: string | number | boolean | { [key: string]: object }) => + isPrimitiveArray + ? String(item) === String(val) + : fields.value && String(getValue(fields.value as string, item)) === String(val) + ); + if (idx !== -1) { + const item: string | number | boolean | { [key: string]: object } = source[idx as number]; + const text: string = isPrimitiveArray + ? String(item) + : fields.text + ? getValue(fields.text as string, item) as string + : String(val); + setTextValue(text); + setActiveIndex(idx); + return true; + } + return false; + }; + + if (!isNullOrUndefined(value)) { + setDropdownValue(value); + if (dataSource && Array.isArray(dataSource)) { + if (!handleArrayDataSource(dataSource, value)) { + setFallback(value); + } + } else if (dataSource instanceof DataManager) { + setFallback(value); + } + } else if (defaultValue && dataSource) { + if (Array.isArray(dataSource)) { + if (!handleArrayDataSource(dataSource, defaultValue)) { + setFallback(defaultValue); + } + } else if (dataSource instanceof DataManager) { + setFallback(defaultValue); + } + setDropdownValue(defaultValue); + } + }, [value, defaultValue, dataSource, fields]); + + useEffect(() => { + if (isPopupOpen && filterable && dropdownbaseRef.current) { + requestAnimationFrame(() => { + setIsSpanFocused(false); + }); + } + }, [isPopupOpen, filterable]); + + useEffect(() => { + setIsLoading(loading); + }, [loading]); + + useEffect(() => { + if (isOpenControlled && open !== isPopupOpen) { + setIsPopupOpen(open as boolean); + if (open && onOpen) { + onOpen(); + } else if (!open && onClose) { + onClose(); + } + } + }, [open, isOpenControlled, isPopupOpen, onOpen, onClose]); + + useEffect(() => { + if (changeEvent && itemData) { + onChangeEvent(changeEvent); + setChangeEvent(null); + } + }, [itemData, changeEvent]); + + useEffect(() => { + const isValid: boolean = valid !== undefined ? valid : (required ? dropdownValue != null : true); + setIsInputValid(isValid); + const message: string = isValid ? '' : validationMessage || ''; + inputElementRef.current?.setCustomValidity(message); + + }, [valid, validationMessage, required, dropdownValue]); + + const publicAPI: Partial = useMemo(() => ({ + dataSource, + query, + fields, + value, + placeholder, + id, + zIndex, + popupWidth, + popupHeight, + allowObjectBinding, + itemTemplate, + headerTemplate, + valueTemplate, + groupTemplate, + noRecordsTemplate, + footerTemplate, + labelMode, + open, + skipDisabledItems, + ignoreCase, + ignoreAccent, + filterable, + filterType, + filterPlaceholder, + sortOrder, + clearButton, + loading, + onChange, + onOpen, + onClose, + onSelect, + onFilterChange, + actionComplete + }), [dataSource, fields, value, placeholder, id, zIndex, popupWidth, popupHeight, allowObjectBinding, onChange, + onOpen, onClose, onSelect, onFilterChange, actionComplete]); + + const filter: ( + dataSource: { [key: string]: Object }[] | DataManager | string[] | number[] | boolean[], + query?: Query, + fields?: FieldSettingsModel + ) => void = useCallback(( + dataSource: { [key: string]: Object }[] | DataManager | string[] | number[] | boolean[], + query?: Query, + fields?: FieldSettingsModel + ) => { + if (dropdownbaseRef.current) { + dropdownbaseRef.current.filter(dataSource, query, fields || props.fields); + } + }, [props.fields]); + + useImperativeHandle(ref, () => ({ + ...publicAPI as IDropDownList, + element: inputElementRef.current, + filter + }), [publicAPI]); + + const showPopup: () => void = useCallback(() => { + if (!isOpenControlled) { + setIsPopupOpen(true); + setAriaExpanded(true); + spanElementRef.current?.focus(); + } + if (onOpen) { + const eventArgs: PopupEventArgs = { + popup: popupRef.current as IPopup, + animation: undefined, + event: undefined + }; + onOpen(eventArgs); + } + }, [onOpen, dropdownValue]); + + const hidePopup: () => void = useCallback(() => { + if (!isOpenControlled) { + setIsPopupOpen(false); + setAriaExpanded(false); + setIsTyped(false); + } + if (onClose && popupRef.current) { + const eventArgs: PopupEventArgs = { + popup: popupRef.current as IPopup, + animation: undefined, + event: undefined + }; + onClose(eventArgs); + } + if (Browser.isDevice) { + setIsFullPagePopup(false); + } + + }, [isPopupOpen, open, onClose]); + + const handleDocumentClick: (e: MouseEvent) => void = (e: MouseEvent) => { + const target: Node = e.target as Node; + const isOutsideInput: boolean | null = spanElementRef.current && !spanElementRef.current.contains(target); + const isOutsidePopup: boolean | null | undefined = popupRef.current?.element && !popupRef.current.element.contains(target); + if (isPopupOpen && isOutsideInput && isOutsidePopup) { + hidePopup(); + setIsSpanFocused(false); + } + if (isOutsideInput && + !(target instanceof Element && target.classList.contains('sf-list-item')) && + !(target instanceof Element && target.classList.contains('e-input-group-icon'))) { + setIsSpanFocused(false); + } + }; + + useEffect(() => { + if (isPopupOpen) { + document.addEventListener('mousedown', handleDocumentClick); + } + return () => { + document.removeEventListener('mousedown', handleDocumentClick); + }; + }, [isPopupOpen, open, hidePopup]); + + useEffect(() => { + preRender('dropdownlist'); + }, []); + + useEffect(() => { + const handleScroll: (e: Event) => void = (e: Event) => { + const isOutsidePopup: boolean | null | undefined = popupRef.current?.element && + !popupRef.current.element.contains(e.target as Node); + if (isPopupOpen && isOutsidePopup) { + hidePopup(); + setIsSpanFocused(false); + } + }; + if (isPopupOpen) { + document.addEventListener('scroll', handleScroll, true); + } + return () => { + document.removeEventListener('scroll', handleScroll, true); + }; + }, [isPopupOpen, hidePopup]); + + + const handleInputChange: (event: React.ChangeEvent) => void = + useCallback((event: React.ChangeEvent) => { + setTextValue(event.target.value); + }, []); + + const handleFocus: () => void = + useCallback(() => { + setIsSpanFocused(true); + if (isPopupOpen && dropdownbaseRef.current) { + const listItems: HTMLLIElement[] = dropdownbaseRef.current.getListItems(); + if (listItems.length > 0 && activeIndex === null) { + setActiveIndex(0); + } + } + + }, [isPopupOpen, activeIndex]); + + const handleClear: () => void = useCallback(() => { + setTextValue(''); + setDropdownValue(null); + setActiveIndex(null); + setSelectedLI(null); + setListKey((prev: number) => prev + 1); + }, []); + + const selectEventCallback: ( + li: Element, + e: React.MouseEvent | React.KeyboardEvent | null, + selectedData?: string | number | boolean | { [key: string]: object }, + value?: string | number | boolean | null + ) => void = useCallback(( + li: Element, + e: React.MouseEvent | React.KeyboardEvent | null, + selectedData?: string | number | boolean | { [key: string]: object }, + value?: string | number | boolean | null + ) => { + setPreviousItemData(itemData || null); + setPreviousValue(itemData); + setSelectedLI(li as HTMLLIElement); + setTextValue((value as string | number | boolean).toString()); + setChangeEvent(e as React.MouseEvent | React.KeyboardEvent); + if (typeof selectedData === 'string' || typeof selectedData === 'number' || typeof selectedData === 'boolean') { + setItemData(selectedData); + setDropdownValue(selectedData); + } else { + setItemData(selectedData as { [key: string]: object; }); + } + if (dropdownbaseRef.current != null && value) { + setActiveIndex(dropdownbaseRef.current.getIndexByValue(value)); + } + }, [itemData]); + + + const getFormattedValue: (value: string) => string | number | boolean | [] = useCallback((value: string) => { + return dropdownbaseRef?.current?.getFormattedValue(value) as string; + }, []); + + const updateSelectedItem: (li: Element, e: React.MouseEvent | React.KeyboardEvent | null) => void = + useCallback((li: Element, e: React.MouseEvent | React.KeyboardEvent | null) => { + li.setAttribute('aria-selected', 'true'); + const value: string | number | boolean | [] | null = getFormattedValue(li.getAttribute('data-value') as string); + const formattedValue: string | number | boolean | null = value as string | number | boolean; + const selectedData: string | number | boolean | {[key: string]: object; } | undefined = + dropdownbaseRef.current?.getDataByValue(formattedValue); + if (value) { + setActiveIndex(dropdownbaseRef.current?.getIndexByValue(value as string | number | boolean) as number); + } + + const dataType: string | undefined = Array.isArray(dataSource) && dataSource.length > 0 ? + typeof dataSource[0] : undefined; + + let itemDataToPass: string | number | boolean | {[key: string]: object} | undefined | null; + if (dataType === 'string' || dataType === 'number' || dataType === 'boolean') { + itemDataToPass = formattedValue; + } else { + itemDataToPass = selectedData; + } + if (e && itemDataToPass && onSelect) { + const eventArgs: SelectEventArgs = { + e: e as React.MouseEvent | React.KeyboardEvent | React.TouchEvent, + item: li as HTMLLIElement, + itemData: selectedData as FieldSettingsModel + }; + onSelect(eventArgs); + } + selectEventCallback(li, e, selectedData, value as string | number | boolean | null); + setSelectedLI(li as HTMLLIElement); + }, [getFormattedValue, onSelect, selectEventCallback, dataSource]); + + + const setSelection: (li: Element | null, e: React.MouseEvent | React.KeyboardEvent | null) => void = + useCallback((li: Element | null, e: React.MouseEvent | React.KeyboardEvent | null) => { + const value: string | null = li?.getAttribute('data-value') as string; + if (value) { + setTextValue(value); + setDropdownValue(getFormattedValue(value)); + updateSelectedItem(li as Element, e); + } + }, [getFormattedValue, updateSelectedItem, setTextValue, setDropdownValue]); + + const onItemClick: (e: React.MouseEvent) => void = useCallback((e: React.MouseEvent) => { + const target: Element = e.target as Element; + const li: HTMLLIElement | null = target.closest('li'); + if (li) { + const dataValue: string | null = li.getAttribute('data-value'); + if (dataValue) { + const isPrimitiveArray: boolean = Array.isArray(dataSource) && dataSource.length > 0 && + (typeof dataSource[0] === 'string' || typeof dataSource[0] === 'number' || typeof dataSource[0] === 'boolean'); + setTextValue(dataValue); + if (isPrimitiveArray) { + setDropdownValue(dataValue); + updateSelectedItem(li as HTMLElement, e); + } else { + setDropdownValue(getFormattedValue(dataValue)); + updateSelectedItem(li as HTMLElement, e); + } + } + hidePopup(); + } + }, [hidePopup, setSelection, getFormattedValue, updateSelectedItem]); + + const getItemData: () => { [key: string]: string } = () => { + const dataItem: { [key: string]: string } = {}; + if (!isNullOrUndefined(itemData)) { + const fieldData: string | boolean | object = allowObjectBinding ? + getValue(fields.value as string, itemData) : + getValue(fields?.value as string, itemData); + dataItem.value = fieldData as string; + dataItem.text = getValue(fields?.text || fields?.value || '', itemData) as string; + } + return dataItem; + }; + + const onChangeEvent: (e?: React.MouseEvent | React.KeyboardEvent) => void = +useCallback((e?: React.MouseEvent | React.KeyboardEvent) => { + if (typeof itemData === 'string' || typeof itemData === 'number' || typeof itemData === 'boolean') { + setTextValue(itemData.toString()); + setDropdownValue(itemData); + if (onChange) { + const eventArgs: ChangeEventArgs = { + event: e as React.MouseEvent | React.KeyboardEvent, + previousItemData: previousItemData, + value: itemData + }; + onChange(eventArgs); + } + setIsInputValid(true); + return; + } + const dataItem: { [key: string]: string } = getItemData(); + const newValue: string | number | boolean | null = dataItem.value; + const displayText: string = dataItem.text !== undefined ? + dataItem.text.toString() : + (dataItem.value !== undefined ? dataItem.value.toString() : ''); + setTextValue(displayText); + setDropdownValue(newValue); + if (onChange) { + const eventArgs: ChangeEventArgs = { + event: e as React.MouseEvent | React.KeyboardEvent, + previousItemData: previousItemData, + value: newValue + }; + onChange(eventArgs); + } + setIsInputValid(true); +}, [getItemData, onChange, previousItemData, itemData]); + + const isItemDisabled: (li: HTMLLIElement) => boolean = useCallback((li: HTMLLIElement) => { + return li.getAttribute('aria-disabled') === 'true' || + li.className.indexOf('e-disabled') !== -1 || + li.className.indexOf('sf-disabled') !== -1; + }, []); + + const findNextEnabledItem: (items: HTMLLIElement[], startIndex: number, direction: number) => number = + useCallback((items: HTMLLIElement[], startIndex: number, direction: number) => { + if (!skipDisabledItems) { + return Math.max(0, Math.min(items.length - 1, startIndex + direction)); + } + let index: number = startIndex; + const maxIterations: number = items.length; + for (let i: number = 0; i < maxIterations; i++) { + index += direction; + if (index < 0 || index >= items.length) { + break; + } + if (!isItemDisabled(items[index as number])) { + return index; + } + } + return startIndex; + }, [skipDisabledItems, isItemDisabled]); + + const findFirstEnabledItem: (items: HTMLLIElement[]) => number = useCallback((items: HTMLLIElement[]) => { + if (!skipDisabledItems) { + return 0; + } + for (let i: number = 0; i < items.length; i++) { + if (!isItemDisabled(items[i as number])) { + return i; + } + } + return 0; + }, [skipDisabledItems, isItemDisabled]); + + const findLastEnabledItem: (items: HTMLLIElement[]) => number = useCallback((items: HTMLLIElement[]) => { + if (!skipDisabledItems) { + return items.length - 1; + } + for (let i: number = items.length - 1; i >= 0; i--) { + if (!isItemDisabled(items[i as number])) { + return i; + } + } + return items.length - 1; + }, [skipDisabledItems, isItemDisabled]); + + const findNearestEnabledItem: (items: HTMLLIElement[], targetIndex: number) => number = + useCallback((items: HTMLLIElement[], targetIndex: number) => { + if (!skipDisabledItems) { + return targetIndex; + } + if (!isItemDisabled(items[targetIndex as number])) { + return targetIndex; + } + let forwardIndex: number = targetIndex; + let backwardIndex: number = targetIndex; + while (forwardIndex < items.length - 1 || backwardIndex > 0) { + if (forwardIndex < items.length - 1) { + forwardIndex++; + if (!isItemDisabled(items[forwardIndex as number])) { + return forwardIndex; + } + } + if (backwardIndex > 0) { + backwardIndex--; + if (!isItemDisabled(items[backwardIndex as number])) { + return backwardIndex; + } + } + } + return targetIndex; + }, [skipDisabledItems, isItemDisabled]); + + const keyActionHandler: (e: React.KeyboardEvent) => void = useCallback((e: React.KeyboardEvent) => { + if (disabled || readOnly) { return; } + if (e.altKey) { + if (e.key === 'ArrowDown') { + showPopup(); + e.preventDefault(); + return; + } + if (e.key === 'ArrowUp') { + hidePopup(); + e.preventDefault(); + return; + } + } + if (e.key === 'Tab' || e.key === 'Escape') { + hidePopup(); + if (e.key === 'Tab') { + setIsSpanFocused(false); + setTimeout(() => { + if (inputElementRef.current) { + inputElementRef.current.blur(); + } + }, 0); + return; + } + e.preventDefault(); + e.stopPropagation(); + return; + } + const isNavigation: boolean = ['ArrowDown', 'ArrowUp', 'PageUp', 'PageDown', 'Home', 'End'].includes(e.key); + setIsTyped((isNavigation || e.key === 'Escape') ? false : isTyped); + if (e.key === 'Enter') { + if (!isPopupOpen) { + showPopup(); + setIsSpanFocused(true); + e.preventDefault(); + return; + } else if (isPopupOpen && activeIndex !== null && activeIndex >= 0) { + const listItems: HTMLLIElement[] | undefined = dropdownbaseRef.current?.getListItems(); + if (listItems && listItems[activeIndex as number]) { + setSelection(listItems[activeIndex as number], e); + hidePopup(); + } + e.preventDefault(); + return; + } + } + if (!isPopupOpen || !dropdownbaseRef.current) { + return; + } + const listItems: HTMLLIElement[] | undefined = dropdownbaseRef.current?.getListItems(); + if (!listItems || listItems.length === 0) { return; } + e.preventDefault(); + switch (e.key) { + case 'ArrowDown': + case 'ArrowUp': { + const direction: number = e.key === 'ArrowDown' ? 1 : -1; + const currentIndex: number = activeIndex !== null ? activeIndex : -1; + if (listItems.length === 1 && e.key === 'ArrowDown') { + const targetItem: HTMLLIElement = listItems[0]; + if (!isItemDisabled(targetItem)) { + setActiveIndex(0); + setSelection(targetItem, e); + targetItem.scrollIntoView({ behavior: 'auto', block: 'nearest' }); + } + break; + } + const newIndex: number = findNextEnabledItem(listItems, currentIndex, direction); + if ( + newIndex !== currentIndex && + newIndex >= 0 && + newIndex < listItems.length + ) { + const targetItem: HTMLLIElement = listItems[newIndex as number]; + if (!skipDisabledItems || !isItemDisabled(targetItem)) { + setActiveIndex(newIndex); + setSelection(targetItem, e); + targetItem.scrollIntoView({ behavior: 'auto', block: 'nearest' }); + } + } + break; + } + case 'PageUp': + case 'PageDown': { + const currentIndex: number = activeIndex !== null ? activeIndex : (e.key === 'PageDown' ? 0 : listItems.length - 1); + const pageSize: number = 10; + const direction: number = e.key === 'PageUp' ? -1 : 1; + const rawIndex: number = Math.max(0, Math.min( + listItems.length - 1, + currentIndex + (direction * pageSize) + )); + const newIndex: number = findNearestEnabledItem(listItems, rawIndex); + if (newIndex !== currentIndex && newIndex >= 0 && newIndex < listItems.length) { + const targetItem: HTMLLIElement = listItems[newIndex as number]; + if (!skipDisabledItems || !isItemDisabled(targetItem)) { + setActiveIndex(newIndex); + setSelection(targetItem, e); + targetItem.scrollIntoView({ behavior: 'auto', block: 'nearest' }); + } + } + break; + } + case 'Home': { + const newIndex: number = findFirstEnabledItem(listItems); + if (newIndex !== activeIndex && newIndex >= 0 && newIndex < listItems.length) { + const firstItem: HTMLLIElement = listItems[newIndex as number]; + if (!skipDisabledItems || !isItemDisabled(firstItem)) { + setActiveIndex(newIndex); + setSelection(firstItem, e); + firstItem.scrollIntoView({ behavior: 'auto', block: 'nearest' }); + } + } + break; + } + case 'End': { + const newIndex: number = findLastEnabledItem(listItems); + if (newIndex !== activeIndex && newIndex >= 0 && newIndex < listItems.length) { + const lastItem: HTMLLIElement = listItems[newIndex as number]; + if (!skipDisabledItems || !isItemDisabled(lastItem)) { + setActiveIndex(newIndex); + setSelection(lastItem, e); + lastItem.scrollIntoView({ behavior: 'auto', block: 'nearest' }); + } + } + break; + } + } + }, [ + isPopupOpen, + disabled, + readOnly, + hidePopup, + showPopup, + activeIndex, + setSelection, + setIsTyped, + skipDisabledItems, + isItemDisabled, + findNextEnabledItem, + findFirstEnabledItem, + findLastEnabledItem, + findNearestEnabledItem + ]); + + const dropDownClick: (e: React.MouseEvent) => void = useCallback((e: React.MouseEvent) => { + if (e.button === 2 || disabled) {return; } + if (e.target instanceof Element && e.target.parentElement?.classList.contains('sf-clear-icon')) { + return; + } + setIsSpanFocused(!isPopupOpen); + if (isLoading && !isDataLoading) { + setIsLoading(false); + } + if (!readOnly) { + if (isPopupOpen) { + hidePopup(); + } else { + if (dataSource instanceof DataManager) { + setIsLoading(true); + setIsDataLoading(true); + setShouldShowPopup(true); + } + showPopup(); + spanElementRef.current?.focus(); + } + } + }, [isPopupOpen, open, disabled, readOnly, hidePopup, showPopup, isLoading, isDataLoading, dataSource]); + + const focusOutAction: (e?: React.MouseEvent | React.KeyboardEvent | React.FocusEvent) => void = + useCallback((e?: React.MouseEvent | React.KeyboardEvent | React.FocusEvent) => { + setIsSpanFocused(false); + if (isSelected) { + onChangeEvent(e as React.MouseEvent | React.KeyboardEvent); + } + }, [isSelected, onChangeEvent]); + + const onBlurHandler: (e: React.FocusEvent) => void = useCallback((e: React.FocusEvent) => { + if (disabled) {return; } + const target: HTMLElement = e.relatedTarget as HTMLElement; + if (!inputElementRef.current?.contains(target)) { + focusOutAction(e); + } + }, [disabled, focusOutAction]); + + const setPopupWidth: () => string = useCallback(() => { + let width: string = formatUnit(popupWidth); + if (width.indexOf('%') > -1) { + const spanWidth: number = spanElementRef.current ? + spanElementRef.current.offsetWidth * parseFloat(width) / 100 : 0; + width = spanWidth.toString() + 'px'; + } + return width; + }, [popupWidth]); + + const setPopupHeight: () => string = useCallback(() => { + let height: string = formatUnit(popupHeight); + if (height.indexOf('%') > -1) { + const inputHeight: number = inputElementRef.current ? + inputElementRef.current.offsetHeight * parseFloat(height) / 100 : 0; + height = inputHeight.toString() + 'px'; + } + return height; + }, [popupHeight]); + + const renderValueTemplate: () => JSX.Element | null = useCallback(() => { + if (!valueTemplate || !dropdownValue) { + return null; + } + let selectedItem: string | number | boolean | {[key: string]: object; } | null | undefined; + if (Array.isArray(dataSource)) { + if (typeof dataSource[0] === 'string' || typeof dataSource[0] === 'number' || typeof dataSource[0] === 'boolean') { + selectedItem = dataSource.find((item: string | number | boolean | {[key: string]: object}) => item === dropdownValue); + } else { + selectedItem = dataSource.find((item: FieldSettingsModel | string | number | boolean | { [key: string]: unknown }) => { + const fieldValue: string | number | boolean | FieldSettingsModel = fields.value ? getValue(fields.value, item) : item; + return allowObjectBinding + ? fieldValue === dropdownValue + : fieldValue === dropdownValue || fieldValue.toString() === dropdownValue.toString(); + }); + } + } + if (!selectedItem) { + return null; + } + return typeof valueTemplate === 'function' + ? valueTemplate(selectedItem, 'dropdownlist') + : valueTemplate; + + }, [valueTemplate, dropdownValue, dataSource, fields.value, allowObjectBinding]); + + const setPlaceholder: string = useMemo(() => { + const l10n: IL10n = L10n('dropdownlist', { placeholder: placeholder }, locale); + l10n.setLocale(locale); + return l10n.getConstant('placeholder'); + }, [locale, placeholder]); + + const handleDataLoaded: () => void = useCallback(() => { + setIsDataLoading(false); + setIsLoading(false); + if (shouldShowPopup) { + showPopup(); + setShouldShowPopup(false); + } + if (dropdownbaseRef.current && dropdownValue !== null) { + const valueData: string | number | boolean | {[key: string]: object} | undefined = + dropdownbaseRef.current.getDataByValue(dropdownValue as string | number | boolean); + if (valueData) { + let displayText: string | number | boolean; + if (typeof valueData === 'object' && fields.text) { + displayText = getValue(fields.text, valueData) as string; + } else if (typeof valueData === 'string' || typeof valueData === 'number' || typeof valueData === 'boolean') { + displayText = valueData.toString(); + } else { + displayText = dropdownbaseRef.current.getTextByValue(dropdownValue as string | number | boolean); + } + setTextValue(displayText); + } + } + }, [dropdownValue, fields, showPopup, shouldShowPopup]); + + const containerClassNames: string = useMemo(() => { + return [ + 'sf-input-group sf-medium', + 'sf-control-wrapper', + 'sf-ddl', + dir === 'rtl' ? 'sf-rtl' : '', + readOnly ? 'sf-readonly' : '', + disabled ? 'sf-disabled' : '', + labelMode !== 'Never' ? CLASS_NAMES.FLOATINPUT : '', + isSpanFocused ? 'sf-input-focus' : '', + (!isInputValid && validityStyles) ? 'sf-error' : '', + className + ].filter(Boolean).join(' '); + }, [readOnly, disabled, dir, isSpanFocused, isInputValid, validityStyles, className]); + + const popupClassNames: string = useMemo(() => { + return [ + isFullPagePopup ? 'sf-popup-full-page' : 'sf-ddl sf-popup', + className + ].filter(Boolean).join(' '); + }, [isFullPagePopup, className]); + + const popupContent: JSX.Element = useMemo(() => ( + + ), [ + dataSource, + fields, + query, + filterable, + filterType, + ignoreAccent, + ignoreCase, + value, + sortOrder, + itemTemplate, + headerTemplate, + footerTemplate, + groupTemplate, + noRecordsTemplate, + dropdownValue, + filterPlaceholder, + filterType, + onItemClick, + keyActionHandler, + setPopupHeight + ]); + + return ( + <> + + {valueTemplate && renderValueTemplate() ? ( + + {renderValueTemplate()} + + ) : null} + + + + {labelMode !== 'Never' && renderFloatLabelElement( + labelMode, + isSpanFocused, + textValue || '', + placeholder, + id + )} + + {clearButton && textValue && (isSpanFocused || isPopupOpen) && renderClearButton(textValue ?? '', handleClear)} + + + {!isLoading && } + + + {isLoading && ( + + )} + + + {isPopupOpen && createPortal( + + {popupContent} + , + document.body + )} + + ); + }); + +export default React.memo(DropDownList); diff --git a/components/dropdowns/src/drop-down-list/index.ts b/components/dropdowns/src/drop-down-list/index.ts new file mode 100644 index 0000000..447c097 --- /dev/null +++ b/components/dropdowns/src/drop-down-list/index.ts @@ -0,0 +1 @@ +export * from './drop-down-list'; diff --git a/components/buttons/styles/floating-action-button/_all.scss b/components/dropdowns/src/dropdowns/drop-down-base/_all.scss similarity index 100% rename from components/buttons/styles/floating-action-button/_all.scss rename to components/dropdowns/src/dropdowns/drop-down-base/_all.scss diff --git a/components/dropdowns/src/dropdowns/drop-down-base/_layout.scss b/components/dropdowns/src/dropdowns/drop-down-base/_layout.scss new file mode 100644 index 0000000..6b9cc18 --- /dev/null +++ b/components/dropdowns/src/dropdowns/drop-down-base/_layout.scss @@ -0,0 +1,124 @@ +@include export-module('dropdownbase-layout') { + .sf-dropdownbase { + display: block; + height: 100%; + min-height: 36px; + position: relative; + width: 100%; + @at-root { + #{if(&, '&', '*')} .sf-list-parent { + margin: 0; + padding: 8px 0; + } + + #{if(&, '&', '*')} .sf-list-group-item, + #{if(&, '&', '*')} .sf-fixed-head { + cursor: default; + } + + #{if(&, '&', '*')} .sf-list-item { + cursor: pointer; + overflow: hidden; + position: relative; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + width: 100%; + } + + #{if(&, '&', '*')} .sf-list-item .sf-list-icon { + font-size: 16px; + vertical-align: middle; + } + + #{if(&, '&', '*')} .sf-fixed-head { + position: absolute; + top: 0; + } + } + } + + .sf-dropdownbase.sf-content { + overflow: auto; + position: relative; + } + + #{&}.sf-popup.sf-ddl .sf-dropdownbase.sf-nodata, + #{&}.sf-popup.sf-mention .sf-dropdownbase.sf-nodata { + color: $ddl-nodata-font-color; + cursor: default; + font-family: $ddl-list-font-family; + font-size: 14px; + padding: 14px 16px; + text-align: center; + } + + .sf-mention.sf-popup { + background: $mention-popup-bg-color; + border: 0; + box-shadow: 0 2px 3px 1px $mention-list-box-shadow-color; + margin-top: 2px; + position: absolute; + } + + .sf-mention.sf-popup .sf-dropdownbase { + min-height: $ddl-list-line-height; + } + + .sf-mention .sf-dropdownbase .sf-list-item .sf-highlight { + display: inline; + font-weight: bold; + vertical-align: baseline; + } + + .sf-mention .sf-mention-chip, + .sf-mention .sf-mention-chip:hover { + border-radius: $mention-chip-border-radius; + border: $mention-chip-border; + color: $mention-active-font-color; + cursor: default; + } + + .sf-mention.sf-editable-element { + border: 2px solid $ddl-list-border-color; + height: auto; + min-height: 120px; + width: 100%; + } + + .sf-form-mirror-div { + white-space: pre-wrap; + } + + .sf-rtl .sf-dropdownbase.sf-dd-group .sf-list-item { + padding-right: $ddl-group-list-padding-left; + } + + .sf-dropdownbase.sf-dd-group .sf-list-item { + padding-left: $ddl-group-list-padding-left; + text-indent: 0; + } + + .sf-small .sf-dropdownbase.sf-dd-group .sf-list-item { + padding-left: $ddl-group-list-small-padding-left; + } + + .sf-popup.sf-multi-select-list-wrapper.sf-multiselect-group .sf-dropdownbase.sf-dd-group .sf-list-group-item { + text-indent: $ddl-group-list-item-text-intent; + } + + .sf-popup.sf-multi-select-list-wrapper.sf-multiselect-group .sf-dropdownbase.sf-dd-group .sf-list-group-item { + cursor: pointer; + font-weight: normal; + overflow: hidden; + position: relative; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + width: 100%; + } + + .sf-rtl.sf-multiselect-group .sf-dropdownbase.sf-dd-group .sf-list-item { + padding-right: $ddl-group-list-padding-left; + } +} diff --git a/components/dropdowns/src/dropdowns/drop-down-base/_material3-definition.scss b/components/dropdowns/src/dropdowns/drop-down-base/_material3-definition.scss new file mode 100644 index 0000000..023bd54 --- /dev/null +++ b/components/dropdowns/src/dropdowns/drop-down-base/_material3-definition.scss @@ -0,0 +1,73 @@ +$ddl-list-line-height: 32px !default; +$ddl-header-font-weight: 600 !default; +$ddl-last-child-bottom-border: 0 !default; +$ddl-default-font-family: inherit !default; +$ddl-line-height: 40px !default; +$ddl-list-border-size: 0 !default; +$ddl-list-bottom-border: $ddl-list-border-size !default; +$ddl-list-border-color: $grey-300 !default; +$ddl-list-gradient-color: rgba($content-bg-color) !default; +$mention-gradient-color: rgba($content-bg-color) !default; +$mention-list-box-shadow-color: rgba(0, 0, 0, .21) !default; +$mention-chip-bg-color: $grey-300 !default; +$mention-chip-border-radius: 2px !default; +$mention-chip-border: none !default; +$ddl-list-bg-color: $flyout-bg-color !default; +$ddl-list-header-bg-color: $transparent !default; +$ddl-list-tap-color: transparent !default; +$ddl-list-header-border-color: rgba($primary) !default; +$ddl-nodata-font-color: rgba($content-text-color-alt1) !default; +$ddl-list-default-font-color: rgba($flyout-text-color) !default; +$ddl-list-active-border-color: $grey-300 !default; +$mention-popup-bg-color: $flyout-bg-color !default; +$ddl-list-active-font-color: rgba($flyout-text-color-selected) !default; +$mention-active-font-color: rgba($primary) !default; +$ddl-list-active-bg-color: rgba($content-bg-color-selected) !default; +$ddl-list-focus-bg-color: $flyout-bg-color-focus !default; +$ddl-list-pressed-bg-color: $flyout-bg-color-pressed !default; +$ddl-list-hover-border-color: $grey-300 !default; +$ddl-list-hover-bg-color: $flyout-bg-color-hover !default; +$ddl-list-hover-font-color: rgba($flyout-text-color-hover) !default; +$ddl-list-header-font-color: rgba($content-text-color) !default; +$ddl-list-font-size: 13px !default; +$ddl-group-list-font-size: 13px !default; +$ddl-list-font-family: inherit !default; +$ddl-default-header-font-color: rgba($content-text-color-alt1) !default; +$ddl-list-focus-color: $grey-50 !default; +$ddl-list-focus-border: 1px solid $grey-400 !default; +$ddl-group-list-padding-left: 2em !default; +$ddl-group-list-small-padding-left: $ddl-group-list-padding-left !default; +$ddl-group-list-bigger-padding-left: $ddl-group-list-padding-left !default; +$ddl-group-list-bigger-small-padding-left: $ddl-group-list-padding-left !default; +$ddl-list-header-padding-left: 12px !default; +$ddl-list-header-small-padding-left: $ddl-list-header-padding-left !default; +$ddl-list-header-bigger-small-padding-left: $ddl-list-header-padding-left !default; +$ddl-list-header-bigger-padding-left: $ddl-list-header-padding-left !default; +$ddl-list-text-size: 16px !default; +$ddl-bigger-text-indent: $ddl-list-text-size !default; +$ddl-list-rtl-padding-right: 0 !default; +$ddl-list-padding-right: 16px !default; +$ddl-list-rtl-padding-left: 16px !default; +$ddl-bigger-list-header-font-size: 14px !default; +$ddl-multi-column-border-width: 0 0 1px 0 !default; +$ddl-multi-column-border-color: $grey-300 !default; +$ddl-group-list-item-text-intent: 0 !default; +$ddl-small-icon-font-size: 14px !default; +$ddl-small-line-height: 26px !default; +$ddl-small-list-font-color: rgba($flyout-text-color) !default; +$ddl-small-list-text-indent: 12px !default; +$ddl-bigger-small-icon-font-size: 18px !default; +$ddl-bigger-small-line-height: 36px !default; +$ddl-bigger-small-list-font-color: rgba($flyout-text-color) !default; +$ddl-bigger-small-list-text-indent: 16px !default; +$ddl-bigger-small-list-header-font-size: 13px !default; +$ddl-list-header-font-size: $ddl-group-list-font-size !default; +$ddl-small-list-header-font-size: $ddl-group-list-font-size !default; +@include export-module('dropdownbase-Material3') { + .e-dropdownbase .e-list-item .e-list-icon { + padding: 0 16px 0 0; + } + .e-small .e-dropdownbase .e-list-item .e-list-icon { + padding: 0 12px 0 0; + } +} diff --git a/components/dropdowns/src/dropdowns/drop-down-base/_theme.scss b/components/dropdowns/src/dropdowns/drop-down-base/_theme.scss new file mode 100644 index 0000000..41cfe4e --- /dev/null +++ b/components/dropdowns/src/dropdowns/drop-down-base/_theme.scss @@ -0,0 +1,183 @@ +@include export-module('dropdownbase-theme') { + .sf-rtl { + #{if(&, '&', '*')} .sf-dropdownbase { + #{if(&, '&', '*')} .sf-list-item { + padding-left: $ddl-list-rtl-padding-left; + padding-right: $ddl-list-rtl-padding-right; + } + } + } + + .sf-dropdownbase { + border-color: $ddl-list-border-color; + background: $ddl-list-bg-color; + @at-root { + #{if(&, '&', '*')} .sf-list-item { + /* stylelint-disable property-no-vendor-prefix */ + -webkit-tap-highlight-color: $ddl-list-tap-color; + background: $ddl-list-bg-color; + border-bottom: $ddl-list-bottom-border; + border-color: $ddl-list-gradient-color; + color: $ddl-list-default-font-color; + font-family: $ddl-list-font-family; + line-height: $ddl-line-height; + min-height: $ddl-list-line-height; + padding-right: $ddl-list-padding-right; + text-indent: $ddl-list-text-size; + } + + #{if(&, '&', '*')} .sf-list-group-item, + .sf-fixed-head { + background: $ddl-list-bg-color; + border-color: $ddl-list-gradient-color; + color: $ddl-list-header-font-color; + font-family: $ddl-list-font-family; + font-weight: $ddl-header-font-weight; + line-height: $ddl-line-height; + min-height: $ddl-list-line-height; + padding-left: $ddl-list-header-padding-left; + padding-right: $ddl-list-header-padding-left; + padding-top: 4px; + padding-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + } + + #{if(&, '&', '*')} .sf-list-item.sf-active, + #{if(&, '&', '*')} .sf-list-item.sf-active.sf-hover { + background: $ddl-list-active-bg-color; + border-color: $ddl-list-active-border-color; + color: $ddl-list-active-font-color; + } + + #{if(&, '&', '*')} .sf-list-item.sf-hover { + background: $ddl-list-hover-bg-color; + border-color: $ddl-list-hover-border-color; + color: $ddl-list-hover-font-color; + } + + #{if(&, '&', '*')} .sf-list-item:active { + background: $ddl-list-pressed-bg-color; + } + + #{if(&, '&', '*')} .sf-list-item:last-child { + border-bottom: $ddl-last-child-bottom-border; + } + + #{if(&, '&', '*')} .sf-list-item.sf-item-focus { + background: $ddl-list-focus-bg-color; + } + } + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open table { + border-collapse: collapse; + table-layout: fixed; + width: 100%; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open th, + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open td { + display: table-cell; + overflow: hidden; + padding-right: 16px; + text-indent: 10px; + text-overflow: ellipsis; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open th { + line-height: 36px; + text-align: left; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-ddl-header { + background: $ddl-list-bg-color; + border-color: $ddl-multi-column-border-color; + border-style: solid; + border-width: $ddl-multi-column-border-width; + color: $ddl-list-header-font-color; + font-family: $ddl-list-font-family; + font-size: $ddl-group-list-font-size; + font-weight: $ddl-header-font-weight; + text-indent: 10px; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-dropdownbase .sf-list-item { + padding-right: 0; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open#{&}.sf-scroller .sf-ddl-header { + padding-right: 16px; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-ddl-header, + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open#{&}.sf-ddl-device .sf-ddl-header { + padding-right: 0; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-text-center { + text-align: center; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-text-right { + text-align: right; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-text-left { + text-align: left; + } + + .sf-small .sf-dropdownbase, + .sf-dropdownbase.sf-small { + @at-root { + #{if(&, '&', '*')} .sf-list-item { + color: $ddl-small-list-font-color; + line-height: $ddl-small-line-height; + min-height: $ddl-small-line-height; + text-indent: $ddl-small-list-text-indent; + } + + #{if(&, '&', '*')} .sf-list-group-item, + #{if(&, '&', '*')} .sf-fixed-head { + font-size: $ddl-small-list-header-font-size; + line-height: $ddl-small-line-height; + min-height: $ddl-small-line-height; + padding-left: $ddl-list-header-small-padding-left; + } + + #{if(&, '&', '*')} .sf-list-item .sf-list-icon { + font-size: $ddl-small-icon-font-size; + } + } + } + + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item { + background: $ddl-list-bg-color; + border-bottom: $ddl-list-bottom-border; + border-color: $ddl-list-gradient-color; + color: $ddl-list-default-font-color; + font-family: $ddl-list-font-family; + text-indent: $ddl-list-text-size; + } + + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item.sf-item-focus { + background: $ddl-list-hover-bg-color; + } + + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item.sf-active, + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item.sf-active.sf-hover { + background: $ddl-list-active-bg-color; + border-color: $ddl-list-active-border-color; + color: $ddl-list-active-font-color; + } + + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item.sf-hover { + background: $ddl-list-hover-bg-color; + border-color: $ddl-list-hover-border-color; + color: $ddl-list-hover-font-color; + } + + .sf-selectall-parent.sf-item-focus{ + background-color: $ddl-list-hover-bg-color; + } +} diff --git a/components/navigations/styles/context-menu/_all.scss b/components/dropdowns/src/dropdowns/drop-down-list/_all.scss similarity index 100% rename from components/navigations/styles/context-menu/_all.scss rename to components/dropdowns/src/dropdowns/drop-down-list/_all.scss diff --git a/components/dropdowns/src/dropdowns/drop-down-list/_layout.scss b/components/dropdowns/src/dropdowns/drop-down-list/_layout.scss new file mode 100644 index 0000000..396dd64 --- /dev/null +++ b/components/dropdowns/src/dropdowns/drop-down-list/_layout.scss @@ -0,0 +1,280 @@ +@include export-module('dropdownlist-layout') { + + .sf-ddl#{&}.sf-popup { + #{if(&, '&', '*')} .sf-input-group { + margin-top: 4px; + } + } + + .sf-popup.sf-wide-popup.sf-ddl-device.sf-popup-close { + display: block; + visibility: hidden; + } + + .sf-popup-full-page { + bottom: 0; + left: 0; + margin: 0; + overflow: hidden; + padding: 0; + right: 0; + top: 0; + + #{&}.sf-ddl.sf-popup.sf-ddl-device-filter { + margin: $ddl-filter-margin; + } + } + + .sf-ddl.sf-control-wrapper .sf-ddl-disable-icon { + position: relative; + } + + .sf-ddl.sf-control-wrapper .sf-ddl-disable-icon::before { + content: ''; + } + + .sf-ddl.sf-control-wrapper.sf-input-group .sf-ddl-icon.sf-ddl-disable-icon { + position: relative; + } + + .sf-ddl.sf-control-wrapper.sf-input-group .sf-ddl-icon.sf-ddl-disable-icon::before { + content: ''; + } + + .sf-ddl-device-filter .sf-filter-parent { + background-color: $ddl-filter-background-color; + } + + /* stylelint-disable property-no-vendor-prefix */ + .sf-ddl input.sf-input::-webkit-contacts-auto-fill-button { + display: none; + pointer-events: none; + position: absolute; + right: 0; + visibility: hidden; + } + /* stylelint-enable property-no-vendor-prefix */ + + .sf-filter-parent { + border: $ddl-filter-border; + border-top-width: $ddl-filter-top-border; + box-shadow: $ddl-filter-box-shadow; + display: block; + padding: $ddl-filter-padding; + } + + .sf-ddl.sf-input-group:not(.sf-disabled) { + cursor: pointer; + } + + .sf-ddl#{&}.sf-popup.sf-ddl-device-filter { + @at-root { + #{if(&, '&', '*')} .sf-input-group.sf-input-focus::before, + #{if(&, '&', '*')} .sf-input-group.sf-input-focus::after { + width: 0; + } + } + } + + .sf-ddl#{&}.sf-popup { + background: $ddl-popup-background-color; + border-radius: 4px; + position: absolute; + @at-root { + #{if(&, '&', '*')} .sf-search-icon { + margin: 0; + opacity: .57; + padding: $ddl-list-search-icon-padding; + } + + #{if(&, '&', '*')} .sf-filter-parent .sf-back-icon { + padding: $ddl-back-icon-padding; + } + + #{if(&, '&', '*')}.sf-rtl .sf-filter-parent .sf-input-group.sf-control-wrapper .sf-input-filter, + #{if(&, '&', '*')} .sf-filter-parent .sf-input-filter, + #{if(&, '&', '*')} .sf-filter-parent .sf-input-filter:focus, + #{if(&, '&', '*')} .sf-filter-parent .sf-input-group.sf-input-focus .sf-input-filter, + #{if(&, '&', '*')} .sf-filter-parent .sf-input-group.sf-control-wrapper.sf-input-focus .sf-input-filter { + padding: $ddl-list-filter-text-indent; + } + + #{if(&, '&', '*')} .sf-input-group { + margin-bottom: 0; + } + + #{if(&, '&', '*')} .sf-ddl-footer, + #{if(&, '&', '*')} .sf-ddl-header { + cursor: default; + line-height: $ddl-line-height; + font-size: $ddl-text-size; + text-indent: $ddl-text-size; + background: $ddl-list-bg-color; + font-weight: 500; + } + } + } + + /* stylelint-disable property-no-vendor-prefix */ + .sf-ddl.sf-input-group .sf-ddl-hidden, + .sf-ddl.sf-float-input .sf-ddl-hidden { + -webkit-appearance: initial; + border: 0; + height: 0; + padding: 0; + visibility: hidden; + width: 0; + } + + .sf-ddl.sf-input-group, + .sf-ddl.sf-input-group.sf-input-focus:focus { + outline: none; + } + + .sf-dropdownbase .sf-list-item .sf-highlight { + display: inline; + font-weight: bold; + vertical-align: baseline; + } + + .sf-ddl.sf-input-group input[readonly] ~ .sf-clear-icon:not(.sf-clear-icon-hide), + .sf-float-input input[readonly] ~ .sf-clear-icon:not(.sf-clear-icon-hide), + .sf-float-input.sf-input-group input[readonly] ~ .sf-clear-icon:not(.sf-clear-icon-hide) { + opacity: 1; + } + + .sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon, + .sf-input-group input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon, + .sf-input-group.sf-control-wrapper input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon, + .sf-float-input input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon, + .sf-float-input.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon, + .sf-float-input.sf-input-group input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon, + .sf-float-input.sf-input-group.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon { + display: flex; + } + + .sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-input-group input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-input-group.sf-control-wrapper input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-input-group input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-input-group.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide { + display: none; + } + + .sf-input-group.sf-static-clear input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-input-group.sf-static-clear.sf-control-wrapper input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-static-clear input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-static-clear.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-static-clear.sf-input-group input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-static-clear.sf-input-group.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide { + cursor: pointer; + display: flex; + } + + .sf-ddl.sf-input-group { + .sf-input-value, + .sf-input-value:focus { + font-family: $ddl-input-font-family; + font-size: $ddl-input-font-size; + height: auto; + margin: $ddl-zero-value; + outline: none; + width: 100%; + overflow: hidden; + } + + input[readonly].sf-input, + input[readonly], + .sf-dropdownlist { + pointer-events: none; + } + } + + .sf-data-form .sf-ddl.sf-input-group.sf-control-container input[readonly].sf-input.sf-dropdownlist { + cursor: pointer; + pointer-events: auto; + } + + .sf-ddl.sf-popup.sf-popup-open .sf-list-item.sf-disabled { + opacity: .7; + pointer-events: none; + } + + ejs-autocomplete, + ejs-combobox, + ejs-dropdownlist { + display: block; + } + + .sf-small .sf-ddl#{&}.sf-popup, + .sf-input-group.sf-ddl.sf-small { + #{if(&, '&', '*')} .sf-list-item { + font-size: $ddl-small-list-font-size; + } + + #{if(&, '&', '*')} .sf-list-group-item { + font-size: $ddl-small-list-font-size; + } + } + + .sf-small.sf-ddl#{&}.sf-popup, + .sf-input-group.sf-ddl.sf-small { + #{if(&, '&', '*')} .sf-list-item { + font-size: $ddl-small-list-font-size; + } + + #{if(&, '&', '*')} .sf-list-group-item { + font-size: $ddl-small-list-font-size; + } + } + + .sf-content-placeholder.sf-ddl.sf-placeholder-ddl, + .sf-content-placeholder.sf-autocomplete.sf-placeholder-autocomplete, + .sf-content-placeholder.sf-combobox.sf-placeholder-combobox { + background-size: 300px 33px; + min-height: 33px; + } + + .sf-ddl.sf-popup.sf-resize .sf-resizer-right { + bottom: 0; + right: 0; + cursor: nwse-resize; + height: 15px; + position: absolute; + width: 15px; + } + + .sf-ddl.sf-popup.sf-resize .sf-resizer-right { + background: transparent; + color: rgb(221, 218, 218); + } + + .sf-ddl .sf-input-group-icon.sf-ddl-icon.sf-icon-container { + display: flex; + align-items: center; + } + + .sf-ddl .sf-input-group-icon.sf-ddl-icon.sf-icon-rotate { + transform: rotate(180deg); + transition: transform 0.3s ease; + } + + .sf-ddl .sf-input-group-icon.sf-ddl-icon.sf-icon-normal { + transform: rotate(0deg); + transition: transform 0.3s ease; + } + + .sf-ddl.sf-popup .sf-ddl-header { + position: sticky; + top: 0; + z-index: 1; + } + + .sf-ddl.sf-popup .sf-ddl-footer { + bottom: 0; + position: sticky; + z-index: 1; + } +} diff --git a/components/dropdowns/src/dropdowns/drop-down-list/_material3-definition.scss b/components/dropdowns/src/dropdowns/drop-down-list/_material3-definition.scss new file mode 100644 index 0000000..2dd29a3 --- /dev/null +++ b/components/dropdowns/src/dropdowns/drop-down-list/_material3-definition.scss @@ -0,0 +1,118 @@ +$ddl-input-font-size: $text-sm !default; +$ddl-zero-value: 0 !default; +$border-type: solid !default; +$border-size: 1px !default; +$ddl-default-border-color: rgba($primary) !default; +$ddl-input-border: $border-size $border-type !default; +$ddl-input-font-family: inherit !default; +$ddl-input-margin-bottom: 4px !default; +$ddl-input-padding: 8px $ddl-zero-value 4px !default; +$ddl-input-group-border-width: $ddl-zero-value !default; +$ddl-active-font-color: rgba($content-text-color) !default; +$ddl-list-search-icon-padding: 12px 8px 8px !default; +$ddl-list-filter-text-indent: 4px 16px 4px !default; +$ddl-bigger-list-font-size: 14px !default; +$ddl-list-box-shadow-color: rgba(0, 0, 0, .21) !default; +$ddl-filter-box-shadow-color: rgba(0, 0, 0, .3) !default; +$ddl-popup-background-color: $flyout-bg-color !default; +$ddl-filter-border: 0 !default; +$ddl-filter-top-border: 0 !default; +$ddl-filter-padding: 0 !default; +$ddl-filter-box-shadow: 0 1.5px 5px -2px $ddl-filter-box-shadow-color !default; +$ddl-filter-background-color: $flyout-bg-color !default; +$ddl-clear-icon-margin-right: 66px !default; +$ddl-back-icon-margin: 0 10px 0 -52px !default; +$ddl-back-icon-padding: 0 8px !default; +$ddl-popup-shadow: $shadow-md !default; +$ddl-filter-margin: 0 !default; +$ddl-icon-hover-bg: rgba($content-text-color, .08) !default; +$ddl-small-list-font-size: $text-xs !default; +$ddl-bigger-small-list-font-size: $text-sm !default; +$ddl-error-border-color: rgba($border-error) !default; +$ddl-line-height: 36px !default; +$ddl-text-size: 14px !default; +$ddl-list-bg-color: $flyout-bg-color !default; +@include export-module('dropdownlist-Material3') { + .sf-ddl.sf-control-wrapper .sf-ddl-icon::before { + transform: rotate(0deg); + transition: transform 300ms ease; + } + + .sf-ddl.sf-control-wrapper.sf-icon-anim .sf-ddl-icon::before { + transform: rotate(180deg); + transition: transform 300ms ease; + } + + .sf-dropdownbase .sf-list-item.sf-active.sf-hover { + color: $ddl-active-font-color; + } + + .sf-input-group:not(.sf-disabled) .sf-control#{&}.sf-dropdownlist ~ .sf-ddl-icon:active, + .sf-input-group:not(.sf-disabled) .sf-control#{&}.sf-dropdownlist ~ .sf-ddl-icon:hover, + .sf-input-group:not(.sf-disabled) .sf-back-icon:active, + .sf-input-group:not(.sf-disabled) .sf-back-icon:hover, + #{&}.sf-popup.sf-ddl .sf-input-group:not(.sf-disabled) .sf-clear-icon:active, + #{&}.sf-popup.sf-ddl .sf-input-group:not(.sf-disabled) .sf-clear-icon:hover { + background: $ddl-icon-hover-bg; + } + + .sf-input-group .sf-ddl-icon:not(:active)::after { + animation: none; + } + + .sf-ddl#{&}.sf-popup { + border: 0; + box-shadow: $ddl-popup-shadow; + margin-top: 2px; + } + + #{&}.sf-popup.sf-ddl .sf-dropdownbase { + min-height: 26px; + border-radius: 4px; + } + + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon { + margin: 0 6px; + min-height: 30px; + min-width: 30px; + } + + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon, + .sf-small.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon { + min-height: 22px; + min-width: 22px; + } + + .sf-input-group.sf-ddl, + .sf-input-group.sf-ddl .sf-input, + .sf-input-group.sf-ddl .sf-ddl-icon { + background: transparent; + } + + .sf-ddl.sf-ddl-device.sf-ddl-devicsf-filter .sf-input-group:hover:not(.sf-disabled):not(.sf-float-icon-left), + .sf-ddl.sf-ddl-device.sf-ddl-devicsf-filter .sf-input-group.sf-control-wrapper:hover:not(.sf-disabled):not(.sf-float-icon-left) { + border-bottom-width: 0; + } + + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group.sf-small .sf-clear-icon, + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon, + .sf-small#{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon, + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group.sf-input-focus.sf-small .sf-clear-icon, + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group.sf-input-focus .sf-clear-icon, + .sf-small#{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group.sf-input-focus .sf-clear-icon { + margin: 4px; + } + + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group .sf-input-filter, + .sf-small#{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group .sf-input-filter, + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group.sf-small .sf-input-filter, + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group.sf-input-focus .sf-input-filter, + .sf-small#{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group.sf-input-focus .sf-input-filter, + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group.sf-small.sf-input-focus .sf-input-filter { + padding: 4px 5px 4px 12px; + } + + .sf-ddl.sf-popup.sf-outline .sf-filter-parent { + padding: 4px 8px; + } +} diff --git a/components/dropdowns/src/dropdowns/drop-down-list/_theme.scss b/components/dropdowns/src/dropdowns/drop-down-list/_theme.scss new file mode 100644 index 0000000..21524c2 --- /dev/null +++ b/components/dropdowns/src/dropdowns/drop-down-list/_theme.scss @@ -0,0 +1,14 @@ +@include export-module('dropdownlist-theme') { + #{&}.sf-popup { + border-color: $ddl-default-border-color; + } + + .sf-float-input.sf-input-group.sf-ddl.sf-control.sf-icon-anim > .sf-float-text, + .sf-float-input.sf-input-focus.sf-input-group.sf-ddl.sf-control.sf-keyboard > .sf-float-text { + color: $ddl-active-font-color; + } + + .sf-input-group.sf-control-wrapper.sf-ddl.sf-error { + border-bottom-color: $ddl-error-border-color; + } +} \ No newline at end of file diff --git a/components/dropdowns/src/index.ts b/components/dropdowns/src/index.ts new file mode 100644 index 0000000..a75b5f0 --- /dev/null +++ b/components/dropdowns/src/index.ts @@ -0,0 +1,2 @@ +export * from './common/index'; +export * from './drop-down-list/index'; diff --git a/components/navigations/styles/h-scroll/_all.scss b/components/dropdowns/styles/drop-down-base/_all.scss similarity index 100% rename from components/navigations/styles/h-scroll/_all.scss rename to components/dropdowns/styles/drop-down-base/_all.scss diff --git a/components/dropdowns/styles/drop-down-base/_layout.scss b/components/dropdowns/styles/drop-down-base/_layout.scss new file mode 100644 index 0000000..6b9cc18 --- /dev/null +++ b/components/dropdowns/styles/drop-down-base/_layout.scss @@ -0,0 +1,124 @@ +@include export-module('dropdownbase-layout') { + .sf-dropdownbase { + display: block; + height: 100%; + min-height: 36px; + position: relative; + width: 100%; + @at-root { + #{if(&, '&', '*')} .sf-list-parent { + margin: 0; + padding: 8px 0; + } + + #{if(&, '&', '*')} .sf-list-group-item, + #{if(&, '&', '*')} .sf-fixed-head { + cursor: default; + } + + #{if(&, '&', '*')} .sf-list-item { + cursor: pointer; + overflow: hidden; + position: relative; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + width: 100%; + } + + #{if(&, '&', '*')} .sf-list-item .sf-list-icon { + font-size: 16px; + vertical-align: middle; + } + + #{if(&, '&', '*')} .sf-fixed-head { + position: absolute; + top: 0; + } + } + } + + .sf-dropdownbase.sf-content { + overflow: auto; + position: relative; + } + + #{&}.sf-popup.sf-ddl .sf-dropdownbase.sf-nodata, + #{&}.sf-popup.sf-mention .sf-dropdownbase.sf-nodata { + color: $ddl-nodata-font-color; + cursor: default; + font-family: $ddl-list-font-family; + font-size: 14px; + padding: 14px 16px; + text-align: center; + } + + .sf-mention.sf-popup { + background: $mention-popup-bg-color; + border: 0; + box-shadow: 0 2px 3px 1px $mention-list-box-shadow-color; + margin-top: 2px; + position: absolute; + } + + .sf-mention.sf-popup .sf-dropdownbase { + min-height: $ddl-list-line-height; + } + + .sf-mention .sf-dropdownbase .sf-list-item .sf-highlight { + display: inline; + font-weight: bold; + vertical-align: baseline; + } + + .sf-mention .sf-mention-chip, + .sf-mention .sf-mention-chip:hover { + border-radius: $mention-chip-border-radius; + border: $mention-chip-border; + color: $mention-active-font-color; + cursor: default; + } + + .sf-mention.sf-editable-element { + border: 2px solid $ddl-list-border-color; + height: auto; + min-height: 120px; + width: 100%; + } + + .sf-form-mirror-div { + white-space: pre-wrap; + } + + .sf-rtl .sf-dropdownbase.sf-dd-group .sf-list-item { + padding-right: $ddl-group-list-padding-left; + } + + .sf-dropdownbase.sf-dd-group .sf-list-item { + padding-left: $ddl-group-list-padding-left; + text-indent: 0; + } + + .sf-small .sf-dropdownbase.sf-dd-group .sf-list-item { + padding-left: $ddl-group-list-small-padding-left; + } + + .sf-popup.sf-multi-select-list-wrapper.sf-multiselect-group .sf-dropdownbase.sf-dd-group .sf-list-group-item { + text-indent: $ddl-group-list-item-text-intent; + } + + .sf-popup.sf-multi-select-list-wrapper.sf-multiselect-group .sf-dropdownbase.sf-dd-group .sf-list-group-item { + cursor: pointer; + font-weight: normal; + overflow: hidden; + position: relative; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + width: 100%; + } + + .sf-rtl.sf-multiselect-group .sf-dropdownbase.sf-dd-group .sf-list-item { + padding-right: $ddl-group-list-padding-left; + } +} diff --git a/components/dropdowns/styles/drop-down-base/_material3-definition.scss b/components/dropdowns/styles/drop-down-base/_material3-definition.scss new file mode 100644 index 0000000..023bd54 --- /dev/null +++ b/components/dropdowns/styles/drop-down-base/_material3-definition.scss @@ -0,0 +1,73 @@ +$ddl-list-line-height: 32px !default; +$ddl-header-font-weight: 600 !default; +$ddl-last-child-bottom-border: 0 !default; +$ddl-default-font-family: inherit !default; +$ddl-line-height: 40px !default; +$ddl-list-border-size: 0 !default; +$ddl-list-bottom-border: $ddl-list-border-size !default; +$ddl-list-border-color: $grey-300 !default; +$ddl-list-gradient-color: rgba($content-bg-color) !default; +$mention-gradient-color: rgba($content-bg-color) !default; +$mention-list-box-shadow-color: rgba(0, 0, 0, .21) !default; +$mention-chip-bg-color: $grey-300 !default; +$mention-chip-border-radius: 2px !default; +$mention-chip-border: none !default; +$ddl-list-bg-color: $flyout-bg-color !default; +$ddl-list-header-bg-color: $transparent !default; +$ddl-list-tap-color: transparent !default; +$ddl-list-header-border-color: rgba($primary) !default; +$ddl-nodata-font-color: rgba($content-text-color-alt1) !default; +$ddl-list-default-font-color: rgba($flyout-text-color) !default; +$ddl-list-active-border-color: $grey-300 !default; +$mention-popup-bg-color: $flyout-bg-color !default; +$ddl-list-active-font-color: rgba($flyout-text-color-selected) !default; +$mention-active-font-color: rgba($primary) !default; +$ddl-list-active-bg-color: rgba($content-bg-color-selected) !default; +$ddl-list-focus-bg-color: $flyout-bg-color-focus !default; +$ddl-list-pressed-bg-color: $flyout-bg-color-pressed !default; +$ddl-list-hover-border-color: $grey-300 !default; +$ddl-list-hover-bg-color: $flyout-bg-color-hover !default; +$ddl-list-hover-font-color: rgba($flyout-text-color-hover) !default; +$ddl-list-header-font-color: rgba($content-text-color) !default; +$ddl-list-font-size: 13px !default; +$ddl-group-list-font-size: 13px !default; +$ddl-list-font-family: inherit !default; +$ddl-default-header-font-color: rgba($content-text-color-alt1) !default; +$ddl-list-focus-color: $grey-50 !default; +$ddl-list-focus-border: 1px solid $grey-400 !default; +$ddl-group-list-padding-left: 2em !default; +$ddl-group-list-small-padding-left: $ddl-group-list-padding-left !default; +$ddl-group-list-bigger-padding-left: $ddl-group-list-padding-left !default; +$ddl-group-list-bigger-small-padding-left: $ddl-group-list-padding-left !default; +$ddl-list-header-padding-left: 12px !default; +$ddl-list-header-small-padding-left: $ddl-list-header-padding-left !default; +$ddl-list-header-bigger-small-padding-left: $ddl-list-header-padding-left !default; +$ddl-list-header-bigger-padding-left: $ddl-list-header-padding-left !default; +$ddl-list-text-size: 16px !default; +$ddl-bigger-text-indent: $ddl-list-text-size !default; +$ddl-list-rtl-padding-right: 0 !default; +$ddl-list-padding-right: 16px !default; +$ddl-list-rtl-padding-left: 16px !default; +$ddl-bigger-list-header-font-size: 14px !default; +$ddl-multi-column-border-width: 0 0 1px 0 !default; +$ddl-multi-column-border-color: $grey-300 !default; +$ddl-group-list-item-text-intent: 0 !default; +$ddl-small-icon-font-size: 14px !default; +$ddl-small-line-height: 26px !default; +$ddl-small-list-font-color: rgba($flyout-text-color) !default; +$ddl-small-list-text-indent: 12px !default; +$ddl-bigger-small-icon-font-size: 18px !default; +$ddl-bigger-small-line-height: 36px !default; +$ddl-bigger-small-list-font-color: rgba($flyout-text-color) !default; +$ddl-bigger-small-list-text-indent: 16px !default; +$ddl-bigger-small-list-header-font-size: 13px !default; +$ddl-list-header-font-size: $ddl-group-list-font-size !default; +$ddl-small-list-header-font-size: $ddl-group-list-font-size !default; +@include export-module('dropdownbase-Material3') { + .e-dropdownbase .e-list-item .e-list-icon { + padding: 0 16px 0 0; + } + .e-small .e-dropdownbase .e-list-item .e-list-icon { + padding: 0 12px 0 0; + } +} diff --git a/components/dropdowns/styles/drop-down-base/_theme.scss b/components/dropdowns/styles/drop-down-base/_theme.scss new file mode 100644 index 0000000..41cfe4e --- /dev/null +++ b/components/dropdowns/styles/drop-down-base/_theme.scss @@ -0,0 +1,183 @@ +@include export-module('dropdownbase-theme') { + .sf-rtl { + #{if(&, '&', '*')} .sf-dropdownbase { + #{if(&, '&', '*')} .sf-list-item { + padding-left: $ddl-list-rtl-padding-left; + padding-right: $ddl-list-rtl-padding-right; + } + } + } + + .sf-dropdownbase { + border-color: $ddl-list-border-color; + background: $ddl-list-bg-color; + @at-root { + #{if(&, '&', '*')} .sf-list-item { + /* stylelint-disable property-no-vendor-prefix */ + -webkit-tap-highlight-color: $ddl-list-tap-color; + background: $ddl-list-bg-color; + border-bottom: $ddl-list-bottom-border; + border-color: $ddl-list-gradient-color; + color: $ddl-list-default-font-color; + font-family: $ddl-list-font-family; + line-height: $ddl-line-height; + min-height: $ddl-list-line-height; + padding-right: $ddl-list-padding-right; + text-indent: $ddl-list-text-size; + } + + #{if(&, '&', '*')} .sf-list-group-item, + .sf-fixed-head { + background: $ddl-list-bg-color; + border-color: $ddl-list-gradient-color; + color: $ddl-list-header-font-color; + font-family: $ddl-list-font-family; + font-weight: $ddl-header-font-weight; + line-height: $ddl-line-height; + min-height: $ddl-list-line-height; + padding-left: $ddl-list-header-padding-left; + padding-right: $ddl-list-header-padding-left; + padding-top: 4px; + padding-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + } + + #{if(&, '&', '*')} .sf-list-item.sf-active, + #{if(&, '&', '*')} .sf-list-item.sf-active.sf-hover { + background: $ddl-list-active-bg-color; + border-color: $ddl-list-active-border-color; + color: $ddl-list-active-font-color; + } + + #{if(&, '&', '*')} .sf-list-item.sf-hover { + background: $ddl-list-hover-bg-color; + border-color: $ddl-list-hover-border-color; + color: $ddl-list-hover-font-color; + } + + #{if(&, '&', '*')} .sf-list-item:active { + background: $ddl-list-pressed-bg-color; + } + + #{if(&, '&', '*')} .sf-list-item:last-child { + border-bottom: $ddl-last-child-bottom-border; + } + + #{if(&, '&', '*')} .sf-list-item.sf-item-focus { + background: $ddl-list-focus-bg-color; + } + } + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open table { + border-collapse: collapse; + table-layout: fixed; + width: 100%; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open th, + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open td { + display: table-cell; + overflow: hidden; + padding-right: 16px; + text-indent: 10px; + text-overflow: ellipsis; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open th { + line-height: 36px; + text-align: left; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-ddl-header { + background: $ddl-list-bg-color; + border-color: $ddl-multi-column-border-color; + border-style: solid; + border-width: $ddl-multi-column-border-width; + color: $ddl-list-header-font-color; + font-family: $ddl-list-font-family; + font-size: $ddl-group-list-font-size; + font-weight: $ddl-header-font-weight; + text-indent: 10px; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-dropdownbase .sf-list-item { + padding-right: 0; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open#{&}.sf-scroller .sf-ddl-header { + padding-right: 16px; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-ddl-header, + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open#{&}.sf-ddl-device .sf-ddl-header { + padding-right: 0; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-text-center { + text-align: center; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-text-right { + text-align: right; + } + + .sf-multi-column#{&}.sf-ddl#{&}.sf-popup#{&}.sf-popup-open .sf-text-left { + text-align: left; + } + + .sf-small .sf-dropdownbase, + .sf-dropdownbase.sf-small { + @at-root { + #{if(&, '&', '*')} .sf-list-item { + color: $ddl-small-list-font-color; + line-height: $ddl-small-line-height; + min-height: $ddl-small-line-height; + text-indent: $ddl-small-list-text-indent; + } + + #{if(&, '&', '*')} .sf-list-group-item, + #{if(&, '&', '*')} .sf-fixed-head { + font-size: $ddl-small-list-header-font-size; + line-height: $ddl-small-line-height; + min-height: $ddl-small-line-height; + padding-left: $ddl-list-header-small-padding-left; + } + + #{if(&, '&', '*')} .sf-list-item .sf-list-icon { + font-size: $ddl-small-icon-font-size; + } + } + } + + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item { + background: $ddl-list-bg-color; + border-bottom: $ddl-list-bottom-border; + border-color: $ddl-list-gradient-color; + color: $ddl-list-default-font-color; + font-family: $ddl-list-font-family; + text-indent: $ddl-list-text-size; + } + + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item.sf-item-focus { + background: $ddl-list-hover-bg-color; + } + + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item.sf-active, + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item.sf-active.sf-hover { + background: $ddl-list-active-bg-color; + border-color: $ddl-list-active-border-color; + color: $ddl-list-active-font-color; + } + + .sf-ddl.sf-popup.sf-multiselect-group .sf-list-group-item.sf-hover { + background: $ddl-list-hover-bg-color; + border-color: $ddl-list-hover-border-color; + color: $ddl-list-hover-font-color; + } + + .sf-selectall-parent.sf-item-focus{ + background-color: $ddl-list-hover-bg-color; + } +} diff --git a/components/buttons/styles/floating-action-button/material3.scss b/components/dropdowns/styles/drop-down-base/material3.scss similarity index 56% rename from components/buttons/styles/floating-action-button/material3.scss rename to components/dropdowns/styles/drop-down-base/material3.scss index 9eeb62e..84409d0 100644 --- a/components/buttons/styles/floating-action-button/material3.scss +++ b/components/dropdowns/styles/drop-down-base/material3.scss @@ -1,2 +1,3 @@ +@import '../../base/themes/material3.scss'; @import 'material3-definition.scss'; @import 'all.scss'; diff --git a/components/navigations/styles/toolbar/_all.scss b/components/dropdowns/styles/drop-down-list/_all.scss similarity index 100% rename from components/navigations/styles/toolbar/_all.scss rename to components/dropdowns/styles/drop-down-list/_all.scss diff --git a/components/dropdowns/styles/drop-down-list/_layout.scss b/components/dropdowns/styles/drop-down-list/_layout.scss new file mode 100644 index 0000000..396dd64 --- /dev/null +++ b/components/dropdowns/styles/drop-down-list/_layout.scss @@ -0,0 +1,280 @@ +@include export-module('dropdownlist-layout') { + + .sf-ddl#{&}.sf-popup { + #{if(&, '&', '*')} .sf-input-group { + margin-top: 4px; + } + } + + .sf-popup.sf-wide-popup.sf-ddl-device.sf-popup-close { + display: block; + visibility: hidden; + } + + .sf-popup-full-page { + bottom: 0; + left: 0; + margin: 0; + overflow: hidden; + padding: 0; + right: 0; + top: 0; + + #{&}.sf-ddl.sf-popup.sf-ddl-device-filter { + margin: $ddl-filter-margin; + } + } + + .sf-ddl.sf-control-wrapper .sf-ddl-disable-icon { + position: relative; + } + + .sf-ddl.sf-control-wrapper .sf-ddl-disable-icon::before { + content: ''; + } + + .sf-ddl.sf-control-wrapper.sf-input-group .sf-ddl-icon.sf-ddl-disable-icon { + position: relative; + } + + .sf-ddl.sf-control-wrapper.sf-input-group .sf-ddl-icon.sf-ddl-disable-icon::before { + content: ''; + } + + .sf-ddl-device-filter .sf-filter-parent { + background-color: $ddl-filter-background-color; + } + + /* stylelint-disable property-no-vendor-prefix */ + .sf-ddl input.sf-input::-webkit-contacts-auto-fill-button { + display: none; + pointer-events: none; + position: absolute; + right: 0; + visibility: hidden; + } + /* stylelint-enable property-no-vendor-prefix */ + + .sf-filter-parent { + border: $ddl-filter-border; + border-top-width: $ddl-filter-top-border; + box-shadow: $ddl-filter-box-shadow; + display: block; + padding: $ddl-filter-padding; + } + + .sf-ddl.sf-input-group:not(.sf-disabled) { + cursor: pointer; + } + + .sf-ddl#{&}.sf-popup.sf-ddl-device-filter { + @at-root { + #{if(&, '&', '*')} .sf-input-group.sf-input-focus::before, + #{if(&, '&', '*')} .sf-input-group.sf-input-focus::after { + width: 0; + } + } + } + + .sf-ddl#{&}.sf-popup { + background: $ddl-popup-background-color; + border-radius: 4px; + position: absolute; + @at-root { + #{if(&, '&', '*')} .sf-search-icon { + margin: 0; + opacity: .57; + padding: $ddl-list-search-icon-padding; + } + + #{if(&, '&', '*')} .sf-filter-parent .sf-back-icon { + padding: $ddl-back-icon-padding; + } + + #{if(&, '&', '*')}.sf-rtl .sf-filter-parent .sf-input-group.sf-control-wrapper .sf-input-filter, + #{if(&, '&', '*')} .sf-filter-parent .sf-input-filter, + #{if(&, '&', '*')} .sf-filter-parent .sf-input-filter:focus, + #{if(&, '&', '*')} .sf-filter-parent .sf-input-group.sf-input-focus .sf-input-filter, + #{if(&, '&', '*')} .sf-filter-parent .sf-input-group.sf-control-wrapper.sf-input-focus .sf-input-filter { + padding: $ddl-list-filter-text-indent; + } + + #{if(&, '&', '*')} .sf-input-group { + margin-bottom: 0; + } + + #{if(&, '&', '*')} .sf-ddl-footer, + #{if(&, '&', '*')} .sf-ddl-header { + cursor: default; + line-height: $ddl-line-height; + font-size: $ddl-text-size; + text-indent: $ddl-text-size; + background: $ddl-list-bg-color; + font-weight: 500; + } + } + } + + /* stylelint-disable property-no-vendor-prefix */ + .sf-ddl.sf-input-group .sf-ddl-hidden, + .sf-ddl.sf-float-input .sf-ddl-hidden { + -webkit-appearance: initial; + border: 0; + height: 0; + padding: 0; + visibility: hidden; + width: 0; + } + + .sf-ddl.sf-input-group, + .sf-ddl.sf-input-group.sf-input-focus:focus { + outline: none; + } + + .sf-dropdownbase .sf-list-item .sf-highlight { + display: inline; + font-weight: bold; + vertical-align: baseline; + } + + .sf-ddl.sf-input-group input[readonly] ~ .sf-clear-icon:not(.sf-clear-icon-hide), + .sf-float-input input[readonly] ~ .sf-clear-icon:not(.sf-clear-icon-hide), + .sf-float-input.sf-input-group input[readonly] ~ .sf-clear-icon:not(.sf-clear-icon-hide) { + opacity: 1; + } + + .sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon, + .sf-input-group input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon, + .sf-input-group.sf-control-wrapper input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon, + .sf-float-input input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon, + .sf-float-input.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon, + .sf-float-input.sf-input-group input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon, + .sf-float-input.sf-input-group.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon { + display: flex; + } + + .sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-input-group input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-input-group.sf-control-wrapper input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-input-group input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-input-group.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide { + display: none; + } + + .sf-input-group.sf-static-clear input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-input-group.sf-static-clear.sf-control-wrapper input.sf-dropdownlist.sf-input:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-static-clear input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-static-clear.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-static-clear.sf-input-group input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide, + .sf-float-input.sf-static-clear.sf-input-group.sf-control-wrapper input.sf-dropdownlist:not(:valid):first-child ~ .sf-clear-icon.sf-clear-icon-hide { + cursor: pointer; + display: flex; + } + + .sf-ddl.sf-input-group { + .sf-input-value, + .sf-input-value:focus { + font-family: $ddl-input-font-family; + font-size: $ddl-input-font-size; + height: auto; + margin: $ddl-zero-value; + outline: none; + width: 100%; + overflow: hidden; + } + + input[readonly].sf-input, + input[readonly], + .sf-dropdownlist { + pointer-events: none; + } + } + + .sf-data-form .sf-ddl.sf-input-group.sf-control-container input[readonly].sf-input.sf-dropdownlist { + cursor: pointer; + pointer-events: auto; + } + + .sf-ddl.sf-popup.sf-popup-open .sf-list-item.sf-disabled { + opacity: .7; + pointer-events: none; + } + + ejs-autocomplete, + ejs-combobox, + ejs-dropdownlist { + display: block; + } + + .sf-small .sf-ddl#{&}.sf-popup, + .sf-input-group.sf-ddl.sf-small { + #{if(&, '&', '*')} .sf-list-item { + font-size: $ddl-small-list-font-size; + } + + #{if(&, '&', '*')} .sf-list-group-item { + font-size: $ddl-small-list-font-size; + } + } + + .sf-small.sf-ddl#{&}.sf-popup, + .sf-input-group.sf-ddl.sf-small { + #{if(&, '&', '*')} .sf-list-item { + font-size: $ddl-small-list-font-size; + } + + #{if(&, '&', '*')} .sf-list-group-item { + font-size: $ddl-small-list-font-size; + } + } + + .sf-content-placeholder.sf-ddl.sf-placeholder-ddl, + .sf-content-placeholder.sf-autocomplete.sf-placeholder-autocomplete, + .sf-content-placeholder.sf-combobox.sf-placeholder-combobox { + background-size: 300px 33px; + min-height: 33px; + } + + .sf-ddl.sf-popup.sf-resize .sf-resizer-right { + bottom: 0; + right: 0; + cursor: nwse-resize; + height: 15px; + position: absolute; + width: 15px; + } + + .sf-ddl.sf-popup.sf-resize .sf-resizer-right { + background: transparent; + color: rgb(221, 218, 218); + } + + .sf-ddl .sf-input-group-icon.sf-ddl-icon.sf-icon-container { + display: flex; + align-items: center; + } + + .sf-ddl .sf-input-group-icon.sf-ddl-icon.sf-icon-rotate { + transform: rotate(180deg); + transition: transform 0.3s ease; + } + + .sf-ddl .sf-input-group-icon.sf-ddl-icon.sf-icon-normal { + transform: rotate(0deg); + transition: transform 0.3s ease; + } + + .sf-ddl.sf-popup .sf-ddl-header { + position: sticky; + top: 0; + z-index: 1; + } + + .sf-ddl.sf-popup .sf-ddl-footer { + bottom: 0; + position: sticky; + z-index: 1; + } +} diff --git a/components/dropdowns/styles/drop-down-list/_material3-definition.scss b/components/dropdowns/styles/drop-down-list/_material3-definition.scss new file mode 100644 index 0000000..2dd29a3 --- /dev/null +++ b/components/dropdowns/styles/drop-down-list/_material3-definition.scss @@ -0,0 +1,118 @@ +$ddl-input-font-size: $text-sm !default; +$ddl-zero-value: 0 !default; +$border-type: solid !default; +$border-size: 1px !default; +$ddl-default-border-color: rgba($primary) !default; +$ddl-input-border: $border-size $border-type !default; +$ddl-input-font-family: inherit !default; +$ddl-input-margin-bottom: 4px !default; +$ddl-input-padding: 8px $ddl-zero-value 4px !default; +$ddl-input-group-border-width: $ddl-zero-value !default; +$ddl-active-font-color: rgba($content-text-color) !default; +$ddl-list-search-icon-padding: 12px 8px 8px !default; +$ddl-list-filter-text-indent: 4px 16px 4px !default; +$ddl-bigger-list-font-size: 14px !default; +$ddl-list-box-shadow-color: rgba(0, 0, 0, .21) !default; +$ddl-filter-box-shadow-color: rgba(0, 0, 0, .3) !default; +$ddl-popup-background-color: $flyout-bg-color !default; +$ddl-filter-border: 0 !default; +$ddl-filter-top-border: 0 !default; +$ddl-filter-padding: 0 !default; +$ddl-filter-box-shadow: 0 1.5px 5px -2px $ddl-filter-box-shadow-color !default; +$ddl-filter-background-color: $flyout-bg-color !default; +$ddl-clear-icon-margin-right: 66px !default; +$ddl-back-icon-margin: 0 10px 0 -52px !default; +$ddl-back-icon-padding: 0 8px !default; +$ddl-popup-shadow: $shadow-md !default; +$ddl-filter-margin: 0 !default; +$ddl-icon-hover-bg: rgba($content-text-color, .08) !default; +$ddl-small-list-font-size: $text-xs !default; +$ddl-bigger-small-list-font-size: $text-sm !default; +$ddl-error-border-color: rgba($border-error) !default; +$ddl-line-height: 36px !default; +$ddl-text-size: 14px !default; +$ddl-list-bg-color: $flyout-bg-color !default; +@include export-module('dropdownlist-Material3') { + .sf-ddl.sf-control-wrapper .sf-ddl-icon::before { + transform: rotate(0deg); + transition: transform 300ms ease; + } + + .sf-ddl.sf-control-wrapper.sf-icon-anim .sf-ddl-icon::before { + transform: rotate(180deg); + transition: transform 300ms ease; + } + + .sf-dropdownbase .sf-list-item.sf-active.sf-hover { + color: $ddl-active-font-color; + } + + .sf-input-group:not(.sf-disabled) .sf-control#{&}.sf-dropdownlist ~ .sf-ddl-icon:active, + .sf-input-group:not(.sf-disabled) .sf-control#{&}.sf-dropdownlist ~ .sf-ddl-icon:hover, + .sf-input-group:not(.sf-disabled) .sf-back-icon:active, + .sf-input-group:not(.sf-disabled) .sf-back-icon:hover, + #{&}.sf-popup.sf-ddl .sf-input-group:not(.sf-disabled) .sf-clear-icon:active, + #{&}.sf-popup.sf-ddl .sf-input-group:not(.sf-disabled) .sf-clear-icon:hover { + background: $ddl-icon-hover-bg; + } + + .sf-input-group .sf-ddl-icon:not(:active)::after { + animation: none; + } + + .sf-ddl#{&}.sf-popup { + border: 0; + box-shadow: $ddl-popup-shadow; + margin-top: 2px; + } + + #{&}.sf-popup.sf-ddl .sf-dropdownbase { + min-height: 26px; + border-radius: 4px; + } + + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon { + margin: 0 6px; + min-height: 30px; + min-width: 30px; + } + + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon, + .sf-small.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon { + min-height: 22px; + min-width: 22px; + } + + .sf-input-group.sf-ddl, + .sf-input-group.sf-ddl .sf-input, + .sf-input-group.sf-ddl .sf-ddl-icon { + background: transparent; + } + + .sf-ddl.sf-ddl-device.sf-ddl-devicsf-filter .sf-input-group:hover:not(.sf-disabled):not(.sf-float-icon-left), + .sf-ddl.sf-ddl-device.sf-ddl-devicsf-filter .sf-input-group.sf-control-wrapper:hover:not(.sf-disabled):not(.sf-float-icon-left) { + border-bottom-width: 0; + } + + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group.sf-small .sf-clear-icon, + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon, + .sf-small#{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group .sf-clear-icon, + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group.sf-input-focus.sf-small .sf-clear-icon, + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group.sf-input-focus .sf-clear-icon, + .sf-small#{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-input-group.sf-input-focus .sf-clear-icon { + margin: 4px; + } + + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group .sf-input-filter, + .sf-small#{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group .sf-input-filter, + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group.sf-small .sf-input-filter, + .sf-small #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group.sf-input-focus .sf-input-filter, + .sf-small#{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group.sf-input-focus .sf-input-filter, + #{&}.sf-popup.sf-ddl:not(.sf-ddl-device) .sf-filter-parent .sf-input-group.sf-small.sf-input-focus .sf-input-filter { + padding: 4px 5px 4px 12px; + } + + .sf-ddl.sf-popup.sf-outline .sf-filter-parent { + padding: 4px 8px; + } +} diff --git a/components/dropdowns/styles/drop-down-list/_theme.scss b/components/dropdowns/styles/drop-down-list/_theme.scss new file mode 100644 index 0000000..21524c2 --- /dev/null +++ b/components/dropdowns/styles/drop-down-list/_theme.scss @@ -0,0 +1,14 @@ +@include export-module('dropdownlist-theme') { + #{&}.sf-popup { + border-color: $ddl-default-border-color; + } + + .sf-float-input.sf-input-group.sf-ddl.sf-control.sf-icon-anim > .sf-float-text, + .sf-float-input.sf-input-focus.sf-input-group.sf-ddl.sf-control.sf-keyboard > .sf-float-text { + color: $ddl-active-font-color; + } + + .sf-input-group.sf-control-wrapper.sf-ddl.sf-error { + border-bottom-color: $ddl-error-border-color; + } +} \ No newline at end of file diff --git a/components/buttons/styles/check-box/material3.scss b/components/dropdowns/styles/drop-down-list/material3.scss similarity index 56% rename from components/buttons/styles/check-box/material3.scss rename to components/dropdowns/styles/drop-down-list/material3.scss index 9eeb62e..84409d0 100644 --- a/components/buttons/styles/check-box/material3.scss +++ b/components/dropdowns/styles/drop-down-list/material3.scss @@ -1,2 +1,3 @@ +@import '../../base/themes/material3.scss'; @import 'material3-definition.scss'; @import 'all.scss'; diff --git a/components/dropdowns/styles/material3.scss b/components/dropdowns/styles/material3.scss new file mode 100644 index 0000000..4710e5e --- /dev/null +++ b/components/dropdowns/styles/material3.scss @@ -0,0 +1,5 @@ +@import '../base/themes/material3.scss'; +@import 'drop-down-base/material3-definition.scss'; +@import 'drop-down-base/all.scss'; +@import 'drop-down-list/material3-definition.scss'; +@import 'drop-down-list/all.scss'; diff --git a/components/splitbuttons/tsconfig.json b/components/dropdowns/tsconfig.json similarity index 99% rename from components/splitbuttons/tsconfig.json rename to components/dropdowns/tsconfig.json index e580aa1..3ab8c23 100644 --- a/components/splitbuttons/tsconfig.json +++ b/components/dropdowns/tsconfig.json @@ -39,4 +39,4 @@ "test-report" ], // Exclude these directories from compilation. "compileOnSave": false // Disable Compile-on-Save. -} \ No newline at end of file +} diff --git a/components/grids/CHANGELOG.md b/components/grids/CHANGELOG.md new file mode 100644 index 0000000..f56cb07 --- /dev/null +++ b/components/grids/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +## [Unreleased] + +## 31.1.17 (2025-09-05) + +### Data Grid + +The Syncfusion® React `Data Grid component` is a fast and feature-rich UI component built specifically for React. It enables high-performance data display in tabular formats and integrates smoothly into React applications. Supporting both local and remote data sources, it efficiently manages complex data operations while maintaining responsiveness and flexibility. + +**Key features** + +- **Editing** : Perform inline CRUD operations (add, edit, and delete) using toolbar actions, built-in methods, or keyboard shortcuts for a seamless editing experience. + +- **Paging** : Efficiently handle large datasets with built-in pagination. Supports both client-side and server-side paging strategies to optimize performance. + +- **Filtering** : Includes a filter bar with customizable UI for selected columns. Features numeric input fields for number columns, date pickers for date columns, and text boxes that support both string and numeric expressions. + +- **Sorting** : Sort data by clicking column headers, with support for multi-column sorting. + +- **Searching** : Quickly search data using a toolbar-integrated search box. + +- **Toolbar** : Customizable toolbar with built-in actions such as Add, Edit, Delete, and Search. + +- **Customization** : Supports custom styling, icons, images, and component injection within content and header cells to enhance flexibility and visual presentation. + +- **Accessibility and Keyboard** : Fully compliant with WCAG 2.1 standards, offering robust screen reader and keyboard navigation support. + +- **Globalization** : Automatically adapts dates, numbers, and text formats for global audiences. \ No newline at end of file diff --git a/components/grids/README.md b/components/grids/README.md new file mode 100644 index 0000000..0e1a69e --- /dev/null +++ b/components/grids/README.md @@ -0,0 +1,91 @@ +# React Data Grid Components + +## What's Included in the React Grid Package + +The React Grid package includes the Data Grid component. + +### React Data Grid + +The **Syncfusion React Data Grid** is a high-performance, feature-rich component designed for building scalable, responsive, and data-intensive web applications. Engineered to meet the requirements of enterprise-grade solutions, it provides a robust architecture that supports dynamic data handling, seamless integration with modern react frameworks, and a highly customizable interface. + +Ideal for react applications requiring structured data presentation, real-time interaction, and flexible configuration, the grid supports essential functionalities such as sorting, filtering, paging, and editing. Extensibility is achieved through custom templates and a comprehensive API surface, enabling integration with complex business logic and external systems. + +Explore the demo [here](https://react.syncfusion.com/grid). + +--- + +**Key Features** + +- **Sorting** + Enables column-based sorting with support for single and multi-column configurations. Sorting operations are optimized for performance and consistency, allowing structured data to be organized efficiently across large datasets. + +- **Filtering** + Provides a built-in filter bar with customizable filter types per column. Supports text, number, date, and dropdown filters, enabling precise data segmentation and contextual filtering based on business logic. + +- **Editing** + Facilitates inline CRUD operations including add, edit, and delete actions. Editing is integrated with toolbar controls and supports validation, making it suitable for transactional data entry and real-time updates. + +- **Toolbar** + Offers a configurable toolbar with built-in actions such as Add, Edit, Delete, and Search. Toolbar elements can be extended or replaced with custom components to align with specific operational workflows. + +- **Searching** + Includes a responsive search box within the toolbar for quick data lookup. Supports keyword-based filtering across multiple columns, improving accessibility to relevant records in large datasets. + +- **Paging** + Manages large volumes of data using built-in pagination. Supports both client-side and server-side paging strategies to ensure scalable performance and efficient data navigation in distributed environments. + +- **Customization** + Allows custom cell rendering, conditional styling, and layout adjustments. Enables integration with design systems and branding guidelines, supporting tailored visual experiences and functional enhancements. + +- **Template Extensibility** + Supports column and row templates for embedding custom components, applying conditional formatting, and creating rich, interactive visual layouts. Template logic can be used to integrate charts, buttons, or nested views within grid cells. + +- **Aggregates** + Displays summary values such as totals, averages, minimums, and maximums using built-in aggregate functions. Aggregation logic can be customized to support analytical dashboards and reporting interfaces. + +- **Interactivity** + Supports clickable headers, row selection, and keyboard navigation. Enhances engagement through responsive UI behavior and intuitive controls, suitable for complex data exploration scenarios. + +- **Accessibility** + Compliant with WCAG 2.1 standards, ensuring compatibility with screen readers, keyboard navigation, and assistive technologies. Designed to meet accessibility requirements for public sector and regulated environments. + +- **Globalization** + Adapts dates, numbers, currencies, and text formats for international audiences. Includes built-in support for localization and internationalization (i18n), enabling deployment across multilingual and multicultural platforms. + +- **Robust API** + Provides a comprehensive and extensible API for programmatic control over grid behavior, data updates, and event handling. Supports integration with external systems, custom business logic, and advanced workflow automation. + +

+Trusted by the world's leading companies + + Syncfusion logo + +

+ +## Setup + +To install `grid` and its dependent packages, use the following command, + +```sh +npm install @syncfusion/react-grid +``` + +## Support + +Product support is available through following mediums. + +* [Support ticket](https://support.syncfusion.com/support/tickets/create) - Guaranteed Response in 24 hours | Unlimited tickets | Holiday support +* Live chat + +## Changelog +Check the changelog [here](https://github.com/syncfusion/react-ui-components/blob/master/components/grid/CHANGELOG.md). Get minor improvements and bug fixes every week to stay up to date with frequent updates. + +## License and copyright + +> This is a commercial product and requires a paid license for possession or use. Syncfusion’s licensed software, including this component, is subject to the terms and conditions of Syncfusion's [EULA](https://www.syncfusion.com/eula/es/). To acquire a license for [React UI components](https://www.syncfusion.com/react-components), you can [purchase](https://www.syncfusion.com/sales/products) or [start a free 30-day trial](https://www.syncfusion.com/account/manage-trials/start-trials). + +> A [free community license](https://www.syncfusion.com/products/communitylicense) is also available for companies and individuals whose organizations have less than $1 million USD in annual gross revenue and five or fewer developers. + +See [LICENSE FILE](https://github.com/syncfusion/react-ui-components/blob/master/license?utm_source=npm&utm_campaign=notification) for more info. + +© Copyright 2025 Syncfusion, Inc. All Rights Reserved. The Syncfusion Essential Studio license and copyright applies to this distribution. diff --git a/components/navigations/gulpfile.js b/components/grids/gulpfile.js similarity index 100% rename from components/navigations/gulpfile.js rename to components/grids/gulpfile.js diff --git a/components/notifications/license b/components/grids/license similarity index 100% rename from components/notifications/license rename to components/grids/license diff --git a/components/popups/package.json b/components/grids/package.json similarity index 56% rename from components/popups/package.json rename to components/grids/package.json index 684a44d..07d03c7 100644 --- a/components/popups/package.json +++ b/components/grids/package.json @@ -1,7 +1,7 @@ { - "name": "@syncfusion/react-popups", - "version": "30.1.37", - "description": "A package of Pure React popup components such as Tooltip that is used to display information or messages in separate pop-ups.", + "name": "@syncfusion/react-grid", + "version": "31.1.17", + "description": "A high-performance React Data Grid component designed for modern web applications.", "author": "Syncfusion Inc.", "license": "SEE LICENSE IN license", "keywords": [ @@ -9,20 +9,7 @@ "web-components", "react", "syncfusion-react", - "library", - "dialog", - "modal", - "popup", - "alert", - "tooltip", - "hint", - "spinner", - "waiting-popup", - "loading-indicator", - "loader", - "busy-indicator", - "waitingfor-loader", - "react-popups" + "react-grid" ], "repository": { "type": "git", @@ -32,8 +19,16 @@ "module": "./index.js", "readme": "README.md", "dependencies": { - "@syncfusion/react-base": "~30.1.37", - "@syncfusion/react-buttons": "~30.1.37" + "@syncfusion/react-base": "~31.1.17", + "@syncfusion/react-buttons": "~31.1.17", + "@syncfusion/react-calendars": "~31.1.17", + "@syncfusion/react-data": "~31.1.17", + "@syncfusion/react-dropdowns": "~31.1.17", + "@syncfusion/react-inputs": "~31.1.17", + "@syncfusion/react-navigations": "~31.1.17", + "@syncfusion/react-notifications": "~31.1.17", + "@syncfusion/react-popups": "~31.1.17", + "@syncfusion/react-pager": "~31.1.17" }, "devDependencies": { "gulp": "4.0.2", diff --git a/components/grids/src/grid/components/Column.tsx b/components/grids/src/grid/components/Column.tsx new file mode 100644 index 0000000..026e289 --- /dev/null +++ b/components/grids/src/grid/components/Column.tsx @@ -0,0 +1,399 @@ +import { + useEffect, + useRef, + useMemo, + useCallback, + memo, + JSX, + RefObject, + MemoExoticComponent, + ReactElement +} from 'react'; +import { + AggregateCellClassEvent, + CellType, + ColumnType +} from '../types'; +import { IGrid } from '../types/grid.interfaces'; +import { SortDescriptorModel } from '../types/sort.interfaces'; +import { MutableGridSetter } from '../types/interfaces'; +import { + useGridComputedProvider, + useGridMutableProvider +} from '../contexts'; +import { useColumn } from '../hooks'; +import { isNullOrUndefined, SanitizeHtmlHelper, SvgIcon } from '@syncfusion/react-base'; +import { Checkbox } from '@syncfusion/react-buttons'; +import { ArrowUpIcon, ArrowDownIcon } from '@syncfusion/react-icons'; +import { ColumnProps, IColumnBase, ColumnRef } from '../types/column.interfaces'; + +// CSS class constants following enterprise naming convention +const CSS_HEADER_CELL_DIV: string = 'sf-headercelldiv'; +const CSS_HEADER_TEXT: string = 'sf-headertext'; +const CSS_SORT_ICON: string = 'sf-sortfilterdiv sf-icons'; +const CSS_SORT_NUMBER: string = 'sf-sortnumber'; +const CSS_DESCENDING_SORT: string = 'sf-descending sf-icon-descending'; +const CSS_ASENDING_SORT: string = 'sf-ascending sf-icon-ascending'; + +/** + * ColumnBase component renders a table cell (th or td) with appropriate content + * + * @component + * @private + * @param {IColumnBase} props - Component properties + * @param {RefObject} ref - Forwarded ref to expose internal elements + * @returns {JSX.Element} The rendered table cell (th or td) + */ +const ColumnBase: MemoExoticComponent<(props: Partial) => JSX.Element> = memo((props: Partial) => { + const grid: Partial & Partial = useGridComputedProvider(); + const { onHeaderCellRender, onCellRender, onAggregateCellRender, enableHtmlSanitizer, getColumnByField, + textWrapSettings, clipMode } = grid; + const { isInitialBeforePaint, cssClass, evaluateTooltipStatus, isInitialLoad } = useGridMutableProvider(); + + // Get column-specific APIs and properties + const { publicAPI, privateAPI } = useColumn(props); + + const { + cellType, + visibleClass, + alignHeaderClass, + alignClass, + formattedValue + } = privateAPI; + + const { ...column } = publicAPI; + + const { + index, + field, + headerText, + disableHtmlEncode, + allowSort, + customAttributes, + headerCellClass, + dataCellClass + } = column; + const aggregateCellClass: string | ((props?: AggregateCellClassEvent) => string) = props?.cell?.aggregateColumn?.aggregateCellClass; + + // Create ref for the cell element + const cellRef: RefObject = useRef({ + cellRef: useRef(null) + }); + + /** + * Handle header cell info event + */ + const handleHeaderCellInfo: Function = useCallback(() => { + if (onHeaderCellRender && cellRef.current?.cellRef.current) { + onHeaderCellRender({ + node: cellRef.current.cellRef.current, + cell: props.cell, + column: column + }); + } + }, []); + + /** + * Handle aggregate cell info event + */ + const handleAggregateCellInfo: Function = useCallback(() => { + if (onAggregateCellRender && cellRef.current?.cellRef.current) { + onAggregateCellRender({ + rowData: props.row.data, + cell: cellRef.current.cellRef.current, + column: props.cell.aggregateColumn + }); + } + }, []); + + /** + * Handle query cell info event + */ + const handleQueryCellInfo: Function = useCallback(() => { + if (onCellRender && cellRef.current?.cellRef.current) { + onCellRender({ + cell: cellRef.current.cellRef.current, + column: column, + rowData: props.row.data, + colSpan: props.cell.colSpan, + rowSpan: props.cell.rowSpan + }); + } + }, []); + + useEffect(() => { + if (column.clipMode === 'Clip' || (!column.clipMode && clipMode === 'Clip')) { + if (cellRef.current?.cellRef.current?.classList?.contains?.('sf-ellipsistooltip')) { + cellRef.current?.cellRef.current?.classList?.remove?.('sf-ellipsistooltip'); + } + cellRef.current?.cellRef.current?.classList?.add?.('sf-gridclip'); + } else if (column.clipMode === 'EllipsisWithTooltip' || (!column.clipMode && clipMode === 'EllipsisWithTooltip') + && !(textWrapSettings?.enabled && (textWrapSettings.wrapMode === 'Content' + || textWrapSettings.wrapMode === 'Both'))) { + if (column.type !== 'checkbox' && evaluateTooltipStatus(cellRef.current?.cellRef.current)) { + if (cellRef.current?.cellRef.current?.classList?.contains?.('sf-gridclip')) { + cellRef.current?.cellRef.current?.classList?.remove?.('sf-gridclip'); + } + cellRef.current?.cellRef.current?.classList?.add?.('sf-ellipsistooltip'); + } + } + }, [column.clipMode, clipMode]); + + /** + * Trigger appropriate cell info events based on cell type + */ + useEffect(() => { + if (isInitialBeforePaint.current) { return; } + if (cellType === CellType.Header) { + handleHeaderCellInfo(); + } else if (cellType === CellType.Summary) { + handleAggregateCellInfo(); + } else if (column?.uid !== 'empty-cell-uid') { + handleQueryCellInfo(); + } + }, [formattedValue, handleHeaderCellInfo, handleQueryCellInfo, handleAggregateCellInfo, isInitialBeforePaint.current]); + + useEffect(() => { + if (isInitialBeforePaint.current) { return; } + if (!isInitialLoad && column?.uid === 'empty-cell-uid') { + handleQueryCellInfo(); + } + }, [formattedValue, isInitialLoad, handleQueryCellInfo, isInitialBeforePaint.current]); + + const headerSortProperties: { index: number, className: string, direction: string } = useMemo(() => { + if (cellType !== CellType.Header) { return null; } + const sortedColumn: SortDescriptorModel[] = grid.sortSettings?.columns; + let index: number | null = null; + let cssSortClassName: string = ''; + let direction: string = 'none'; + for (let i: number = 0, len: number = sortedColumn?.length; i < len; i++) { + if (column.field === sortedColumn?.[parseInt(i.toString(), 10)].field) { + index = sortedColumn?.length > 1 ? i + 1 : null; + direction = sortedColumn?.[parseInt(i.toString(), 10)].direction; + cssSortClassName = sortedColumn?.[parseInt(i.toString(), 10)].direction === 'Ascending' ? CSS_ASENDING_SORT : + sortedColumn?.[parseInt(i.toString(), 10)].direction === 'Descending' ? CSS_DESCENDING_SORT : ''; + } + } + return { index: index, className: cssSortClassName, direction: direction }; + }, [grid.sortSettings]); + + /** + * Method to sanitize any suspected untrusted strings and scripts before rendering them. + * + * @param {string} value - Specifies the html value to sanitize + * @returns {string} Returns the sanitized html string + */ + + const sanitizeContent: (value: string) => string | JSX.Element = useCallback((value: string): string | JSX.Element => { + let sanitizedValue: string; + if (enableHtmlSanitizer) { + sanitizedValue = SanitizeHtmlHelper.sanitize(value); + } else { + sanitizedValue = value; + } + if (cellType === CellType.Data && getColumnByField?.(column.field)?.type === ColumnType.Boolean && column.displayAsCheckBox) { + const checked: boolean = isNaN(parseInt(sanitizedValue?.toString(), 10)) ? sanitizedValue === 'true' : + parseInt(sanitizedValue.toString(), 10) > 0; + return ; + } else { + return sanitizedValue; + } + }, [getColumnByField]); + + /** + * Memoized header cell content + */ + const headerCellContent: JSX.Element | null = useMemo(() => { + if (cellType !== CellType.Header) { return null; } + + // Extract existing className from customAttributes to avoid duplication + const existingClassName: string = customAttributes.className; + + // Create array of unique class names to avoid duplicates + const classNames: string[] = props.cell.className.split(' '); + + // Add existing classes from customAttributes (includes cell type classes from Row.tsx) + if (!isNullOrUndefined(existingClassName)) { + classNames.push(...existingClassName.split(' ').filter((cls: string) => cls.trim())); + } + + // Add alignment class if not already present + classNames.push(alignHeaderClass); + + // Add custom header cell class. + classNames.push(!isNullOrUndefined(headerCellClass) ? (typeof headerCellClass === 'function' ? + headerCellClass({rowIndex: props.row.index, column}) : headerCellClass) : ''); + + // Remove duplicates and join + const finalClassName: string = [...new Set(classNames)].filter((cls: string) => cls).join(' '); + const content: string | JSX.Element = !isNullOrUndefined(props.cell.column.headerTemplate) ? formattedValue as ReactElement + : sanitizeContent(formattedValue as string || headerText || field); + + return ( + +
+ {headerSortProperties.index && {headerSortProperties.index}} + +
+ {allowSort && grid?.sortSettings?.enabled && +
+ {headerSortProperties.direction === 'Ascending' ? : + headerSortProperties.direction === 'Descending' ? : } +
} + + ); + }, [ + cellType, + index, + customAttributes, + headerCellClass, + alignHeaderClass, + visibleClass, + formattedValue, + field, + headerText, + disableHtmlEncode, + props.row?.index, + grid.sortSettings + ]); + + /** + * Memoized data cell content + */ + const dataCellContent: JSX.Element | null = useMemo(() => { + if (cellType !== CellType.Data) { return null; } + + // Extract existing className from customAttributes to avoid duplication + const existingClassName: string = customAttributes.className; + + // Create array of unique class names to avoid duplicates + const classNames: string[] = props.cell.className.split(' '); + + // Add existing classes from customAttributes (includes cell type classes from Row.tsx) + if (!isNullOrUndefined(existingClassName)) { + classNames.push(...existingClassName.split(' ').filter((cls: string) => cls.trim())); + } + + // Add alignment class if not already present + classNames.push(alignClass); + + // Add custom content cell class. + classNames.push(!isNullOrUndefined(dataCellClass) ? (typeof dataCellClass === 'function' ? + dataCellClass({rowData: props.row.data, rowIndex: props.row.index, column}) : dataCellClass) : ''); + + // Remove duplicates and join + const finalClassName: string = [...new Set(classNames)].filter((cls: string) => cls).join(' '); + const content: string | JSX.Element = !isNullOrUndefined(props.cell.column.template) ? formattedValue as ReactElement + : sanitizeContent(formattedValue as string); + + return ( + + ); + }, [ + cellType, + customAttributes, + dataCellClass, + alignClass, + visibleClass, + formattedValue, + index, + disableHtmlEncode, + props.row?.index + ]); + + /** + * Memoized summary cell content + */ + const summaryCellContent: JSX.Element | null = useMemo(() => { + if (cellType !== CellType.Summary) { return null; } + + // Extract existing className from customAttributes to avoid duplication + const existingClassName: string = customAttributes.className; + + // Create array of unique class names to avoid duplicates + const classNames: string[] = []; + + // Add existing classes from customAttributes (includes cell type classes from Row.tsx) + classNames.push(...existingClassName.split(' ').filter((cls: string) => cls.trim())); + + // Add alignment class if not already present + classNames.push(alignClass); + + // Add custom aggregate class. + classNames.push(!isNullOrUndefined(aggregateCellClass) ? (typeof aggregateCellClass === 'function' ? + aggregateCellClass({rowData: props.row.data, rowIndex: props.row.index, column}) : aggregateCellClass) : ''); + + // Remove duplicates and join + const finalClassName: string = [...new Set(classNames)].filter((cls: string) => cls).join(' '); + const content: string | JSX.Element = props.cell.isTemplate ? formattedValue as ReactElement + : sanitizeContent(formattedValue as string); + return ( + + ); + }, [ + cellType, + customAttributes, + aggregateCellClass, + alignClass, + visibleClass, + formattedValue, + index, + disableHtmlEncode, + props.row?.index + ]); + + // Return the appropriate cell content based on cell type + return cellType === CellType.Header ? headerCellContent : cellType === CellType.Summary ? summaryCellContent : dataCellContent; +} +); + +/** + * Set display name for debugging purposes + */ +ColumnBase.displayName = 'ColumnBase'; + +/** + * Column component for declarative usage in user code + * + * @component + * @example + * ```tsx + * + * ``` + * @param {Partial} _props - Column configuration properties + * @returns {JSX.Element} ColumnBase component with the provided properties + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const Column: (props: Partial) => JSX.Element = (_props: Partial): JSX.Element => { + return null; +}; + +/** + * Export the ColumnBase component for internal use + * + * @private + */ +export { ColumnBase }; diff --git a/components/grids/src/grid/components/Grid.tsx b/components/grids/src/grid/components/Grid.tsx new file mode 100644 index 0000000..66bd586 --- /dev/null +++ b/components/grids/src/grid/components/Grid.tsx @@ -0,0 +1,313 @@ +import { + forwardRef, + useRef, + useImperativeHandle, + Children, + RefAttributes, + ForwardRefExoticComponent, + useMemo, + ReactElement, + JSX, + RefObject, + Ref, + useEffect +} from 'react'; +import { ITooltip, Tooltip } from '@syncfusion/react-popups'; +import { SortDirection, RenderRef, ValueType } from '../types'; +import { GridProps, GridRef, IGridBase } from '../types/grid.interfaces'; +import { PagerArgsInfo } from '../types/page.interfaces'; +import { useGridComputedProps } from '../hooks'; +import { RenderBase, ConfirmDialog } from '../views'; +import { ColumnProps } from '../types/column.interfaces'; +import { GridComputedProvider, GridMutableProvider } from '../contexts'; + +/** + * The Syncfusion React Grid component is a feature-rich, customizable data grid for building responsive, high-performance applications. + * It supports advanced functionalities like sorting, filtering, paging, and editing, with flexible data binding to local or remote data sources. + * Key features include customizable columns, aggregates, row templates, and built-in support for localization. + * The component offers a robust API with methods for dynamic data manipulation and events for handling user interactions. + * + * ```typescript + * import { Grid, Columns, Column } from '@syncfusion/react-grid'; + * + * + * + * + * + * + * + * + * + * + * ``` + */ +const GridBase: ForwardRefExoticComponent & RefAttributes> = forwardRef>( + (props: Partial, ref: Ref) => { + const gridRef: RefObject = useRef(null); + const renderExposedRef: RefObject = useRef(null); + const ellipsisTooltipRef: RefObject = useRef(null); + // Update gridRef with render properties when they become available + useEffect(() => { + gridRef.current = { + ...gridRef.current, + ...renderExposedRef.current, + columns + }; + }, [renderExposedRef.current]); + const { publicAPI, privateAPI, protectedAPI } = useGridComputedProps(props, gridRef, ellipsisTooltipRef); + const { className, id, columns } = publicAPI; + const { styles, setCurrentViewData, setCurrentPage, + setTotalRecordsCount, setGridAction, setInitialLoad } = privateAPI; + const { columnsDirective } = protectedAPI; + + // Initialize gridRef with all the properties + if (gridRef.current === null) { + gridRef.current = { + // Grid specific properties + element: null, + getColumns: () => (protectedAPI?.uiColumns ?? columns).map((col: ColumnProps) => ({...col})), + currentViewData: [], + focusModule: protectedAPI.focusModule, + selectionModule: protectedAPI.selectionModule, + pageSettings: publicAPI.pageSettings, + // Filter method + filterByColumn: (fieldName: string, filterOperator: string, + filterValue: string | number | Date | boolean| number[]| string[]| Date[]| boolean[], + predicate?: string, caseSensitive?: boolean, + ignoreAccent?: boolean) => { + protectedAPI.filterModule?.filterByColumn?.(fieldName, filterOperator, filterValue, predicate, + caseSensitive, ignoreAccent); + }, + clearFilter: (fields?: string[]) => { + protectedAPI.filterModule?.clearFilter?.(fields); + }, + removeFilteredColsByField: (field?: string, isClearFilterBar?: boolean) => { + protectedAPI.filterModule?.removeFilteredColsByField?.(field, isClearFilterBar); + }, + + // Search method + search: (searchString: string) => { + protectedAPI.searchModule.search(searchString); + }, + + // Sort method + sortByColumn: (columnName: string, sortDirection: SortDirection | string, isMultiSort?: boolean) => { + protectedAPI.sortModule?.sortByColumn?.(columnName, sortDirection, isMultiSort); + }, + removeSortColumn: (columnName: string) => { + protectedAPI.sortModule?.removeSortColumn?.(columnName); + }, + clearSort: () => { + protectedAPI.sortModule?.clearSort?.(); + }, + + //page Method + goToPage: async(pageNo: number) => { + const args: PagerArgsInfo = { cancel: false, currentPage: pageNo, + previousPage: publicAPI.pageSettings.currentPage, requestType: 'paging' + }; + args.type = 'pageChanging'; + const confirmResult: boolean = await protectedAPI?.editModule?.checkUnsavedChanges?.(); + if (!confirmResult) { + return; + } + props.onPageChangeStart?.(args); + if (args.cancel) { + return; + } + setCurrentPage(pageNo); + setGridAction(args); + }, + updatePagerMessage: (message: string) => { + renderExposedRef.current.pagerModule?.updateExternalMessage(message); + }, + get selectedRowIndexes(): number[] { + return protectedAPI.selectionModule.selectedRowIndexes; + }, + getSelectedRows: () => { + return protectedAPI.selectionModule.selectedRecords as Object[]; + }, + getSelectedRecords: () => { + return protectedAPI.selectionModule.getSelectedRecords() as Object[]; + }, + getSelectedRowIndexes: () => { + return protectedAPI.selectionModule.getSelectedRowIndexes() as number[]; + }, + selectRow: (rowIndex: number, isToggle?: boolean) => { + protectedAPI.selectionModule.selectRow(rowIndex, isToggle); + }, + selectRows: (rowIndexes: number[]) => { + protectedAPI.selectionModule.selectRows(rowIndexes); + }, + selectRowByRange: (startIndex: number, endIndex: number) => { + protectedAPI.selectionModule.selectRowByRange(startIndex, endIndex); + }, + clearRowSelection: (indexes: number[]) => { + protectedAPI.selectionModule.clearRowSelection(indexes); + }, + clearSelection: () => { + protectedAPI.selectionModule.clearSelection(); + }, + + // Edit methods + isEdit: protectedAPI.editModule?.isEdit || false, + editSettings: protectedAPI.editModule?.editSettings || {}, + editRowIndex: protectedAPI.editModule?.editRowIndex || -1, + editData: (protectedAPI.editModule?.editData as Record | null) || + null, + editRow: protectedAPI.editModule?.editRow, + saveChanges: protectedAPI.editModule?.saveChanges, + cancelChanges: protectedAPI.editModule?.cancelChanges, + addRecord: protectedAPI.editModule?.addRecord, + deleteRecord: protectedAPI.editModule?.deleteRecord, + setRowData: publicAPI.setRowData, + updateRow: protectedAPI.editModule?.updateRow, + setCellValue: publicAPI.setCellValue, + validateEditForm: protectedAPI.editModule?.validateEditForm, + validateField: protectedAPI.editModule?.validateField, + + // Include all public API computed properties + ...publicAPI, + ...renderExposedRef.current + }; + } + + // Update gridRef with render properties when they become available + useEffect(() => { + gridRef.current = { + ...gridRef.current, + columns: (protectedAPI?.uiColumns ?? columns).map((col: ColumnProps) => ({...col})), + getColumns: () => (protectedAPI?.uiColumns ?? columns).map((col: ColumnProps) => ({...col})), + currentViewData: protectedAPI?.currentViewData, + editModule: protectedAPI.editModule, + // Update edit methods directly on gridRef + isEdit: protectedAPI.editModule?.isEdit, + editSettings: protectedAPI.editModule?.editSettings, + editRowIndex: protectedAPI.editModule?.editRowIndex, + editData: protectedAPI.editModule?.editData as Record | null + }; + gridRef.current.pageSettings.currentPage = protectedAPI.currentPage; + gridRef.current.pageSettings.totalRecordsCount = protectedAPI.totalRecordsCount; + }, [protectedAPI.currentPage, protectedAPI.totalRecordsCount, protectedAPI.editModule, protectedAPI.uiColumns]); + + // Expose gridRef directly through ref + useImperativeHandle(ref, () => ({ + ...gridRef.current, + ...renderExposedRef.current, + getHeaderContent: () => renderExposedRef.current.headerPanelRef, + getContent: () => renderExposedRef.current.contentPanelRef, + isEdit: protectedAPI.editModule?.isEdit, + editRow: protectedAPI.editModule?.editRow, + saveChanges: protectedAPI.editModule?.saveChanges, + cancelChanges: protectedAPI.editModule?.cancelChanges, + addRecord: protectedAPI.editModule?.addRecord, + deleteRecord: protectedAPI.editModule?.deleteRecord, + setRowData: publicAPI.setRowData, + updateRow: protectedAPI.editModule?.updateRow, + setCellValue: publicAPI.setCellValue, + validateEditForm: protectedAPI.editModule?.validateEditForm, + validateField: protectedAPI.editModule?.validateField, + getCurrentViewRecords: () => protectedAPI?.currentViewData, + get selectedRowIndexes(): number[] { + return protectedAPI.selectionModule.selectedRowIndexes; + } + }), [gridRef.current, renderExposedRef.current, protectedAPI, publicAPI]); + + // Calculate column count for accessibility + const colCount: number = useMemo(() => { + return Children.count(((columnsDirective).props as { children: ReactElement }).children); + }, [columnsDirective]); + + // Conditionally render ellipsis tooltip only when needed + const ellipsisTooltip: JSX.Element | null = useMemo(() => { + if (!privateAPI.isEllipsisTooltip) { + return null; + } + return ( +
{privateAPI.getEllipsisTooltipContent()}
} + /> + ); + }, [privateAPI.isEllipsisTooltip, id, protectedAPI.cssClass, privateAPI.getEllipsisTooltipContent]); + + // Memoize render component to prevent unnecessary re-renders + const renderComponent: JSX.Element = useMemo(() => { + return ( + + ); + }, [columnsDirective]); + + return ( + ({ + ...gridRef.current, ...publicAPI, setCurrentViewData, setCurrentPage, + setTotalRecordsCount, setGridAction, setInitialLoad + }), [publicAPI, setCurrentViewData, setCurrentPage, + setTotalRecordsCount, setGridAction, setInitialLoad])}> + +
{ + gridRef.current.element = el; + }} + id={id} + className={className} + role='grid' + tabIndex={-1} + aria-colcount={colCount} + aria-rowcount={protectedAPI?.currentViewData?.length} + style={styles} + onMouseOut={privateAPI.handleGridMouseOut} + onMouseOver={privateAPI.handleGridMouseOver} + onMouseDown={privateAPI.handleGridMouseDown} + onKeyDown={publicAPI.allowKeyboard ? privateAPI.handleGridKeyDown : undefined} + onKeyUp={privateAPI.handleGridKeyUp} + onClick={privateAPI.handleGridClick} + onDoubleClick={privateAPI.handleGridDoubleClick} + onFocus={privateAPI.handleGridFocus} + onBlur={privateAPI.handleGridBlur} + onMouseUp={props.onMouseUp} + > + {renderComponent} + {ellipsisTooltip} + {/* Add ConfirmDialog component for inline editing confirmation dialogs */} + {protectedAPI.editModule && protectedAPI.editModule?.isDialogOpen && ( + + )} +
+
+
+ ); + } +); + +/** + * Grid component that provides a data grid with sorting, filtering, and other features. + * Wraps the GridBase component with a Provider for localization and RTL support. + * + * @param {Partial} props - Configuration for the grid + * @param {RefObject} ref - Forwarded ref that exposes imperative methods + * @returns {JSX.Element} The rendered grid component + */ +export const Grid: ForwardRefExoticComponent & RefAttributes> = forwardRef>( + (props: Partial, ref: Ref) => { + return ( + + ); + }); + +export { GridBase }; + +Grid.displayName = 'Grid'; +GridBase.displayName = 'GridBase'; diff --git a/components/grids/src/grid/components/Row.tsx b/components/grids/src/grid/components/Row.tsx new file mode 100644 index 0000000..9e51c55 --- /dev/null +++ b/components/grids/src/grid/components/Row.tsx @@ -0,0 +1,377 @@ +import { + forwardRef, + useImperativeHandle, + useRef, + Children, + useEffect, + ReactElement, + useMemo, + useCallback, + memo, + JSX, + RefObject, + RefAttributes, + NamedExoticComponent, + useState, + isValidElement +} from 'react'; +import { + IRowBase, + RowRef, + ICell, CellType, RenderType, IRow +} from '../types'; +import { ColumnProps, IColumnBase, CustomAttributes } from '../types/column.interfaces'; +import { AggregateColumnProps, AggregateRowRenderEvent } from '../types/aggregate.interfaces'; +import { RowRenderEvent, ValueType } from '../types/interfaces'; +import { useGridComputedProvider, useGridMutableProvider } from '../contexts'; +import { ColumnBase } from './Column'; +import { FilterBase, InlineEditForm } from '../views'; +import { InlineEditFormRef } from '../types/edit.interfaces'; +import { IL10n, isNullOrUndefined } from '@syncfusion/react-base'; + +// CSS class constants following enterprise naming convention +const CSS_HEADER_CELL: string = 'sf-headercell'; +const CSS_FILTER_CELL: string = 'sf-filterbarcell'; +const CSS_SUMMARY_CELL: string = 'sf-summarycell'; +const CSS_SUMMARY_FIRST_CELL: string = 'sf-firstsummarycell'; +const CSS_SUMMARY_LAST_CELL: string = 'sf-lastsummarycell'; +const CSS_LAST_CELL: string = 'sf-lastcell'; +const CSS_ROW_CELL: string = 'sf-rowcell'; +const CSS_DEFAULT_CURSOR: string = ' sf-defaultcursor'; +const CSS_MOUSE_POINTER: string = ' sf-mousepointer'; +const CSS_SORT_ICON: string = ' sf-sorti-con'; +const CSS_CELL_HIDE: string = 'sf-hide'; + +/** + * RowBase component renders a table row with cells based on provided column definitions + * + * @component + * @private + * @param {IRowBase} props - Component properties + * @param {RenderType} [props.rowType=RenderType.Content] - Type of row (header or content) + * @param {object} [props.row] - Data for the row + * @param {ReactElement[]} [props.children] - Column definitions + * @param {string} [props.className] - Additional CSS class names + * @param {RefObject} ref - Forwarded ref to expose internal elements and methods + * @returns {JSX.Element} The rendered table row with cells + */ +const RowBase: NamedExoticComponent> = memo(forwardRef( + (props: IRowBase, ref: RefObject) => { + const { + rowType, + row, + children, + tableScrollerPadding, + aggregateRow, + ...attr + } = props; + const { headerRowDepth, isInitialBeforePaint, editModule, uiColumns, isInitialLoad } = useGridMutableProvider(); + const { onRowRender, onAggregateRowRender, serviceLocator, rowClass, + sortSettings, rowHeight, editSettings, columns, rowTemplate } = useGridComputedProvider(); + const rowRef: RefObject = useRef(null); + const cellsRef: RefObject[]> = useRef[]>([]); + const localization: IL10n = serviceLocator?.getService('localization'); + const editInlineFormRef: RefObject = useRef(null); + const [syncFormState, setSyncFormState] = useState(editInlineFormRef.current?.formState); + const [rowObject, setRowObject] = useState>(row); + /** + * Returns the cell options objects + * + * @returns {ICell[]} Array of cell options objects + */ + const getCells: () => ICell[] = useCallback(() => { + return cellsRef.current; + }, []); + + const inlineEditForm: JSX.Element = useMemo(() => { + // Properly check for edit permissions and active edit state + // This ensures double-click properly triggers edit mode on data rows + if (rowType === RenderType.Content && + (!editModule?.editSettings?.allowEdit || + !(editModule?.isEdit && editModule?.editRowIndex >= 0 && editModule?.editRowIndex === row.index) || + isNullOrUndefined(editModule?.originalData))) { + return null; + } + return ( + { + editInlineFormRef.current = ref; + setSyncFormState(ref?.formState); + }} + key={`edit-${row?.uid}`} + stableKey={`edit-${row?.uid}-${editModule?.editRowIndex}`} + isAddOperation={false} + columns={uiColumns ?? columns} + editData={editModule?.editData || {}} + validationErrors={editModule?.validationErrors || {}} + editRowIndex={row?.index} + rowUid={row?.uid} + onFieldChange={(field: string, value: ValueType | null) => { + if (editModule?.updateEditData) { + editModule?.updateEditData?.(field, value); + } + }} + onSave={() => editModule?.saveChanges()} + onCancel={editModule?.cancelChanges} + template={editSettings?.template} + /> + ); + }, [ + uiColumns, + columns, + editModule?.isEdit, + editModule?.editRowIndex, + editModule?.isShowAddNewRowActive, + editModule?.isShowAddNewRowDisabled, + editModule?.showAddNewRowData, + editModule?.editSettings?.showAddNewRow, + editModule?.editSettings?.newRowPosition, + editSettings?.template + ]); + + /** + * Expose internal elements through the forwarded ref + */ + useImperativeHandle(ref, () => ({ + rowRef: rowRef, + getCells, + editInlineRowFormRef: editInlineFormRef, + setRowObject + }), [getCells, editModule?.isEdit, syncFormState, rowObject]); + + /** + * Handle row data bound event for content rows + */ + const handleRowDataBound: () => void = useCallback(() => { + if (rowType === RenderType.Content && onRowRender && rowRef.current) { + const rowArgs: RowRenderEvent = { + row: rowRef.current, + data: row.data, + rowHeight: rowHeight, + isSelectable: true // Until isPartialSelection is implemented, all data rows are selectable. + }; + onRowRender(rowArgs); + if (!isNullOrUndefined(rowArgs.rowHeight)) { + rowRef.current.style.height = `${rowArgs.rowHeight}px`; + } + } + }, [rowType, rowObject, onRowRender]); + + /** + * Call rowDataBound callback after render + */ + useEffect(() => { + if (isInitialBeforePaint.current) { return; } + if (rowObject?.uid !== 'empty-row-uid') { + handleRowDataBound(); + } + }, [handleRowDataBound, inlineEditForm, rowObject, isInitialBeforePaint.current]); + + useEffect(() => { + if (isInitialBeforePaint.current) { return; } + if (!isInitialLoad && rowObject?.uid === 'empty-row-uid') { + handleRowDataBound(); + } + }, [handleRowDataBound, isInitialLoad, rowObject, isInitialBeforePaint.current]); + + /** + * Handle aggregate row data bound event for aggregate rows + */ + const handleAggregateRowDataBound: () => void = useCallback(() => { + if (rowType === RenderType.Summary && onAggregateRowRender && rowRef.current) { + const rowArgs: AggregateRowRenderEvent = { + row: rowRef.current, + rowData: row.data, + rowHeight: rowHeight + }; + onAggregateRowRender(rowArgs); + if (!isNullOrUndefined(rowArgs.rowHeight)) { + rowRef.current.style.height = `${rowArgs.rowHeight}px`; + } + } + }, [rowType, rowObject, onAggregateRowRender]); + + /** + * Call aggregateRowDataBound callback after render + */ + useEffect(() => { + if (isInitialBeforePaint.current) { return; } + handleAggregateRowDataBound(); + }, [handleAggregateRowDataBound, rowObject, isInitialBeforePaint.current]); + + + /** + * Process children to create column elements with proper props + */ + const processedChildren: JSX.Element[] = useMemo(() => { + + const childrenArray: ReactElement[] = Children.toArray(children) as ReactElement[]; + const cellOptions: ICell[] = []; + const elements: JSX.Element[] = []; + + for (let index: number = 0; index < childrenArray.length; index++) { + const child: ReactElement = childrenArray[index as number]; + + // Determine cell class based on row type and position + const cellClassName: string = rowType === RenderType.Header + ? `${CSS_HEADER_CELL}${child.props.allowSort && sortSettings?.enabled ? `${CSS_MOUSE_POINTER}` : `${CSS_DEFAULT_CURSOR}`}${rowHeight && !isNullOrUndefined(child.props.field) ? `${CSS_SORT_ICON}` : ''}${index === childrenArray.length - 1 ? ` ${CSS_LAST_CELL}` : ''}` + : rowType === RenderType.Filter ? CSS_FILTER_CELL : rowType === RenderType.Summary ? `${CSS_SUMMARY_CELL} ${tableScrollerPadding && index === 0 ? CSS_SUMMARY_FIRST_CELL : tableScrollerPadding && index === childrenArray.length - 1 ? CSS_SUMMARY_LAST_CELL : ''}` : CSS_ROW_CELL; + + const cellType: CellType = rowType === RenderType.Header ? CellType.Header : rowType === RenderType.Filter ? + CellType.Filter : rowType === RenderType.Summary ? CellType.Summary : CellType.Data; + + const colSpan: number = !child.props.field && child.props.headerText && (rowType === RenderType.Header && + (child.props.columns && child.props.columns.length) || (child.props.children && + (child.props as { children: ReactElement[] }).children.length)) ? child.props.columns?.length || + (child.props as { children: ReactElement[] }).children.length : 1; + const rowSpan: number = rowType !== RenderType.Header || (rowType === RenderType.Header && + ((child.props.columns && child.props.columns.length) || child.props.children)) ? 1 : + headerRowDepth - row.index; + + const { ...cellAttributes } = child.props.customAttributes || {}; + + // Determine if the cell is visible + const isVisible: boolean = child.props.visible !== false; + + // Build custom attributes object with proper typing + const customAttributesWithSpan: CustomAttributes = { + ...cellAttributes, + ...(child.props?.template && child.props?.templateSettings?.ariaLabel?.length > 0 ? { 'aria-label': child.props?.templateSettings?.ariaLabel } : {}), + className: `${cellClassName}${!isVisible ? ` ${CSS_CELL_HIDE}` : ''}`, + title: rowType === RenderType.Filter ? (child.props.headerText || child.props.field) + localization?.getConstant('filterBarTooltip') : undefined, + role: rowType === RenderType.Header || rowType === RenderType.Filter ? 'columnheader' : 'gridcell', + tabIndex: -1, + 'aria-colindex': index ? index + 1 : 1, + ...(colSpan > 1 ? { 'aria-colspan': colSpan } : {}) + }; + + // Create cell options object for getCells method + const cellOption: ICell = { + visible: isVisible, + isDataCell: rowType !== RenderType.Header && rowType !== RenderType.Filter, // true for data cells + isTemplate: rowType === RenderType.Header + ? Boolean(child.props.headerTemplate) + : Boolean(child.props.template), + rowID: row?.uid || '', + column: { + customAttributes: customAttributesWithSpan, + index, + ...child.props as IColumnBase, + type: uiColumns ? uiColumns?.[index as number]?.type : columns?.[index as number]?.type + }, + cellType, + colSpan: colSpan, + rowSpan: rowSpan, + index, + colIndex: index, + className: row?.uid === 'empty-row-uid' ? '' : `${cellClassName}${!isVisible ? ` ${CSS_CELL_HIDE}` : ''}` + }; + if (rowType === RenderType.Summary) { + const aggregateColumn: AggregateColumnProps = aggregateRow.columns + .find((aggregate: AggregateColumnProps) => aggregate.columnName === child.props.field); + cellOption.isDataCell = aggregateColumn ? true : false; + cellOption.isTemplate = aggregateColumn && aggregateColumn.footerTemplate ? true : false; + cellOption.aggregateColumn = aggregateColumn || {}; + } + + // Build column props + const columnProps: IColumnBase = { + row: rowObject, + cell: cellOption + }; + + // Store cell options + cellOptions.push(cellOption); + + if (rowType === RenderType.Filter) { + elements.push( + + ); + } else { + elements.push( + + ); + } + } + + // Update the ref with cell options + cellsRef.current = cellOptions; + + return elements; + }, [children, rowObject, rowType]); + + /** + * Row template + */ + const renderRowTemplate: string | ReactElement = useMemo((): string | ReactElement => { + if (rowTemplate && rowType === RenderType.Content && rowObject?.data ) { + if (typeof rowTemplate === 'string' || isValidElement(rowTemplate)) { + return rowTemplate; + } + else { + return rowTemplate(rowObject.data); + } + } + return null; + }, [rowTemplate, rowObject?.data, rowType]); + + const customRowClass: string | undefined = useMemo(() => { + if (rowType === RenderType.Content && rowObject?.uid !== 'empty-row-uid') { + return !isNullOrUndefined(rowClass) ? (typeof rowClass === 'function' ? + rowClass({rowType: 'content', rowData: rowObject.data, rowIndex: rowObject.index}) : rowClass) : undefined; + } + return undefined; + }, [rowClass, inlineEditForm, rowObject]); + const customNoRecordRowClass: string | undefined = useMemo(() => { + if (isInitialBeforePaint.current) { return undefined; } + if (rowType === RenderType.Content && !isInitialLoad && rowObject?.uid === 'empty-row-uid') { + return !isNullOrUndefined(rowClass) ? + (typeof rowClass === 'function' ? rowClass({rowType: 'content', rowIndex: 0}) : rowClass) : undefined; + } + return undefined; + }, [rowClass, isInitialLoad, rowObject, isInitialBeforePaint.current]); + const customAggregateRowClass: string | undefined = useMemo(() => { + if (isInitialBeforePaint.current) { return undefined; } + return rowType === RenderType.Summary && !isNullOrUndefined(rowClass) ? (typeof rowClass === 'function' ? + rowClass({rowType: 'aggregate', rowData: rowObject.data, rowIndex: rowObject.index}) : rowClass) : undefined; + }, [rowClass, rowObject, isInitialBeforePaint.current]); + return ( + <> + { renderRowTemplate ? renderRowTemplate : + rowType === RenderType.Content && editModule?.editSettings?.allowEdit && editModule?.isEdit && + editModule?.editRowIndex >= 0 && editModule?.editRowIndex === row.index && !isNullOrUndefined(editModule?.originalData) + ? inlineEditForm + : ( + {processedChildren} + ) + } + + ); + } +)); + +/** + * Set display name for debugging purposes + */ +RowBase.displayName = 'RowBase'; + +/** + * Export the RowBase component for use in other components + * + * @private + */ +export { RowBase }; diff --git a/components/grids/src/grid/components/index.ts b/components/grids/src/grid/components/index.ts new file mode 100644 index 0000000..91e5ae8 --- /dev/null +++ b/components/grids/src/grid/components/index.ts @@ -0,0 +1,3 @@ +export * from './Grid'; +export * from './Column'; +export * from './Row'; diff --git a/components/grids/src/grid/contexts/GridProviders.tsx b/components/grids/src/grid/contexts/GridProviders.tsx new file mode 100644 index 0000000..89585a8 --- /dev/null +++ b/components/grids/src/grid/contexts/GridProviders.tsx @@ -0,0 +1,71 @@ +import { Context, createContext, FC, JSX, ReactElement, ReactNode, useContext } from 'react'; +import { MutableGridBase } from '../types'; +import { GridRef, IGrid } from '../types/grid.interfaces'; +import { MutableGridSetter } from '../types/interfaces'; +/** + * Context for computed grid properties + */ +const GridComputedContext: Context & Partial> = + createContext & Partial>(null); + +/** + * Provider component for computed grid properties + * + * @param {Object} props - The provider props + * @param {Object} props.grid - Grid model and state setter + * @param {Object} props.children - Child components + * @returns {Object} Provider component with children + */ +export const GridComputedProvider: FC<{ + grid: Partial & Partial; + children: ReactElement | ReactNode; +}> = ({ grid, children }: { grid: Partial & Partial; children: ReactElement | ReactNode; }): JSX.Element => { + return ( + + {children} + + ); +}; + +/** + * Hook to access computed grid properties from context + * + * @returns {Object} Grid computed context + */ +export const useGridComputedProvider: () => Partial & Partial = + (): Partial & Partial => { + return useContext(GridComputedContext); + }; + +/** + * Context for mutable grid properties + */ +const GridMutableContext: Context> = createContext(null); + +/** + * Provider component for mutable grid properties + * + * @param {Object} props - The provider props + * @param {Partial} props.grid - Mutable grid properties + * @param {ReactElement | ReactNode} props.children - Child components + * @returns {JSX.Element} Provider component with children + */ +export const GridMutableProvider: FC<{ + grid: Partial; + children: ReactElement | ReactNode; +}> = ({ grid, children }: { grid: Partial; children: ReactElement | ReactNode; }): JSX.Element => { + return ( + + {children} + + ); +}; + +/** + * Hook to access mutable grid properties from context + * + * @returns {MutableGridBase} Grid mutable context + */ +export const useGridMutableProvider: () => MutableGridBase = (): MutableGridBase => { + return useContext(GridMutableContext); +}; diff --git a/components/grids/src/grid/contexts/index.ts b/components/grids/src/grid/contexts/index.ts new file mode 100644 index 0000000..a473d13 --- /dev/null +++ b/components/grids/src/grid/contexts/index.ts @@ -0,0 +1 @@ +export * from './GridProviders'; diff --git a/components/navigations/styles/v-scroll/_all.scss b/components/grids/src/grid/grid/_all.scss similarity index 100% rename from components/navigations/styles/v-scroll/_all.scss rename to components/grids/src/grid/grid/_all.scss diff --git a/components/grids/src/grid/grid/_layout.scss b/components/grids/src/grid/grid/_layout.scss new file mode 100644 index 0000000..a0fd9c2 --- /dev/null +++ b/components/grids/src/grid/grid/_layout.scss @@ -0,0 +1,727 @@ +@mixin wrap-styles { + height: Auto; + line-height: $grid-rowcell-wrap-height; + overflow-wrap: break-word; + text-overflow: clip; + white-space: normal; + word-wrap: break-word; +} + +@mixin grid-top-bottom-padding($bottom, $top) { + padding-bottom: $bottom; + padding-top: $top; +} + +@mixin grid-margin-padding($margin, $padding) { + margin: $margin; + padding: $padding; +} + +@mixin float-with-margin($float, $margin) { + float: $float; + margin: $margin; +} + +@mixin grid-padding-left-right($left, $right) { + padding-left: $left; + padding-right: $right; +} + +@mixin grid-line-height-padding-styles($lheight, $padding) { + line-height: $lheight; + padding: $padding; +} + +@mixin grid-font-size-weight-styles($size, $weight) { + font-size: $size; + font-weight: $weight; +} + +@mixin grid-border-style-weight($style, $width) { + border-style: $style; + border-width: $width; +} + +@mixin border-and-font($style, $width, $size, $weight) { + @include grid-border-style-weight($style, $width); + @include grid-font-size-weight-styles($size, $weight); +} + +@mixin grid-border-style-width-font-size-weight($style, $width, $size, $weight) { + @include grid-border-style-weight($style, $width); + @include grid-font-size-weight-styles($size, $weight); +} + +@include export-module('grid-layout') { + #{&}.sf-grid { + @include grid-border-style-weight(none $grid-border-style $grid-border-style, $grid-border-width); + border-radius: $grid-border-radius; + display: block; + font-family: $grid-content-font-family; + font-size: $grid-content-text-size; + height: auto; + position: relative; + + .sf-toolbar.sf-sticky, + .sf-gridheader.sf-sticky { + position: sticky; + z-index: 10; + } + + .sf-gridheader { + user-select: none; + border-bottom-style: $grid-border-style; + border-bottom-width: $grid-border-width; + border-top-style: $grid-border-style; + border-top-width: $grid-border-width; + + .sf-headercell { + user-select: none; + } + + thead .sf-icons:not(.sf-check, .sf-stop) { + font-size: $grid-icon-font-size; + } + + tr:first-child th { + border-top: 0 none; + } + + .sf-rightalign { + .sf-sortfilterdiv { + @include float-with-margin(left, $grid-sort-right-align-margin); + } + + .sf-sortnumber { + @include float-with-margin(left, $grid-sort-number-right-align-margin); + } + } + + .sf-centeralign.sf-headercell[aria-sort = 'none'] .sf-headercelldiv, + .sf-centeralign.sf-headercell:not([aria-sort]) .sf-headercelldiv { + padding-right: $grid-headercell-sort-padding-right; + } + + .sf-sortfilter { + .sf-rightalign .sf-headercelldiv { + padding: $grid-headercell-right-align-padding; + margin-left: 8px; + } + + .sf-headercelldiv { + padding: $grid-headercell-padding; + } + } + } + + .sf-gridcontent .sf-normaledit .sf-rowcell.sf-lastrowadded { + border-bottom: $grid-border-width $grid-border-style $grid-border-color; + border-top: 0 none $grid-border-color; + } + + .sf-gridcontent table tbody .sf-normaledit .sf-rowcell { + border-top: $grid-border-width $grid-border-style; + } + + .sf-toolbar { + border-bottom: 0; + border-left: 0; + border-right: 0; + border-top: $grid-toolbar-border $grid-border-color; + border-radius: 0; + } + + .sf-toolbar-items { + .sf-toolbar-item.sf-search-wrapper { + @include grid-top-bottom-padding($grid-toolbar-search-wrapper-padding-bottom, $grid-toolbar-search-wrapper-padding-top); + .sf-search:focus { + opacity: $grid-toolbar-search-bar-text-opacity; + } + + .sf-search::placeholder { + color: $grid-toolbar-searchwrapper-text-color; + } + + .sf-search { + margin-bottom: $grid-toolbar-search-margin-bottom; + opacity: 1; + width: $grid-toolbar-search-width; + &.sf-input-focus { + opacity: 1; + } + .sf-search-icon { + min-width: $grid-toolbar-search-icon-min-width; + svg { + margin-bottom: 3px; + } + } + } + } + } + + .sf-headercell.sf-defaultcursor { + cursor: default; + } + + .sf-table { + border-collapse: separate; + table-layout: fixed; + width: 100%; + } + + .sf-headercelldiv { + border: 0 none; + display: block; + @include grid-font-size-weight-styles($grid-header-font-size, $grid-header-font-weight); + height: $grid-header-height; + @include grid-line-height-padding-styles($grid-headercell-line-height, $grid-headercell-padding); + margin: $grid-headercell-margin; + overflow: hidden; + text-align: left; + text-transform: $grid-header-text-transform; + user-select: none; + } + + .sf-rightalign, + .sf-leftalign, + .sf-centeralign { + .sf-headercelldiv { + padding: 0 .4em; + } + } + + .sf-headercell.sf-templatecell .sf-headercelldiv { + height: auto; + min-height: $grid-header-height; + } + + .sf-headercell.sf-mousepointer { + cursor: pointer; + } + + & .sf-gridcontent { + tr:first-child td { + border-top: 0 none; + } + } + + .sf-headercell { + @include border-and-font($grid-border-style, $grid-header-border-width, $grid-header-font-size, $grid-header-font-weight); + height: $grid-headercell-height; + overflow: hidden; + padding: $grid-header-padding-top $grid-header-padding $grid-header-padding-bottom; + position: relative; + text-align: left; + letter-spacing: 0.24px; + } + + .sf-rowcell { + @include grid-border-style-weight($grid-border-style, $grid-rowcell-border-width); + display: table-cell; + font-size: $grid-content-text-size; + @include grid-line-height-padding-styles($grid-rowcell-line-height, $grid-content-padding $grid-content-right-padding); + overflow: hidden; + vertical-align: middle; + white-space: nowrap; + width: auto; + letter-spacing: 0.24px; + } + + .sf-rightalign { + &.sf-rowcell, + &.sf-summarycell, + &.sf-headercell .sf-headercelldiv { + text-align: right; + } + } + + .sf-leftalign { + &.sf-rowcell, + &.sf-summarycell, + &.sf-headercell .sf-headercelldiv { + text-align: left; + } + } + + .sf-centeralign { + &.sf-rowcell, + &.sf-summarycell, + &.sf-headercell .sf-headercelldiv { + text-align: center; + } + } + + .sf-justifyalign { + &.sf-rowcell, + &.sf-summarycell, + &.sf-headercell .sf-headercelldiv { + text-align: justify; + } + } + + &:not(.sf-grid-min-height) tr.sf-insertedrow .sf-rowcell:empty, + .sf-row.sf-emptyrow { + height: $grid-rowcell-line-height + $grid-content-padding + $grid-content-padding + 1; + } + + .sf-editedrow, + .sf-addedrow { + .sf-input-group input.sf-input, + .sf-input-group.sf-control-wrapper input.sf-input { + min-height: unset; + } + } + + &:not(.sf-grid-min-height) .sf-gridcontent { + tr td:first-child:empty, + tr.sf-row .sf-rowcell:empty { + height: $grid-rowcell-line-height + $grid-content-padding + $grid-content-padding; + } + } + + .sf-summarycell { + @include border-and-font(solid, 1px 0 0, $grid-summary-cell-font-size, $grid-header-font-weight); + height: auto; + @include grid-line-height-padding-styles($grid-summary-cell-line-height, $grid-content-padding $grid-content-right-padding); + white-space: normal; + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; + letter-spacing: 0.24px; + &.sf-lastrowcell { + border-bottom-width: 1px; + } + } + + .sf-rowcell.sf-lastrowcell { + border-bottom-width: 1px; + } + + &.sf-bothlines { + .sf-rowcell.sf-lastrowcell { + border-bottom-width: 1px; + } + .sf-rowcell { + border-width: $grid-rowcell-both-border-width; + } + .sf-filterbarcell { + border-width: $grid-filter-cell-both-border-width; + border-top-width: 1px; + } + } + + &.sf-gridheader th[rowspan] { + padding-bottom: 13px; + vertical-align: bottom; + } + + .sf-emptyrow td { + letter-spacing: 0.24px; + font-size: $grid-content-text-size; + @include grid-line-height-padding-styles($grid-rowcell-line-height, .7em); + } + + &.sf-bothlines, + .sf-filterbartable { + .sf-headercell { + border-width: $grid-headercell-both-border-width; + } + } + + .sf-filterbartable .sf-filterbarcell { + border-width: $grid-filter-cell-both-border-width; + border-top-width: 1px; + } + + &:not(.sf-rtl) tr { + & td:first-child, + & th.sf-headercell:first-child, + & th.sf-filterbarcell:first-child { + border-left-width: 0; + } + } + + .sf-hide, + th.sf-headercell.sf-hide { + display: none; + } + + .sf-rowcell, + .sf-gridcontent, + .sf-gridheader, + .sf-headercontent, + .sf-gridfooter, + .sf-summarycontent { + overflow: hidden; + vertical-align: middle; + } + + .sf-sortfilterdiv { + float: right; + height: $grid-sort-height; + @include grid-margin-padding($grid-sort-mg, $grid-sort-padding); + width: $grid-sort-width; + } + + .sf-sortnumber { + border-radius: $grid-sort-number-border-radius; + display: inline-block; + float: right; + text-align: center; + font-size: $grid-sort-number-font-size; + height: $grid-sort-number-size; + line-height: $grid-sort-number-line-height; + margin: $grid-sort-number-margin; + width: $grid-sort-number-size; + } + + &.sf-verticallines { + .sf-rowcell, + .sf-headercell, + .sf-filterbarcell { + border-width: 0 0 0 $grid-border-width; + } + } + + &.sf-hidelines { + .sf-rowcell, + .sf-headercell, + .sf-filterbarcell { + border-width: 0; + } + } + + &.sf-horizontallines { + .sf-headercell { + border-width: 0; + } + + .sf-rowcell, + .sf-filterbarcell { + border-width: $grid-border-width 0 0; + } + } + + &.sf-horizontallines, + &.sf-verticallines, + &.sf-hidelines { + .sf-rowcell.sf-lastrowcell { + border-bottom-width: $grid-border-width; + } + } + + .sf-filterbarcell { + border-collapse: collapse; + @include grid-border-style-weight($grid-border-style, $grid-filterbar-cell-border-width); + cursor: default; + height: $grid-filterbar-cell-height; + overflow: hidden; + padding: $grid-filterbar-cell-padding; + vertical-align: bottom; + } + + .sf-searchclear { + position: relative; + float: right; + } + + .sf-filterdiv, + .sf-fltrtempdiv { + padding: $grid-filter-padding; + position: relative; + text-align: center; + width: 100%; + } + + .sf-pager { + border-bottom: transparent; + border-left: transparent; + border-right: transparent; + } + + .sf-footerpadding { + @include grid-padding-left-right(0, 14px); + + .sf-lastsummarycell { + border-left: none; + border-right: 1px solid $grid-border-color; + } + } + + .sf-content:not(.sf-tooltip-content) { + -webkit-overflow-scrolling: touch; + overflow-x: auto; + overflow-y: scroll; + position: relative; + } + + .sf-headercontent { + @include grid-border-style-weight(solid, 0); + } + + .sf-griderror label { + display: inline !important; + } + + .sf-tooltip-wrap.sf-griderror { + z-index: 1000; + } + + .sf-normaledit { + border-top: 0; + padding: 0; + .sf-rowcell { + @include grid-top-bottom-padding($grid-edit-cell-padding, $grid-edit-top-cell-padding); + } + } + + &:not(.sf-row-responsive) .sf-gridcontent tr.sf-row:first-child .sf-rowcell { + border-top: 0; + } + + .sf-row { + .sf-input-group .sf-input.sf-field, + .sf-input-focus .sf-input.sf-field { + font-size: $grid-content-text-size; + @include grid-top-bottom-padding($grid-edit-input-padding-bottom, $grid-edit-input-padding-top); + } + + .sf-input-group { + margin-bottom: $grid-edit-input-margin; + margin-top: $grid-edit-input-margin-top; + vertical-align: middle; + line-height: 28.5px; + } + } + + .sf-content.sf-mac-safari::-webkit-scrollbar { + width: 7px; + } + + .sf-content.sf-mac-safari::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, .5); + border-radius: 4px; + } + + .sf-rowcell, + .sf-headercelldiv { + text-overflow: ellipsis; + white-space: nowrap; + } + + &.sf-wrap { + .sf-gridheader { + .sf-rightalign .sf-sortnumber { + margin: $grid-wrap-sort-number-margin; + } + + .sf-sortnumber { + margin: 0 16px 0 2px; + } + + .sf-sortfilterdiv { + margin: $grid-wrap-sort-icon-margin; + } + } + + .sf-rowcell, + .sf-headercelldiv { + @include wrap-styles; + } + + .sf-columnheader { + .sf-icon-group::before { + display: inline-block; + } + } + } + + + &.sf-rtl { + .sf-headercell { + border-width: $grid-rtl-headercell-border-width; + + .sf-headercelldiv { + padding: $grid-rtl-headercell-padding; + + .sf-sortnumber { + @include float-with-margin(left, $grid-rtl-sort-number-margin); + } + } + + .sf-filterbarcell input { + border-width: $grid-filterbar-input-border-width; + } + + .sf-sortfilterdiv { + @include float-with-margin(left, $grid-rtl-sort-cell-margin); + } + + &.sf-leftalign { + .sf-sortfilterdiv { + @include float-with-margin(right, $grid-sort-margin); + } + + .sf-headercelldiv { + padding: 0 25px 0 .3em; + + .sf-sortnumber { + @include float-with-margin(right, $grid-sort-right-margin); + } + } + } + + &.sf-rightalign { + .sf-sortnumber { + @include float-with-margin(left, $grid-rtl-sort-number-right-align-margin); + } + } + } + + .sf-gridheader { + .sf-rightalign .sf-sortfilterdiv { + margin: $grid-rtl-sort-cell-right-align-margin; + } + + .sf-centeralign.sf-headercell[aria-sort = 'none'] .sf-headercelldiv, + .sf-centeralign.sf-headercell:not([aria-sort]) .sf-headercelldiv { + padding-left: $grid-headercell-sort-padding-right; + } + } + + .sf-rowcell, + .sf-filterbarcell { + border-width: $grid-rowcell-border-width; + } + + .sf-lastrowcell { + border-width: $grid-border-width $grid-border-width $grid-border-width 0; + } + + tr td:first-child, + tr th:first-child { + border-left-width: $grid-rtl-header-first-cell-border-left; + } + + &.sf-bothlines, + .sf-filterbartable { + .sf-headercell { + border-width: $grid-headercell-both-border-width; + } + } + + .sf-filterbartable .sf-filterbarcell { + border-width: $grid-filter-cell-both-border-width; + } + + &.sf-bothlines { + .sf-filterbarcell, + .sf-rowcell, + .sf-rowcell.sf-lastrowcell { + border-width: $grid-filter-cell-both-border-width; + } + } + + &.sf-verticallines { + .sf-rowcell, + .sf-filterbarcell { + border-width: 1px 0 0 $grid-border-width; + } + } + + &.sf-hidelines { + .sf-rowcell, + .sf-headercell, + .sf-filterbarcell { + border-width: 0; + } + } + + &.sf-horizontallines { + .sf-rowcell, + .sf-filterbarcell { + border-width: $grid-border-width 0 0; + } + } + + &.sf-horizontallines, + &.sf-verticallines, + &.sf-hidelines { + .sf-rowcell.sf-lastrowcell { + border-bottom-width: $grid-border-width; + } + } + + .sf-searchclear, + .sf-cc-searchdiv span.sf-ccsearch-icon.sf-icons { + float: left; + } + + .sf-footerpadding { + @include grid-padding-left-right(14px, 0); + + tr.sf-summaryrow td.sf-lastsummarycell:last-child { + border-right: none; + border-left: 1px solid $grid-border-color; + } + } + + &.sf-wrap .sf-rightalign .sf-sortnumber { + margin: $grid-rtl-wrap-sort-number-margin; + } + &.sf-wrap .sf-sortnumber { + margin: 0 5px 0 7px; + } + } + + &.sf-wrap .sf-columnheader, + .sf-columnheader.sf-wrap { + .sf-headercelldiv { + @include wrap-styles; + margin-bottom: $grid-text-wrap-margin-bottom; + margin-top: $grid-text-wrap-margin-top; + } + + .sf-sortfilterdiv { + margin: $grid-header-wrap-sort-filter-margin; + } + + .sf-rightalign, + .sf-leftalign { + .sf-sortfilterdiv { + margin: $grid-header-wrap-right-align-sort-margin; + } + } + } + + .sf-gridcontent.sf-wrap .sf-rowcell { + @include wrap-styles; + } + } +} + +#{&}.sf-grid-min-height { + .sf-rowcell { + line-height: 0; + @include grid-top-bottom-padding(0, 0); + } + + .sf-gridheader { + .sf-headercell, + .sf-headercell .sf-headercelldiv:not(.sf-sort-icon) { + height: auto; + } + } + + .sf-summarycell { + @include grid-line-height-padding-styles(normal, 0 8px); + } +} + +@-moz-document url-prefix() { + #{&}.sf-grid-min-height { + .sf-rowcell { + line-height: normal; + } + } +} \ No newline at end of file diff --git a/components/grids/src/grid/grid/_material3-definition.scss b/components/grids/src/grid/grid/_material3-definition.scss new file mode 100644 index 0000000..1b89fad --- /dev/null +++ b/components/grids/src/grid/grid/_material3-definition.scss @@ -0,0 +1,119 @@ +$grid-content-padding: 13px !default; +$grid-icon-color: $icon-color !default; +$grid-border-style: solid !default; +$grid-content-font-family: $font-family !default; +$grid-header-font-size: 14px !default; +$grid-header-text-color: $content-text-color-alt1 !default; +$grid-text-color: $content-text-color !default; +$grid-sort-number-bg: $content-bg-color-alt2 !default; +$grid-sort-number-text-color: $content-text-color-alt1 !default; +$grid-table-bg-color: rgba($content-bg-color) !default; +$grid-row-selection-color: $table-bg-color-selected !default; +$grid-row-selection-hover-bg-color: $table-bg-color-selected-hover !default; +$grid-text-wrap-margin-bottom: 2px !default; +$grid-text-wrap-margin-top: 0 !default; +$grid-header-padding-bottom: 12px !default; +$grid-header-padding-top: 12px !default; +$grid-headercell-line-height: 20px !default; +$grid-filterbar-cell-text-indent: 1px !default; +$grid-rtl-header-first-cell-border-left: 0 !default; +$grid-toolbar-border: 1px solid !default; +$grid-filter-padding: 0 !default; +$grid-sort-number-border-radius: 65% !default; +$grid-wrap-sort-number-margin: 0 2px 0 0 !default; +$grid-wrap-sort-icon-margin: -9px 10px !default; +$grid-rtl-headercell-border-width: 0 !default; +$grid-rtl-wrap-sort-number-margin: 0 5px 0 0 !default; +$grid-device-headercell-padding: 6px 12px 6px !default; +$grid-device-header-first-cell-padding: 6px 12px 6px 16px !default; +$grid-device-header-last-cell-padding: 6px 16px 6px 12px !default; +$grid-device-rowcell-padding: 8px 12px !default; +$grid-device-row-first-cell-padding: 8px 12px 8px 16px !default; +$grid-device-row-last-cell-padding: 8px 16px 8px 12px !default; +$grid-device-filterbar-cell-padding: 8px 12px !default; +$grid-device-filterbar-first-cell-padding: 8px 12px 8px 16px !default; +$grid-device-filterbar-last-cell-padding: 8px 16px 8px 12px !default; +$grid-filter-padding: 2px !default; +$grid-rtl-filter-padding: 2px !default; +$grid-rtl-filter-float: left !default; +$grid-rtl-headercell-filter-icon-padding: 0 1.6em 0 10px !default; +$grid-row-bg-color: rgba($content-bg-color) !default; +$grid-header-bg-color: $content-bg-color-alt1 !default; +$grid-headercell-both-border-width: 0 0 0 1px !default; +$grid-headercell-margin: -7px -7px -7px -5px !default; +$grid-icon-font-size: 16px !default; +$grid-border-width: 1px !default; +$grid-header-padding: 12px !default; +$grid-header-wrap-sort-filter-margin: -29px 10px !default; +$grid-headercell-right-align-padding: 0 8px !default; +$grid-headercell-height: 48px !default; +$grid-filterbar-cell-height: 58px !default; +$grid-header-font-weight: 500 !default; +$grid-filter-cell-both-border-width: 1px 0 1px 1px !default; +$grid-border-color: rgba($border-light) !default; +$grid-rowcell-line-height: 21px !default; +$grid-hover-content-text-color: rgba($content-text-color-hover) !default; +$grid-content-bg-color: rgba($content-bg-color) !default; +$grid-row-selection-bg-color: $table-bg-color-selected !default; +$grid-hover-bg-color: $content-bg-color-hover !default; +$grid-content-text-size: 14px !default; +$grid-header-height: 20px !default; +$grid-rowcell-border-width: 1px 0 0 !default; +$grid-content-right-padding: 12px !default; +$grid-toolbar-search-margin-bottom: 0 !default; +$grid-toolbar-search-width: 160px !default; +$grid-toolbar-search-icon-min-width: 32px !default; +$grid-toolbar-search-clear-icon-min-width: 32px !default; +$grid-toolbar-search-clear-icon-font-size: 16px !default; +$grid-toolbar-search-clear-icon-padding: 0 !default; +$grid-toolbar-search-clear-icon-margin-right: 0 !default; +$grid-rtl-toolbar-search-clear-icon-padding: 0 !default; +$grid-rtl-toolbar-search-clear-icon-margin: 0 !default; +$grid-toolbar-search-wrapper-padding-bottom: 3px !default; +$grid-toolbar-search-wrapper-padding-top: 3px !default; +$grid-summary-cell-font-size: 14px !default; +$grid-filterbar-cell-border-width: 1px 0 0 !default; +$grid-filterbar-cell-padding: 6px 12px !default; +$grid-filterbar-input-border-width: 0 !default; +$grid-border-radius: 1px !default; +$grid-summary-row-bg: $content-bg-color-alt1 !default; +$grid-sort-width: 20px !default; +$grid-sort-height: 20px !default; +$grid-sort-number-margin: 0 4px 0 12px !default; +$grid-sort-number-right-align-margin: 0 2px 0 -2px !default; +$grid-sort-number-line-height: 20px !default; +$grid-sort-right-align-margin: -19px 8px -12px -6px !default; +$grid-sort-mg: -19px -4px -12px 8px !default; +$grid-sort-padding: 2px !default; +$grid-rowcell-both-border-width: 1px 0 0 1px !default; +$grid-summary-cell-line-height: 21px !default; +$grid-validation-error-text-color: rgba($error) !default; +$grid-validation-error-bg-color: rgba($error-container) !default; +$grid-edit-input-padding-bottom: 1px !default; +$grid-edit-input-padding-top: 2px !default; +$grid-edit-input-margin: 3px !default; +$grid-edit-top-cell-padding: 0 !default; +$grid-edit-cell-padding: 0 !default; +$grid-rtl-sort-number-margin: 0 8px 0 4px !default; +$grid-rtl-sort-number-right-align-margin: 0 8px 0 2px !default; +$grid-rtl-sort-cell-margin: -19px 8px -12px -2px !default; +$grid-rtl-sort-cell-right-align-margin: -19px 8px -12px -2px !default; +$grid-rtl-headercell-border-width: 0 !default; +$grid-rtl-headercell-padding: 0 .4em 0 2.2em !default; +$grid-headercell-sort-padding-right: 8px !default; +$grid-headercell-padding: 0 1.4em 0 .4em !default; +$grid-filterbar-border-radius: 4px !default; +$grid-header-border-width: 0 !default; +$grid-edit-input-margin-top: 0 !default; +$grid-filterbar-cell-text-input: 32px !default; +$grid-cell-focus-shadow: 0 0 0 1px rgba($primary) inset !default; +$grid-header-wrap-right-align-sort-margin: -29px -5px !default; +$grid-rowcell-wrap-height: 21px !default; +$grid-header-text-transform: none !default; +$grid-sort-number-size: 20px !default; +$grid-sort-number-font-size: 14px !default; +$grid-toolbar-searchwrapper-text-color: rgba($placeholder-text-color) !default; +$grid-toolbar-search-bar-text-opacity: 1 !default; +$grid-row-boolean-cell-color: rgba($primary-text-color) !default; +$grid-sort-margin: -19px 2px !default; +$grid-sort-right-margin: 0 4px 0 4px !default; diff --git a/components/grids/src/grid/grid/_theme.scss b/components/grids/src/grid/grid/_theme.scss new file mode 100644 index 0000000..8249b11 --- /dev/null +++ b/components/grids/src/grid/grid/_theme.scss @@ -0,0 +1,92 @@ +@mixin bgcolor-color-styles($bgcolor, $color) { + background: $bgcolor; + color: $color; +} + +@include export-module('grid-theme') { + + #{&}.sf-grid{ + border-color: $grid-border-color; + + .sf-content:not(.sf-tooltip-content) { + background-color: $grid-row-bg-color; + } + + .sf-table { + background-color: $grid-header-bg-color; + } + + .sf-focused { + box-shadow: $grid-cell-focus-shadow; + } + + .sf-gridheader { + @include bgcolor-color-styles($grid-header-bg-color, $grid-header-text-color); + border-bottom-color: $grid-border-color; + border-top-color: $grid-border-color; + } + + .sf-gridcontent tr:first-child td { + border-top-color: transparent; + } + + .sf-verticallines th, + .sf-filterbarcell, + .sf-headercell, + .sf-summarycell, + .sf-headercontent, + .sf-rowcell, + &.sf-rtl .sf-default.sf-verticallines th:last-child, + .sf-emptyrow.sf-show-added-row .sf-lastrowcell { + border-color: $grid-border-color; + } + + .sf-headercontent.sf-headerborder { + border-right-color: transparent; + } + + &.sf-rtl .sf-headercontent.sf-headerborder { + border-left-color: transparent; + } + + .sf-gridfooter { + background: $grid-summary-row-bg; + } + + .sf-rowcell, + .sf-summarycell, + .sf-emptyrow { + color: $grid-text-color; + } + + &.sf-gridhover .sf-row:not(.sf-editedrow):hover { + .sf-rowcell:not(.sf-active) { + @include bgcolor-color-styles($grid-hover-bg-color, $grid-hover-content-text-color); + } + + .sf-rowcell { + @include bgcolor-color-styles($grid-row-selection-hover-bg-color, $grid-text-color); + } + } + + .sf-sortnumber { + @include bgcolor-color-styles($grid-sort-number-bg, $grid-sort-number-text-color); + } + + td.sf-active { + @include bgcolor-color-styles($grid-row-selection-bg-color, $grid-text-color); + } + + .sf-gridcontent table tbody .sf-normaledit .sf-rowcell { + border-top-color: $grid-border-color; + } + } + + .sf-tooltip-wrap.sf-griderror { + background-color: $grid-validation-error-bg-color; + border-color: $grid-validation-error-bg-color; + .sf-tooltip-arrow-outer-top, .sf-tooltip-arrow-outer-left, .sf-tooltip-arrow-outer-bottom, .sf-tooltip-arrow-outer-right { + border-bottom-color: $grid-validation-error-bg-color; + } + } +} diff --git a/components/grids/src/grid/hooks/index.ts b/components/grids/src/grid/hooks/index.ts new file mode 100644 index 0000000..5cff625 --- /dev/null +++ b/components/grids/src/grid/hooks/index.ts @@ -0,0 +1,12 @@ +export * from './useGrid'; +export * from './useColumn'; +export * from './useRender'; +export * from './useScroll'; +export * from './useFocusStrategy'; +export * from './useSelection'; +export * from './useSort'; +export * from './useFilter'; +export * from './useSearch'; +export * from './useEdit'; +export * from './useToolbar'; +export * from './useEditDialog'; diff --git a/components/grids/src/grid/hooks/useColumn.ts b/components/grids/src/grid/hooks/useColumn.ts new file mode 100644 index 0000000..9984d94 --- /dev/null +++ b/components/grids/src/grid/hooks/useColumn.ts @@ -0,0 +1,316 @@ +import { isValidElement, ReactElement, useMemo } from 'react'; +import { DateFormatOptions, IL10n, formatUnit, isNullOrUndefined, NumberFormatOptions } from '@syncfusion/react-base'; +import { IValueFormatter, CellType, IRow, EditType, ValueType } from '../types'; +import { ColumnProps, IColumnBase } from '../types/column.interfaces'; +import { AggregateColumnProps } from '../types/aggregate.interfaces'; +import { useGridComputedProvider } from '../contexts'; +import { setStringFormatter, getObject, getUid, headerValueAccessor as defaultHeaderValueAccessor, + valueAccessor as defaultValueAccessor, isDateOrNumber } from '../utils'; +/** + * CSS class names used in the Column component + */ +const CSS_CLASS_NAMES: Record = { + LEFT_ALIGN: 'sf-leftalign', + RIGHT_ALIGN: 'sf-rightalign', + CENTER_ALIGN: 'sf-centeralign', + HIDDEN: 'sf-hide' +}; +/** + * Applies default column properties to the provided column configuration + * + * @param {IColumnBase} props - The column properties to enhance with defaults + * @returns {IColumnBase} The column properties with defaults applied + * @private + */ +export const defaultColumnProps: (props: Partial) => Partial = + (props: Partial): Partial => { + // computed values should handle in component inside alone since react not allowed us to compute here using memo. + return { + visible: true, + textAlign: 'Left', + disableHtmlEncode: true, + allowEdit: true, + edit: {type: EditType.TextBox}, + filter: { type: 'FilterBar', filterBarType: 'stringFilter' }, + ...props, + width: props.width ? formatUnit(props.width) : '', + onValueAccessor: props.onValueAccessor ?? defaultValueAccessor, + onHeaderValueAccessor: props.onHeaderValueAccessor ?? defaultHeaderValueAccessor, + type: props.type === 'none' ? null : (props.type ? (typeof (props.type) === 'string' ? props.type.toLowerCase() : undefined) : props.type), + uid: isNullOrUndefined(props.uid) ? getUid('grid-column') : props.uid, + getFormatter: props.formatFn, + getParser: props.parseFn, + allowSort: props.allowSort ?? true, + allowFilter: props.allowFilter ?? true, + allowSearch: props.allowSearch ?? true, + templateSettings: { + ariaLabel: '', + ...props.templateSettings + } + }; + }; +/** + * `useColumn` is a custom hook that provides column configuration and formatting logic for grid columns. + * It handles value formatting, alignment classes, visibility, and template rendering. + * + * @private + * @param {IColumnBase} props - The column configuration properties + * @returns {Object} Object containing publicAPI (ColumnProps) and privateAPI with cell formatting properties + */ +export const useColumn: (props: Partial) => { + publicAPI: Partial; + privateAPI: { + cellType: CellType; + row: IRow; + alignClass: string; + alignHeaderClass: string; + visibleClass: string; + formattedValue: string | Object | ReactElement; + }; +} = (props: Partial): { + publicAPI: Partial; + privateAPI: { + cellType: CellType; + row: IRow; + alignClass: string; + alignHeaderClass: string; + visibleClass: string; + formattedValue: string | Object | ReactElement; + }; +} => { + const { + cell, + row + } = props; + const { + column, + cellType, + aggregateColumn + } = cell; + const { + customAttributes, + field, + width, + headerText, + headerTemplate, + template, + onValueAccessor, + onHeaderValueAccessor, + format, + type, + textAlign, + visible, + disableHtmlEncode, + headerTextAlign, + ...rest + } = column; + const { serviceLocator } = useGridComputedProvider(); + const formatter: IValueFormatter = serviceLocator?.getService('valueFormatter'); + const localization: IL10n = serviceLocator?.getService('localization'); + /** + * Formats a value according to the column's type and format specification + * + * @type {(value: string | Object | null) => string} + */ + const formatValue: (value: string | object | null) => string = useMemo(() => { + return (value: string | Object | null): string => { + let updatedType: string = type; + if (!isNullOrUndefined(format) && formatter) { + // Handle number validation + if (type === 'number' && typeof value === 'string' && isNaN(parseInt(value, 10))) { + return ''; + } + // Auto-detect type if not specified + if (!isNullOrUndefined(value) && !type) { + updatedType = value instanceof Date && !isNullOrUndefined(value.getDay) ? + ((value.getHours() || value.getMinutes() || value.getSeconds() || value.getMilliseconds()) ? 'datetime' : 'date') : + typeof value; + } + // Get appropriate formatter function + const formatterFn: Function = isDateOrNumber(value) ? (typeof format === 'string' ? + setStringFormatter(formatter, updatedType, format) : + formatter.getFormatFunction?.(format as NumberFormatOptions | DateFormatOptions)) : undefined; + + if (type === 'number' && typeof value === 'string' && isNullOrUndefined(value.split('.')[1])) { + value += '.0'; + } + if (!isNullOrUndefined(formatterFn)) { + const viewableContent: string = formatter.toView(value as number | Date, formatterFn) as string; + value = viewableContent !== 'NaN' ? viewableContent : value; + } + } + if (type === 'boolean' && !column.displayAsCheckBox) { + // Handle boolean values properly - check actual boolean value, not just string representation + // Convert value to string first, then check string representation + const stringValue: string = value?.toString(); + const localeStr: string = (stringValue !== 'true' && stringValue !== 'false') ? null : + stringValue === 'true' ? 'booleanTrueLabel' : 'booleanFalseLabel'; + + // If localeStr exists, get localized version, otherwise fall back to original stringValue + return localeStr ? (localization ? localization.getConstant(localeStr) : stringValue) : stringValue; + } + return String(value); + }; + }, [type, format, formatter]); + /** + * Computes the CSS class for header alignment + * + * @type {string} + */ + const alignHeaderClass: string = useMemo(() => { + const alignment: string = (headerTextAlign ?? textAlign ?? 'Left').toLowerCase(); + return `sf-${alignment}align`; + }, [headerTextAlign, textAlign]); + /** + * Computes the CSS class for cell alignment + * + * @type {string} + */ + const alignClass: string = useMemo(() => { + const alignment: string = (textAlign ?? 'Left').toLowerCase(); + return `sf-${alignment}align`; + }, [textAlign]); + /** + * Computes visibility class and updates style attributes + * + * @type {string} + */ + const visibleClass: string = useMemo(() => { + if (CellType.Data === cellType && customAttributes) { + customAttributes.style = { + ...customAttributes.style, + display: visible || isNullOrUndefined(visible) ? '' : 'none' + }; + } + return visible || isNullOrUndefined(visible) ? '' : ` ${CSS_CLASS_NAMES.HIDDEN}`; + }, [visible, cellType, customAttributes]); + /** + * Retrieves the raw value from the data row based on field + * + * @type {ValueType} + */ + const value: ValueType = useMemo(() => { + return (cellType === CellType.Data && field && row && row.isDataRow) ? + getObject(field, row.data) : undefined; + }, [cellType, field, row]); + /** + * Retrieves the aggregate value from the data based on column information + * + * @param {Object} data - The data object containing the column values + * @param {AggregateColumnProps} column - The column model with aggregation details + * @returns {Object} - The aggregated value for the specified column, or an empty string if not found + */ + const getAggregateValue: (data: Object, column: AggregateColumnProps) => Object = + (data: Object, column: AggregateColumnProps): Object => { + let key: string = !isNullOrUndefined(column.type) ? + column.field + ' - ' + (typeof column.type === 'string' ? column.type.toLowerCase() : '') : column.columnName; + if (column.format && !isNullOrUndefined(column.type) && typeof column.type === 'string') { + key = column.type; + } + return data[column.columnName] ? data[column.columnName][`${key}`] : ''; + }; + /** + * Computes the formatted value for display, handling templates and type conversions + * + * @type {string | ReactElement} + */ + const formattedValue: string | Object | ReactElement = useMemo(() => { + let formattedVal: string | Object | ReactElement = value; + // Handle header cell formatting + if (cellType === CellType.Header) { + if (isNullOrUndefined(headerTemplate)) { + formattedVal = onHeaderValueAccessor({headerText: 'headerText', column}); + } else if (typeof headerTemplate === 'string' || isValidElement(headerTemplate)) { + return headerTemplate; + } else { + return headerTemplate({ column: column, columnIndex: cell.index }); + } + } else if (cellType === CellType.Filter) { + return formattedVal; + } else if (cellType === CellType.Summary) { + const footerTemplate: string | ReactElement | ((props?: Object) => ReactElement | string) = aggregateColumn.footerTemplate; + if (isNullOrUndefined(footerTemplate)) { + return getAggregateValue(row.data, aggregateColumn); + } else if (typeof footerTemplate === 'string' || isValidElement(footerTemplate)) { + return footerTemplate; + } else { + return footerTemplate(row.data[aggregateColumn.columnName]); + } + } + // Handle data cell formatting + else { + if (isNullOrUndefined(template)) { + formattedVal = onValueAccessor({field: (field as string), rowData: row.data, column: column}); + } else if (typeof template === 'string' || isValidElement(template)) { + return template; + } else { + return template({ column: column, rowData: row.data, rowIndex: row.index }); + } + } + // Apply type-specific formatting for values + if (!isNullOrUndefined(formattedVal)) { + if ((type === 'date' || type === 'datetime') && !isNullOrUndefined(formattedVal)) { + const dateValue: Date = new Date(formattedVal as string); + formattedVal = !isNaN(dateValue?.getTime?.()) ? dateValue : formattedVal; + } + if (type === 'dateonly' && typeof formattedVal === 'string') { + const arr: string[] = formattedVal.split(/[^0-9.]/); + const dateValue: Date = new Date(parseInt(arr[0], 10), parseInt(arr[1], 10) - 1, parseInt(arr[2], 10)); + formattedVal = !isNaN(dateValue?.getTime?.()) ? dateValue : formattedVal; + } + return formatValue(formattedVal); + } else { + return formattedVal as string; + } + }, [ + value, + field, + row, + cellType, + template, + headerTemplate, + headerText, + type, + onValueAccessor, + onHeaderValueAccessor, + formatValue + ]); + /** + * Private API for internal component use + * + * @type {{ cellType: CellType, row: Object, alignClass: string, alignHeaderClass: string, visibleClass: string, formattedValue: string | ReactElement }} + */ + const privateAPI: { + cellType: CellType; + row: Object; + alignClass: string; + alignHeaderClass: string; + visibleClass: string; + formattedValue: string | Object | ReactElement; + } = useMemo(() => ({ + cellType, + row, + alignClass, + alignHeaderClass, + visibleClass, + formattedValue + }), [cellType, row, alignClass, alignHeaderClass, visibleClass, formattedValue]); + /** + * Public API exposed to parent components + * + * @type {Partial} + */ + const publicAPI: Partial = useMemo(() => ({ + field, + headerText, + textAlign, + headerTextAlign, + format, + width: formatUnit(width as string | number || ''), + customAttributes, + visible, + disableHtmlEncode, + ...rest + }), [field, headerText, textAlign, headerTextAlign, format, width, customAttributes, visible, rest]); + return { publicAPI, privateAPI }; +}; diff --git a/components/grids/src/grid/hooks/useEdit.ts b/components/grids/src/grid/hooks/useEdit.ts new file mode 100644 index 0000000..2f2b9d7 --- /dev/null +++ b/components/grids/src/grid/hooks/useEdit.ts @@ -0,0 +1,1505 @@ +import { useState, useCallback, useRef, useEffect, RefObject, SetStateAction, Dispatch } from 'react'; +import { EditEndAction, IRow, ValueType } from '../types'; +import { GridRef, RowInfo } from '../types/grid.interfaces'; +import { EditSettings, EditState, UseEditResult, UseConfirmDialogResult, SaveEvent, DeleteEvent, CancelFormEvent, FormRenderEvent, RowAddEvent, RowEditEvent } from '../types/edit.interfaces'; +import { ColumnProps } from '../types/column.interfaces'; +import { UseDataResult } from '../types/interfaces'; +import { useConfirmDialog } from './useEditDialog'; +import { ServiceLocator } from '../types/interfaces'; +import { IL10n, isNullOrUndefined, addClass } from '@syncfusion/react-base'; +import { FocusedCellInfo, FocusStrategyResult } from '../types/focus.interfaces'; +import { DataManager, DataResult, DataUtil, ReturnType } from '@syncfusion/react-data'; +import { FormState, IFormValidator } from '@syncfusion/react-inputs'; + +/** + * Edit hook for managing inline editing functionality in React Grid + * + * @private + * @param {RefObject} _gridRef - Reference to the grid instance + * @param {ServiceLocator} serviceLocator - Service locator for accessing grid services + * @param {ColumnProps[]} columns - Column definitions for validation + * @param {Object[]} currentViewData - Current data source array + * @param {UseDataResult} dataOperations - Data operations object containing DataManager and related methods + * @param {FocusStrategyResult} focusModule - Reference to the focus module + * @param {EditSettings} editSettings - Edit configuration settings + * @param {Dispatch>} setGridAction - Function to set grid actions + * @param {Dispatch>} setCurrentPage - Function to set grid currentpage + * @param {Dispatch>} setResponseData - Function to set aggregate updated data + * @returns {Object} Edit state and methods + */ +export const useEdit: ( + _gridRef: RefObject, + serviceLocator: ServiceLocator, + columns: ColumnProps[], + currentViewData: Object[], + dataOperations: UseDataResult, + focusModule: FocusStrategyResult, + editSettings: EditSettings, + setGridAction: Dispatch>, + setCurrentPage: Dispatch>, + setResponseData: Dispatch> +) => UseEditResult = ( + _gridRef: RefObject, + serviceLocator: ServiceLocator, + columns: ColumnProps[], + currentViewData: Object[], + dataOperations: UseDataResult, + focusModule: FocusStrategyResult, + editSettings: EditSettings, + setGridAction: Dispatch>, + setCurrentPage: Dispatch>, + setResponseData: Dispatch> +) => { + // Use currentViewData (processed array) for display + const viewData: Object[] = currentViewData; + + const dialogHook: UseConfirmDialogResult = useConfirmDialog(serviceLocator); + const localization: IL10n = serviceLocator?.getService('localization'); + const { confirmOnDelete } = dialogHook; + const prevFocusedCell: RefObject = useRef({} as FocusedCellInfo); + const nextPrevEditRowInfo: RefObject = useRef({} as KeyboardEvent); + const focusLastField: RefObject = useRef(false); + const escEnterIndex: RefObject = useRef(0); + const notKeyBoardAllowedClickRowInfo: RefObject = useRef({}); + + // Edit state management + const [editState, setEditState] = useState({ + isEdit: false, + editRowIndex: -1, + editCellField: null, + editData: null, + originalData: null, + validationErrors: {}, + showAddNewRowData: null, + isShowAddNewRowActive: false, + isShowAddNewRowDisabled: false + }); + + /** + * Default edit settings with fallbacks + */ + const defaultEditSettings: EditSettings = { + allowAdd: false, + allowEdit: false, + allowDelete: false, + mode: 'Normal', + editOnDoubleClick: true, + confirmOnEdit: true, + confirmOnDelete: false, + showAddNewRow: false, + newRowPosition: 'Top', + ...editSettings + }; + + const validateEditForm: () => boolean = useCallback(() => { + return (editState.originalData ? _gridRef.current?.editInlineRowFormRef?.current?.validateForm?.() : + _gridRef.current?.addInlineRowFormRef?.current?.validateForm?.()) ?? true; + }, [ + editState.originalData, + _gridRef.current?.editInlineRowFormRef?.current, + _gridRef.current?.addInlineRowFormRef?.current + ]); + + const validateField: (field: string) => boolean = useCallback((field: string): boolean => { + const column: ColumnProps | undefined = columns.find((col: ColumnProps) => col.field === field); + if (!column || !column.validationRules) { + return true; + } + + if (isNullOrUndefined(editState.originalData)) { + return _gridRef.current?.addInlineRowFormRef?.current?.formRef?.current?.validateField?.(field); + } else { + return _gridRef.current?.editInlineRowFormRef?.current?.formRef?.current?.validateField?.(field); + } + }, [columns]); + + /** + * Gets the primary key field name from columns + */ + const getPrimaryKeyField: () => string = useCallback((): string => { + const primaryKeys: string[] = _gridRef.current?.getPrimaryKeyFieldNames?.(); + return primaryKeys[0]; + }, [_gridRef.current?.getPrimaryKeyFieldNames]); + + /** + * Starts editing for the specified row or selected row + */ + const editRow: (rowElement?: HTMLTableRowElement) => Promise = useCallback(async (rowElement?: HTMLTableRowElement) => { + const eventTarget: HTMLElement = event?.target as HTMLElement; + if (!defaultEditSettings.allowEdit) { + return; + } + if (editState.isEdit && !defaultEditSettings.showAddNewRow) { + const isValid: boolean = validateEditForm(); + if (!isValid) { + return; + } + } + let rowIndex: number = -1; + let hasValidSelection: boolean = false; + + if (rowElement) { + const rowIndexAttr: string | null = rowElement.getAttribute('aria-rowindex'); + rowIndex = rowIndexAttr ? (parseInt(rowIndexAttr, 10) - 1) : -1; + hasValidSelection = rowIndex >= 0; + } else { + const gridRef: GridRef | null = _gridRef?.current; + let selectedIndexes: number[] = []; + + selectedIndexes = gridRef?.selectionModule?.getSelectedRowIndexes(); + + if (selectedIndexes.length === 0 && typeof gridRef.getSelectedRowIndexes === 'function') { + selectedIndexes = gridRef.getSelectedRowIndexes(); + } + + if (selectedIndexes.length > 0) { + rowIndex = selectedIndexes[0]; + hasValidSelection = true; + } + } + + if (!hasValidSelection || rowIndex < 0) { + const message: string = localization.getConstant('noRecordsEditMessage'); + await dialogHook.confirmOnEdit({ + title: '', + message: message, + confirmText: localization.getConstant('okButtonLabel'), + cancelText: '', + type: 'info' + }); + eventTarget?.focus?.(); + return; + } + + // Validate row index bounds + if (rowIndex < 0 || rowIndex >= viewData.length) { + return; + } + + const rowData: Object = viewData[rowIndex as number]; + if (!rowData) { + return; + } + + const actionBeginArgs: Record = { + cancel: false, + requestType: 'beginEdit', + type: 'actionBegin', + rowIndex: rowIndex, + action: 'beginEdit', + rowData: { ...rowData } + }; + + // Get grid reference for actionBegin event + const gridRef: GridRef | null = _gridRef?.current; + const startArgs: RowEditEvent = { + cancel: false, + rowData: actionBeginArgs.rowData, + rowIndex: actionBeginArgs.rowIndex as number + }; + gridRef?.onRowEditStart?.(startArgs); + + // If the operation was cancelled, return early + if (startArgs.cancel) { + return; + } + + editDataRef.current = { ...rowData }; + const updateState: Partial = { + isEdit: true, + editRowIndex: rowIndex, + editData: { ...rowData }, + originalData: { ...rowData }, + validationErrors: {} + }; + + if (defaultEditSettings.showAddNewRow) { + updateState.isShowAddNewRowActive = true; + updateState.isShowAddNewRowDisabled = true; + updateState.showAddNewRowData = editState.showAddNewRowData; + } + + // Set edit state + setEditState((prev: EditState) => ({ + ...prev, + ...updateState + })); + const editGridElement: HTMLDivElement | null | undefined = _gridRef?.current?.element; + const editStateEvent: CustomEvent = new CustomEvent('editStateChanged', { + detail: { isEdit: true, editRowIndex: rowIndex } + }); + editGridElement?.dispatchEvent(editStateEvent); + requestAnimationFrame(() => { + setTimeout(() => { + const actionCompleteArgs: Record = { + requestType: 'beginEdit', + type: 'actionComplete', + rowData: { ...rowData }, + rowIndex: rowIndex, + action: 'beginEdit', + formRef: gridRef?.editInlineRowFormRef.current?.formRef?.current + }; + const eventArgs: FormRenderEvent = { + formRef: gridRef?.editInlineRowFormRef.current?.formRef as RefObject, + rowData: actionCompleteArgs.rowData, + rowIndex: actionCompleteArgs.rowIndex as number + }; + gridRef?.onFormRender?.(eventArgs); + prevFocusedCell.current = { ...focusModule.getFocusedCell() }; + }, 0); + }); + }, [ + defaultEditSettings.allowEdit, + defaultEditSettings.showAddNewRow, + viewData, + _gridRef, + editState.isEdit, + editState.editRowIndex, + editState.editData, + editState.originalData, + editState.showAddNewRowData, + focusModule + ]); + + /** + * Get current edit data from ref for save operations + * This ensures we always get the latest typed values, not stale state values + */ + const getCurrentEditData: () => Object = useCallback(() => { + return (editState.originalData ? _gridRef.current?.editInlineRowFormRef?.current?.formState?.values : + _gridRef.current?.addInlineRowFormRef?.current?.formState?.values) || editDataRef.current; + }, [ + editState.editData, + _gridRef.current?.editInlineRowFormRef?.current, + _gridRef.current?.addInlineRowFormRef?.current + ]); + + const getCurrentFormRef: () => RefObject = useCallback(() => { + return (editState.originalData ? _gridRef.current?.editInlineRowFormRef?.current?.formRef : + _gridRef.current?.addInlineRowFormRef?.current?.formRef); + }, [ + editState.editData, + _gridRef.current?.editInlineRowFormRef?.current, + _gridRef.current?.addInlineRowFormRef?.current + ]); + + const getCurrentFormState: () => FormState = useCallback(() => { + return (editState.originalData ? _gridRef.current?.editInlineRowFormRef?.current?.formState : + _gridRef.current?.addInlineRowFormRef?.current?.formState); + }, [ + editState.editData, + _gridRef.current?.editInlineRowFormRef?.current, + _gridRef.current?.addInlineRowFormRef?.current + ]); + + /** + * Ends editing and saves changes using DataManager operations + */ + const saveChanges: (isValidationRequired?: boolean, insertIndex?: number, endAction?: EditEndAction) => Promise = + useCallback(async (isValidationRequired: boolean = true, insertIndex: number, endAction?: EditEndAction): Promise => { + if (!editState.isEdit && isNullOrUndefined(insertIndex)) { + return false; + } + + const customBinding: boolean = dataOperations.dataManager && 'result' in dataOperations.dataManager; + const currentEditData: Object = getCurrentEditData(); + + if (!currentEditData) { + return false; + } + + // Store the current edit row index for focus management + const savedRowIndex: number = editState.editRowIndex; + + const isFormValid: boolean = isValidationRequired ? validateEditForm() : true; + if (!isFormValid) { + setEditState((prev: EditState) => ({ + ...prev, + validationErrors: !prev.originalData ? _gridRef.current?.addInlineRowFormRef.current?.formState.errors : + _gridRef.current?.editInlineRowFormRef.current?.formState.errors + })); + // FormValidator found validation errors, don't proceed with save + return false; + } + + const isAddOperation: boolean = editState.editRowIndex === -1 || !editState.originalData || + Object.keys(editState.originalData).length === 0; + const customBindingEdit: boolean = customBinding && !isAddOperation; + + const actionBeginArgs: Record = { + cancel: false, + requestType: 'save', + type: 'actionBegin', + rowData: currentEditData, + rowIndex: editState.editRowIndex, + action: isAddOperation ? 'add' : 'edit', + previousData: editState.originalData + }; + + // Get grid reference for actionBegin event + const gridRef: GridRef | null = _gridRef?.current; + const startArgs: SaveEvent = { + action: actionBeginArgs.action as string, + editedRowData: actionBeginArgs.rowData, + rowData: actionBeginArgs.previousData, + rowIndex: actionBeginArgs.rowIndex as number, + cancel: false + }; + gridRef?.onDataChangeStart?.(startArgs); + + // If the operation was cancelled, return early + if (startArgs.cancel) { + return false; + } + setGridAction({}); + + try { + if (isAddOperation) { + let insertIndex: number; + + if (defaultEditSettings.showAddNewRow) { + // For showAddNewRow, respect the newRowPosition setting + if (defaultEditSettings.newRowPosition === 'Bottom') { + insertIndex = viewData.length; // Add at the end + } else { + insertIndex = 0; // Add at the beginning (Top - default) + } + } else if (editState.editRowIndex !== -1) { + // For programmatic addRecord(data, index), use the specified index + insertIndex = editState.editRowIndex; + } else { + // Fallback to newRowPosition setting for regular add operations + if (defaultEditSettings.newRowPosition === 'Top') { + insertIndex = 0; + } else { + insertIndex = viewData.length; + } + } + + await dataOperations.getData(customBinding ? { ...actionBeginArgs, index: insertIndex } : { + requestType: 'save', + data: currentEditData, + index: insertIndex + }); + + if (!customBinding) { + _gridRef.current?.refresh(); // initiate getData with requestType as 'refresh' + } + } else { + // For edit operations, use update operation + await dataOperations.getData(customBinding ? actionBeginArgs : { + requestType: 'update', + data: currentEditData + }); + if (_gridRef.current?.aggregates?.length) { + let isFiltered: boolean = false; + if (!(dataOperations.isRemote() || (!isNullOrUndefined(dataOperations.dataManager) + && (dataOperations.dataManager as DataResult).result)) && ((_gridRef.current?.filterSettings?.enabled + && _gridRef.current?.filterSettings?.columns?.length) + || _gridRef.current?.searchSettings?.value?.length)) { + isFiltered = true; + } + let currentViewData: Object[]; + if (!isNullOrUndefined(dataOperations.dataManager) && (dataOperations.dataManager as DataResult).result) { + currentViewData = _gridRef.current?.getCurrentViewRecords(); + } else { + currentViewData = ((dataOperations.dataManager as DataManager).dataSource.json.length ? + (isFiltered ? (await (_gridRef.current?.getData(true, true) as Promise)).result : + (dataOperations.dataManager as DataManager).dataSource.json) + : _gridRef.current?.getCurrentViewRecords()); + } + setResponseData((prevData: DataResult) => ({ + ...prevData, + aggregates: customBinding ? prevData.aggregates : undefined, + result: [...currentViewData.map((item: Object) => + item[getPrimaryKeyField()] === currentEditData[getPrimaryKeyField()] ? + { ...item, ...currentEditData } : item + )] + })); + } + } + } catch (error) { + // Trigger actionFailure event on error + // This provides consistent error handling similar to other grid operations + gridRef?.onError({ + error: error as Error + }); + return false; + } + + const actionCompleteArgs: Record = { + requestType: 'save', + type: 'actionComplete', + rowData: currentEditData, + rowIndex: isNullOrUndefined(insertIndex) ? editState.editRowIndex : insertIndex, + action: isAddOperation ? 'add' : 'edit', + previousData: editState.originalData + }; + + const nextPrevEditRow: () => boolean = (): boolean => { + const isNextPrevEditRow: boolean = !isAddOperation && Object.keys(nextPrevEditRowInfo.current).length + && nextPrevEditRowInfo.current.key === 'Tab' + && ((!nextPrevEditRowInfo.current.shiftKey && editState.editRowIndex < currentViewData.length - 1) + || (nextPrevEditRowInfo.current.shiftKey && editState.editRowIndex > 0)) + ? true : false; + if (isNextPrevEditRow) { + const shiftKey: boolean = nextPrevEditRowInfo.current.shiftKey; + focusLastField.current = shiftKey; + setTimeout(() => { + gridRef.selectionModule?.selectRow(shiftKey ? editState.editRowIndex - 1 : editState.editRowIndex + 1); + setTimeout(() => { + editRow(); + }, 0); + }, 0); + } + nextPrevEditRowInfo.current = {} as KeyboardEvent; + return isNextPrevEditRow; + }; + + const addDeleteActionComplete: () => void = () => { + if (customBindingEdit && gridRef && gridRef.selectionModule) { + requestAnimationFrame(() => { + attemptFocusAfterSave(); + }); + } else if (isAddOperation && gridRef && gridRef.selectionModule) { + // Calculate the correct row index to select based on newRowPosition + let rowIndexToSelect: number = editState.editRowIndex; + + // For showAddNewRow with Bottom position, the newly added row will be at the end + if (defaultEditSettings.showAddNewRow) { + if (defaultEditSettings.newRowPosition === 'Bottom') { + // For bottom position, the new row is added at the end of the current data + rowIndexToSelect = viewData.length; // This will be the index after the data is updated + } else { + // For top position (default), the new row is added at index 0 + rowIndexToSelect = 0; + } + } + + setTimeout(() => { + if (gridRef.selectionModule && rowIndexToSelect >= 0) { + gridRef.selectionModule?.selectRow(rowIndexToSelect); + + // Focus the corresponding cell after auto-selection + // This ensures that the focus moves to the selected row's first visible cell + gridRef?.focusModule?.setGridFocus(true); + requestAnimationFrame(() => { + // Navigate to the selected row's first visible cell + gridRef?.focusModule?.navigateToCell(rowIndexToSelect, focusModule.firstFocusableContentCellIndex[1]); + }); + } + }, 0); + } + requestAnimationFrame(() => { + const tr: HTMLTableRowElement = gridRef.contentTableRef?.rows?.[gridRef.contentTableRef?.rows?.length - 1]; + if (gridRef.height !== 'auto' && !isAddOperation && (editState.editRowIndex + 1).toString() === tr.getAttribute('aria-rowindex') && + (gridRef.contentPanelRef?.firstElementChild as HTMLElement)?.offsetHeight > gridRef.contentTableRef?.scrollHeight) { + addClass([].slice.call(tr.getElementsByClassName('sf-rowcell')), 'sf-lastrowcell'); + } + }); + const eventArgs: SaveEvent = { + action: actionCompleteArgs.action as string, + editedRowData: actionCompleteArgs.rowData, + rowData: actionCompleteArgs.previousData, + rowIndex: actionCompleteArgs.rowIndex as number + }; + gridRef?.onDataChangeComplete?.(eventArgs); + _gridRef.current.element.removeEventListener('actionComplete', addDeleteActionComplete); + }; + + _gridRef.current.element.addEventListener('actionComplete', addDeleteActionComplete); + if (!isAddOperation && !customBindingEdit) { + _gridRef.current.element.dispatchEvent(new CustomEvent('actionComplete')); + } + + editDataRef.current = null; + + // This ensures showAddNewRow inputs are properly re-enabled after saving edits + const newEditState: Partial = { + editRowIndex: -1, + editData: null, + originalData: null, + validationErrors: {} + }; + + if (defaultEditSettings.showAddNewRow) { + newEditState.isEdit = true; + newEditState.isShowAddNewRowActive = true; + + // Explicitly set isShowAddNewRowDisabled to false + newEditState.isShowAddNewRowDisabled = false; + + // Restore the original add form data and set as current edit data + newEditState.showAddNewRowData = editState.showAddNewRowData; + newEditState.editData = editState.showAddNewRowData; + newEditState.originalData = null; // null for showAddNewRow operations + + // Update edit data ref with the restored add row data + // This ensures consistent data state across component + editDataRef.current = editState.showAddNewRowData ? { ...editState.showAddNewRowData } : null; + } else { + // Normal behavior - exit edit state completely + newEditState.isEdit = false; + newEditState.isShowAddNewRowActive = false; + newEditState.isShowAddNewRowDisabled = false; + newEditState.showAddNewRowData = null; + } + + // Always ensure isShowAddNewRowDisabled is explicitly set to false + // This is essential for test case: "should re-enable showAddNewRow inputs after saving edited row" + setEditState((prev: EditState) => ({ + ...prev, + ...newEditState, + // Always force isShowAddNewRowDisabled to false when showAddNewRow is enabled + isShowAddNewRowDisabled: false + })); + + // Dispatch custom event for toolbar refresh when exiting edit mode + const exitEditGridElement: HTMLDivElement | null | undefined = _gridRef?.current?.element; + const editStateEvent: CustomEvent = new CustomEvent('editStateChanged', { + detail: { + isEdit: newEditState.isEdit || false, + editRowIndex: newEditState.editRowIndex || -1 + } + }); + exitEditGridElement?.dispatchEvent(editStateEvent); + + const attemptFocusAfterSave: () => void = () => { + if (nextPrevEditRow()) { + return; + } + const lastFocusedCellinfo: FocusedCellInfo = !_gridRef.current?.allowKeyboard ? + { + rowIndex: notKeyBoardAllowedClickRowInfo.current.rowIndex, + colIndex: notKeyBoardAllowedClickRowInfo.current?.columnIndex, + isHeader: false + } : _gridRef.current.focusModule.getFocusedCell(); + // First ensure grid has focus + gridRef?.focusModule?.setGridFocus(true); + + // Select the appropriate row + gridRef?.selectionModule?.selectRow(lastFocusedCellinfo?.rowIndex > -1 && endAction === 'Click' ? lastFocusedCellinfo?.rowIndex : savedRowIndex); + + // Calculate the proper target row index based on configuration + const targetRowIndex: number = lastFocusedCellinfo?.rowIndex > -1 ? lastFocusedCellinfo?.rowIndex : + (defaultEditSettings.showAddNewRow && defaultEditSettings.newRowPosition === 'Top' ? + savedRowIndex + 1 : savedRowIndex); + requestAnimationFrame(() => { + gridRef?.focusModule?.navigateToCell(endAction === 'Click' ? targetRowIndex : savedRowIndex, lastFocusedCellinfo?.colIndex !== -1 && endAction === 'Click' ? + lastFocusedCellinfo?.colIndex : endAction === 'Key' ? escEnterIndex.current : 0); + }); + }; + + // Only perform special focus management for keyboard (Tab) navigation + // maintain focus on the clicked cell while Tab navigation follows standard patterns + requestAnimationFrame(() => { + if (!isAddOperation && !customBindingEdit) { + attemptFocusAfterSave(); + } + }); + + return true; + }, [ + editState, + getCurrentEditData, + dataOperations, + _gridRef, + focusModule + ]); + + /** + * Closes editing without saving changes + * Enhanced to handle showAddNewRow behavior correctly + * When showAddNewRow is enabled, canceling should re-enable the add new row and keep grid in edit state + */ + const cancelChanges: (endAction?: EditEndAction) => Promise = useCallback(async (endAction?: EditEndAction) => { + // Handle showAddNewRow special case + // If showAddNewRow is enabled and we only have the add new row active (no edited row), + // then we should just re-enable the add new row and return + if (defaultEditSettings.showAddNewRow && editState.isShowAddNewRowActive && editState.editRowIndex === -1 && + !editState.isShowAddNewRowDisabled && !editState.originalData && + Object.keys(_gridRef.current?.addInlineRowFormRef?.current?.formState?.modified).length) { + const defaultRecord: Object = {}; + setDefaultValueRecords(defaultRecord); + const editStateEvent: CustomEvent = new CustomEvent('resetShowAddNewRowForm', { + detail: { + editData: defaultRecord + } + }); + _gridRef.current?.addInlineRowFormRef?.current?.formRef?.current?.element?.dispatchEvent?.(editStateEvent); + // Re-enable the add new row and clear any validation errors + setEditState((prev: EditState) => ({ + ...prev, + validationErrors: {}, + editData: prev.showAddNewRowData // Reset to original add new row data + })); + + // Reset the edit data ref + editDataRef.current = editState.showAddNewRowData ? { ...editState.showAddNewRowData } : null; + return; + } + + if (!editState.isEdit) { + return; + } + + // For inline/normal editing, do NOT show confirm dialog + // Trigger cancel event + const cancelArgs: Record = { + requestType: 'cancel', + rowIndex: editState.editRowIndex, + rowData: getCurrentEditData(), + formRef: getCurrentFormRef() + }; + + setTimeout(() => { + _gridRef.current?.selectionModule?.selectRow(editState.editRowIndex); + _gridRef.current?.focusModule?.setGridFocus(true); + requestAnimationFrame(() => { + _gridRef.current?.focusModule?.navigateToCell(editState.editRowIndex, endAction === 'Key' ? escEnterIndex.current : 0); + }); + }, 0); + const eventArgs: CancelFormEvent = { + formRef: cancelArgs.formRef as RefObject, + rowData: cancelArgs.rowData, + rowIndex: cancelArgs.rowIndex as number + }; + _gridRef.current?.onDataChangeCancel?.(eventArgs); + + // Enhanced reset edit state with improved showAddNewRow handling + // This ensures showAddNewRow inputs are properly re-enabled after cancel + const newEditState: Partial = { + editRowIndex: -1, + editData: null, + originalData: null, + validationErrors: {} + }; + + if (defaultEditSettings.showAddNewRow) { + newEditState.isEdit = true; + newEditState.isShowAddNewRowActive = true; + + // Explicitly set isShowAddNewRowDisabled to false + // This ensures inputs are always re-enabled after cancel + newEditState.isShowAddNewRowDisabled = false; + + // Restore the original add form data and set as current edit data + newEditState.showAddNewRowData = editState.showAddNewRowData; + newEditState.editData = editState.showAddNewRowData; + newEditState.originalData = null; // Empty for add operations + + // Update edit data ref with the restored add row data + // This ensures consistent data state across component + editDataRef.current = editState.showAddNewRowData ? { ...editState.showAddNewRowData } : null; + } else { + // Normal behavior - exit edit state completely + newEditState.isEdit = false; + newEditState.isShowAddNewRowActive = false; + newEditState.isShowAddNewRowDisabled = false; + newEditState.showAddNewRowData = null; + editDataRef.current = null; + } + + // Always ensure isShowAddNewRowDisabled is explicitly set to false + // This is essential for test case: "should re-enable showAddNewRow inputs after canceling edited row" + setEditState((prev: EditState) => ({ + ...prev, + ...newEditState, + // Always force isShowAddNewRowDisabled to false when showAddNewRow is enabled + isShowAddNewRowDisabled: false + })); + + // Dispatch custom event for toolbar refresh when canceling edit mode + const cancelEditGridElement: HTMLDivElement | null | undefined = _gridRef?.current?.element; + const editStateEvent: CustomEvent = new CustomEvent('editStateChanged', { + detail: { + isEdit: newEditState.isEdit || false, + editRowIndex: newEditState.editRowIndex || -1 + } + }); + cancelEditGridElement?.dispatchEvent(editStateEvent); + }, [editState, defaultEditSettings.showAddNewRow, _gridRef, _gridRef.current?.editInlineRowFormRef?.current?.formState, + _gridRef.current?.addInlineRowFormRef?.current?.formState]); + + const setDefaultValueRecords: (data: Object) => void = (data: Object) => { + columns.forEach((column: ColumnProps) => { + // Only set value if column has explicit defaultValue + // Otherwise leave undefined to render truly empty edit forms + if (column.defaultValue !== undefined) { + // Apply the explicit default value + if (column.type === 'string') { + data[column.field] = typeof column.defaultValue === 'string' + ? column.defaultValue + : String(column.defaultValue); + } else { + data[column.field] = column.defaultValue; + } + } + // Don't set any value if no defaultValue is specified + }); + }; + /** + * Adds a new record to the grid + */ + const addRecord: (data?: Object, index?: number) => void = + useCallback(async (data?: Record, index?: number) => { + if (!defaultEditSettings.allowAdd || _gridRef.current?.addInlineRowFormRef?.current?.formRef?.current) { + return; + } + + // Create new record with proper default value handling + // Only apply defaultValue when explicitly set, otherwise leave undefined + const newRecord: Record = data || {}; + + // If no data provided, only initialize fields that have explicit defaultValue + if (!data) { + setDefaultValueRecords(newRecord); + } + + let insertIndex: number; + if (index !== undefined) { + // Only use provided index when explicitly passed programmatically + // This is the ONLY case where addRecord should add at a specific position + insertIndex = index; + } else { + // For normal addRecord operations (like button clicks), + // NEVER use selected row index - only use newRowPosition setting + if (defaultEditSettings.newRowPosition === 'Top') { + insertIndex = 0; // Always add at top + } else { + insertIndex = viewData.length; // Always add at bottom + } + // Remove the selected row logic completely for normal add operations + // This was causing the issue where add record was adding at selected row index + } + + const actionBeginArgs: Record = { + cancel: false, + requestType: 'add', + type: 'actionBegin', + rowData: newRecord, + index: insertIndex, + action: 'add' + }; + + // Get grid reference for actionBegin event + const gridRef: GridRef | null = _gridRef?.current; + if (gridRef && (gridRef.onRowAddStart) && !data) { + const startArgs: RowAddEvent = { + cancel: false, + rowData: actionBeginArgs.rowData, + rowIndex: actionBeginArgs.index as number + }; + gridRef?.onRowAddStart?.(startArgs); + + // If the operation was cancelled, return early + if (startArgs.cancel) { + return; + } + } + + // Set edit state for add operation without updating currentViewData + // This creates a dummy edit row that doesn't affect the data source until save + + // Initialize the edit data ref to prevent re-renders during typing + editDataRef.current = { ...newRecord }; + + // When using toolbar add or programmatic addRecord(), + // we need to ensure we're not triggering the isEditingExistingRow logic + // Set originalData explicitly to null or empty object to signal this is an add operation + setEditState((prev: EditState) => ({ + ...prev, + isEdit: !data ? true : false, + editRowIndex: insertIndex, + editData: { ...newRecord }, // Use the newRecord (which may be empty) for add operations + originalData: null, // For toolbar/programmatic add, explicitly set to null + validationErrors: {} + })); + + if (!data) { + // Dispatch custom event for toolbar refresh when entering add mode + const addRecordGridElement: HTMLDivElement | null | undefined = _gridRef?.current?.element; + const editStateEvent: CustomEvent = new CustomEvent('editStateChanged', { + detail: { + isEdit: true, + editRowIndex: insertIndex, + isAdd: true // Explicitly mark this as an add operation + } + }); + addRecordGridElement?.dispatchEvent(editStateEvent); + + // STEP 3: Trigger actionComplete event after the grid is actually in edit state + // Use requestAnimationFrame to ensure DOM is updated and edit form is rendered + requestAnimationFrame(() => { + // Additional timeout to ensure edit form is fully rendered + setTimeout(() => { + const actionCompleteArgs: Record = { + requestType: 'add', + type: 'actionComplete', + data: { ...newRecord }, + rowIndex: insertIndex, + action: 'add', + rowData: { ...newRecord }, + form: gridRef?.addInlineRowFormRef.current?.formRef.current?.element + }; + const eventArgs: FormRenderEvent = { + formRef: gridRef?.addInlineRowFormRef.current?.formRef as RefObject, + rowData: actionCompleteArgs.rowData, + rowIndex: actionCompleteArgs.rowIndex as number + }; + gridRef?.onFormRender?.(eventArgs); + gridRef?.focusModule?.removeFocusTabIndex(); + prevFocusedCell.current = { rowIndex: -1, colIndex: -1, isHeader: false }; + }, 0); + }); + } else { + saveChanges(false, insertIndex); + } + }, [ + defaultEditSettings.allowAdd, + defaultEditSettings.newRowPosition, + defaultEditSettings.mode, + dataOperations, + _gridRef, + columns, + viewData + ]); + + /** + * Deletes a record from the grid + */ + const deleteRecord: (fieldName?: string, data?: Object | Object[]) => Promise = + useCallback(async (fieldName?: string, data?: Object | Object[]) => { + const eventTarget: HTMLElement = event?.target as HTMLElement; + if (!defaultEditSettings.allowDelete) { + return; + } + + let recordsToDelete: Object[] = []; + let deleteIndexes: number[] = []; + + if (!data) { + // Delete selected records using selection module (multiple row support) + const gridRef: GridRef | null = _gridRef?.current; + const selectedIndexes: number[] = gridRef.selectionModule.getSelectedRowIndexes(); + if (selectedIndexes && selectedIndexes.length > 0) { + // Handle multiple selected rows for deletion + deleteIndexes = [...selectedIndexes]; + recordsToDelete = selectedIndexes.map((index: number) => viewData[index as number]); + } else { + // Show validation message if no records are selected + const message: string = localization?.getConstant('noRecordsDeleteMessage'); + await dialogHook?.confirmOnEdit({ + title: '', + message: message, + confirmText: localization?.getConstant('okButtonLabel'), + cancelText: '', // No cancel button for alert dialogs + type: 'info' + }); + eventTarget?.focus?.(); + return; + } + } else { + // Handle single record deletion with provided data + if (data instanceof Array) { + const dataLen: number = data.length; + const primaryKeyField: string = fieldName || getPrimaryKeyField(); + + for (let i: number = 0; i < dataLen; i++) { + let tmpRecord: Object; + const contained: boolean = viewData.some((record: Object) => { + tmpRecord = record; + return data[i as number] === (record as Record)[primaryKeyField as string] || + data[i as number] === record; + }); + + if (contained) { + recordsToDelete.push(tmpRecord); + const index: number = viewData.indexOf(tmpRecord); + if (index !== -1) { + deleteIndexes.push(index); + } + } else { + // Handle case where data[i] is a partial record with primary key + const recordData: Object = (data[i as number] as Record)[primaryKeyField as string] ? + data[i as number] : { [primaryKeyField as string]: data[i as number] }; + recordsToDelete.push(recordData); + + // Find index by primary key + const index: number = viewData.findIndex((item: Object) => + (item as Record)[primaryKeyField as string] === + (recordData as Record)[primaryKeyField as string] + ); + if (index !== -1) { + deleteIndexes.push(index); + } + } + } + } else if (fieldName) { + // Find record by field value + const deleteIndex: number = viewData.findIndex((item: Object) => + (item as Record)[fieldName as string] === (data as Record)[fieldName as string] + ); + if (deleteIndex !== -1) { + recordsToDelete = [viewData[deleteIndex as number] as Object]; + deleteIndexes = [deleteIndex as number]; + } + } else { + // Single record deletion + recordsToDelete = [data as Object]; + const index: number = viewData.indexOf(data); + if (index !== -1) { + deleteIndexes = [index as number]; + } + } + } + + if (recordsToDelete.length === 0) { + return; + } + + // Show delete confirmation if enabled + if (defaultEditSettings.confirmOnDelete) { + // Use React dialog component instead of window.confirm + // This provides a consistent UI experience and follows React patterns + const confirmResult: boolean = await confirmOnDelete(); + if (!confirmResult) { + eventTarget?.focus?.(); + return; + } + } + + // This ensures consistent event handling pattern across all grid operations + const actionBeginArgs: Record = { + cancel: false, + requestType: 'delete', + type: 'actionBegin', + data: recordsToDelete, + rows: deleteIndexes.map((index: number) => _gridRef?.current?.getRowByIndex?.(index)).filter(Boolean), + action: 'delete' + }; + + // Get grid reference for actionBegin event + const gridRef: GridRef | null = _gridRef?.current; + const startArgs: DeleteEvent = { + action: actionBeginArgs.action as string, + data: actionBeginArgs.data as Object[], + cancel: false + }; + gridRef?.onDataChangeStart?.(startArgs); + + // If the operation was cancelled, return early + if (startArgs.cancel) { + return; + } + setGridAction({}); + + // Store the selected row index before deletion for auto-selection after deletion + const lastFocusedCellinfo: FocusedCellInfo = _gridRef.current.focusModule.getFocusedCell(); + const selectedRowIndexBeforeDeletion: number = lastFocusedCellinfo.rowIndex; + const customBinding: boolean = dataOperations.dataManager && 'result' in dataOperations.dataManager; + + // Single deletion: use dataManager.remove() + // Multiple deletion: use dataManager.saveChanges() with deletedRecords + try { + const len: number = recordsToDelete.length; + + if (len === 1) { + await dataOperations.getData(customBinding ? actionBeginArgs : { + requestType: 'delete', + data: recordsToDelete[0] + }); + // Single record deletion + } else { + await dataOperations.getData(customBinding ? actionBeginArgs : { + requestType: 'delete', + data: recordsToDelete + }); + } + let isCurrentPageChanged: boolean = false; + if (((len === 1 && (_gridRef.current?.currentViewData.length - len) <= 0) || + ((_gridRef.current?.currentViewData.length - recordsToDelete.length) <= 0)) && + (_gridRef.current?.pageSettings?.currentPage - 1) >= 1 && _gridRef.current?.pagerModule) { + setCurrentPage(_gridRef.current?.pageSettings?.currentPage - 1); + _gridRef.current?.pagerModule?.goToPage(_gridRef.current?.pageSettings?.currentPage - 1); + isCurrentPageChanged = true; + } else if (!customBinding) { + _gridRef.current?.refresh(); // initiate getData with requestType as 'refresh' + } + + // Trigger actionComplete event after successful operation + const actionCompleteArgs: Record = { + requestType: 'delete', + type: 'actionComplete', + data: recordsToDelete, + rows: deleteIndexes.map((index: number) => _gridRef?.current?.getRowByIndex?.(index)).filter(Boolean), + action: 'delete' + }; + + const addDeleteActionComplete: () => void = () => { + // This ensures that after deletion, the grid automatically selects the corresponding row index + if (gridRef && gridRef.selectionModule && selectedRowIndexBeforeDeletion >= 0 && !eventTarget) { + // Calculate the new row index to select after deletion + let newRowIndexToSelect: number = selectedRowIndexBeforeDeletion; + + // If the deleted row was the last row, select the previous row + if (selectedRowIndexBeforeDeletion >= _gridRef.current?.currentViewData?.length - recordsToDelete.length) { + newRowIndexToSelect = Math.max(0, _gridRef.current?.currentViewData?.length - recordsToDelete.length - 1); + } + + // Auto-select the corresponding row after deletion + // the row at the same index (or previous if it was the last row) after deletion + if ((newRowIndexToSelect >= 0 && newRowIndexToSelect < viewData.length - recordsToDelete.length) || + isCurrentPageChanged) { + // Use setTimeout to ensure the data source is updated before selection + setTimeout(() => { + gridRef?.selectionModule?.selectRow(newRowIndexToSelect); + + // Focus the corresponding cell after auto-selection + // This ensures that the focus moves to the selected row's first visible cell + gridRef?.focusModule?.setGridFocus(true); + requestAnimationFrame(() => { + // Navigate to the selected row's first visible cell + gridRef.focusModule?.navigateToCell(newRowIndexToSelect, lastFocusedCellinfo.colIndex !== -1 ? + lastFocusedCellinfo.colIndex : 0); + }); + }, 0); + } + } + eventTarget?.focus?.(); + const eventArgs: DeleteEvent = { + action: actionCompleteArgs.action as string, + data: actionCompleteArgs.data as Object[] + }; + gridRef?.onDataChangeComplete?.(eventArgs); + _gridRef.current.element.removeEventListener('actionComplete', addDeleteActionComplete); + }; + _gridRef.current.element.addEventListener('actionComplete', addDeleteActionComplete); + } catch (error) { + // Trigger actionFailure event on error + // This provides consistent error handling similar to other grid operations + gridRef?.onError({ + error: error as Error + }); + return; + } + }, [ + defaultEditSettings.allowDelete, + defaultEditSettings.confirmOnDelete, + defaultEditSettings.mode, + _gridRef, + viewData, + getPrimaryKeyField + ]); + + /** + * Updates a specific row with new data + */ + const updateRow: (index: number, data: Object) => void = + useCallback(async (index: number, data: Object) => { + if (!defaultEditSettings.allowEdit || index < 0 || index >= viewData.length) { + return; + } + + const previousData: Object = viewData[index as number]; + + const actionBeginArgs: Record = { + cancel: false, + requestType: 'save', + type: 'actionBegin', + rowData: data, + index: index, + action: 'edit', + previousData: previousData + }; + + // Get grid reference for actionBegin event + const gridRef: GridRef | null = _gridRef?.current; + const startArgs: SaveEvent = { + action: actionBeginArgs.action as string, + editedRowData: actionBeginArgs.rowData, + rowData: actionBeginArgs.previousData, + rowIndex: actionBeginArgs.index as number, + cancel: false + }; + gridRef?.onDataChangeStart?.(startArgs); + + // If the operation was cancelled, return early + if (startArgs.cancel) { + return; + } + + // Perform CRUD operation through DataManager + try { + await dataOperations.getData({ + requestType: 'update', + data + }); + + // Update local data source for immediate UI feedback + const selectedRow: IRow = _gridRef.current?.getRowsObject()[index as number]; + const rowObjectData: Object = { ...selectedRow.data, ...data }; + selectedRow.setRowObject({ ...selectedRow, data: rowObjectData }); + + // Trigger actionComplete event AFTER successful operation + const actionCompleteArgs: Record = { + requestType: 'save', + type: 'actionComplete', + rowData: data, + rowIndex: index, + action: 'edit', + previousData: previousData + }; + const eventArgs: SaveEvent = { + action: actionCompleteArgs.action as string, + editedRowData: actionCompleteArgs.rowData, + rowData: actionCompleteArgs.previousData, + rowIndex: actionCompleteArgs.rowIndex as number + }; + gridRef?.onDataChangeComplete?.(eventArgs); + + } catch (error) { + // Trigger actionFailure event on error + // This provides consistent error handling similar to other grid operations + gridRef?.onError({ + error: error as Error + }); + } + }, [defaultEditSettings.allowEdit, viewData, _gridRef]); + + /** + * Use a stable ref-based approach for edit data management + * This prevents re-renders during typing while maintaining data consistency + */ + const editDataRef: React.RefObject = + useRef>(null); + + /** + * Enhanced updateEditData function with proper data isolation and persistence + */ + const updateEditData: (field: string, value: string | number | boolean | Record | Date) => void = + useCallback((field: string, value: string | number | boolean | Record | Date) => { + if (!editState.isEdit) { + return; + } + + // Initialize editDataRef if it's null + if (!editDataRef.current) { + editDataRef.current = { ...editState.editData }; + } + + // Update ref immediately for instant access without triggering re-renders + // This is the key to preventing multiple EditForm re-renders during typing + const topLevelKey: string = field.split('.')[0]; + const copiedComplexData: Object = field.includes('.') && typeof editDataRef.current[topLevelKey as string] === 'object' + ? { + ...editDataRef.current, + [topLevelKey]: JSON.parse(JSON.stringify(editDataRef.current[topLevelKey as string])) + } + : { ...editDataRef.current }; + + editDataRef.current = DataUtil.setValue(field, value, copiedComplexData); + + // Update the state editData to persist values + // This ensures that typed values are maintained even when focus moves out of grid + setEditState((prev: EditState) => ({ + ...prev, + editData: { + ...editDataRef.current + } + })); + + }, [editState.isEdit, editState.editData, _gridRef.current]); + + /** + * Handle click events for showAddNewRow functionality and validation workflow + */ + const handleGridClick: (event: React.MouseEvent) => void = useCallback(async (event: React.MouseEvent) => { + const target: Element = event.target as Element; + + // Check if the click is within grid content or header content (for frozen rows) + const isWithinGridContent: boolean | Element = target.closest('.sf-gridcontent'); + + // Only handle clicks within grid content and not on unbound cells + if (isWithinGridContent && !target.closest('.sf-unboundcelldiv')) { + + // If grid is in edit mode with an actual edited row, end the current edit + const hasEditedRow: boolean = editState.editRowIndex >= 0 && !isNullOrUndefined(editState.editData); + + if (editState.isEdit && hasEditedRow && !target.closest('.sf-form-validator')) { + notKeyBoardAllowedClickRowInfo.current = !_gridRef.current?.allowKeyboard ? _gridRef.current?.getRowInfo?.(target) : {}; + saveChanges(undefined, undefined, 'Click'); + const isValid: boolean = validateEditForm(); + // If save failed (validation errors), prevent the click from proceeding + if (!isValid) { + event.preventDefault(); + event.stopPropagation(); + return; + } + } + } + }, [ + defaultEditSettings.showAddNewRow, + editState.isShowAddNewRowActive, + editState.editRowIndex, + editState.editData, + editState.originalData, + editState.isEdit, + saveChanges, + _gridRef + ]); + + /** + * Enhanced double-click handler for showAddNewRow functionality + * This ensures proper interaction between showAddNewRow and normal data row editing + */ + const handleGridDoubleClick: (event: React.MouseEvent) => void = + useCallback((event: React.MouseEvent) => { + + // Use editModule?.editSettings instead of rest.editSettings to get proper defaults + // The editModule applies default values including editOnDoubleClick: true + const editSettings: EditSettings = defaultEditSettings; + + // Only handle double-click for editing if editing is enabled + // Check editOnDoubleClick with proper default value (true) + const editOnDoubleClick: boolean = editSettings.editOnDoubleClick !== false; // Default to true + if (!editSettings.allowEdit || !editOnDoubleClick) { + return; + } + + const target: Element = event.target as Element; + + const clickedCell: HTMLTableCellElement = target.closest('td[role="gridcell"], th[role="columnheader"]') as HTMLTableCellElement; + + // Only proceed if we clicked on a valid cell + if (!clickedCell) { + return; + } + + const clickedRow: HTMLTableRowElement = clickedCell.closest('tr[role="row"]') as HTMLTableRowElement; + const rowElement: HTMLTableRowElement = clickedRow; + // Only proceed if we have a valid data row with proper attributes + if (!clickedRow || (!clickedRow.hasAttribute('aria-rowindex') && !clickedRow.hasAttribute('aria-rowindex'))) { + return; + } + + // Handle showAddNewRow double-click behavior first - this is critical + if (editSettings.showAddNewRow) { + // Check if the double-click is on the add new row - if so, ignore it + const isAddNewRowClick: boolean = target.closest('.sf-addedrow') !== null || + target.closest('tr[data-uid*="grid-add-row"]') !== null || + clickedRow.classList.contains('sf-addedrow') || + (clickedRow.getAttribute('data-uid') && clickedRow.getAttribute('data-uid').includes('grid-add-row')); + if (isAddNewRowClick) { + return; // Don't allow editing the add new row via double-click + } + } + + // Basic preconditions check + if (!defaultEditSettings.editOnDoubleClick || !defaultEditSettings.allowEdit) { + return; + } + + // Only allow double-click on row cells + const isRowCellClick: boolean = target.closest('td[role="gridcell"]') !== null; + if (!isRowCellClick) { + return; + } + + // Start editing the double-clicked row + // This ensures double-clicking always starts edit mode on data rows + // even when showAddNewRow is enabled + event.preventDefault(); // Prevent text selection on double-click + + editRow(rowElement); + }, [ + defaultEditSettings.editOnDoubleClick, + defaultEditSettings.allowEdit, + defaultEditSettings.showAddNewRow, + editRow, + editState.isEdit, + editState.editRowIndex, + editState.editData, + editState.originalData + ]); + + /** + * Enhanced showAddNewRow initialization with proper toolbar integration + * This effect manages the persistent add new row feature that allows users to add records + * without clicking an "Add" button. The grid remains in edit state when this feature is enabled. + */ + useEffect(() => { + if (defaultEditSettings.showAddNewRow && defaultEditSettings.allowAdd) { + // Initialize the add new row data with default values + const newRowData: string | number | boolean | Record | Date = {}; + + // Only apply defaultValue when explicitly set, otherwise leave undefined + columns.forEach((column: ColumnProps) => { + if (column.field && column.defaultValue !== undefined) { + // Apply the explicit default value + if (column.type === 'string') { + newRowData[column.field] = typeof column.defaultValue === 'string' + ? column.defaultValue + : String(column.defaultValue); + } else { + newRowData[column.field] = column.defaultValue; + } + } + // Don't set any value if no defaultValue is specified + }); + + // Set the grid in edit state with the add new row + setEditState((prev: EditState) => ({ + ...prev, + isEdit: true, // Grid remains in edit state when showAddNewRow is enabled + showAddNewRowData: newRowData, + isShowAddNewRowActive: true, + isShowAddNewRowDisabled: false, // Initially enabled + editRowIndex: -1, // Special index for add new row + editData: newRowData, + originalData: null, // Empty for add operations + validationErrors: {} + })); + + // Initialize the edit data ref for the add new row + editDataRef.current = { ...newRowData }; + + // Dispatch custom event for toolbar refresh when entering showAddNewRow mode + // This ensures toolbar buttons maintain proper state (Update/Cancel enabled) + const gridElement: HTMLDivElement | null | undefined = _gridRef?.current?.element; + // Synthetic event to update toolbar state for showAddNewRow mode + const toolbarStateEvent: CustomEvent = new CustomEvent('editStateChanged', { + detail: { + isEdit: true, + editRowIndex: -1, + isShowAddNewRowActive: true + } + }); + gridElement?.dispatchEvent(toolbarStateEvent); + } else { + // Reset showAddNewRow state when disabled + setEditState((prev: EditState) => ({ + ...prev, + showAddNewRowData: null, + isShowAddNewRowActive: false, + isShowAddNewRowDisabled: false, + isEdit: false // Reset edit state when showAddNewRow is disabled + })); + editDataRef.current = null; + + // Update toolbar state when showAddNewRow is disabled + const gridElement: HTMLDivElement | null | undefined = _gridRef?.current?.element; + const toolbarStateEvent: CustomEvent = new CustomEvent('editStateChanged', { + detail: { + isEdit: false, + editRowIndex: -1, + isShowAddNewRowActive: false + } + }); + gridElement?.dispatchEvent(toolbarStateEvent); + } + }, [defaultEditSettings.showAddNewRow, defaultEditSettings.allowAdd, _gridRef]); + + /** + * Checks if there are unsaved changes and shows confirmation dialog if needed + * + * @private + * @returns { Promise } - true if operation should proceed, false if cancelled + */ + const checkUnsavedChanges: () => Promise = useCallback(async (): Promise => { + const hasUnsavedChanges: boolean | Object = _gridRef.current?.editInlineRowFormRef?.current?.formRef?.current || + _gridRef.current?.addInlineRowFormRef?.current?.formRef?.current; + + if (hasUnsavedChanges && defaultEditSettings.confirmOnEdit) { + // Show confirmation dialog for unsaved changes + const message: string = localization.getConstant('unsavedChangesConfirmation'); + focusModule?.clearIndicator?.(); + const confirmResult: boolean = await dialogHook.confirmOnEdit({ + title: '', + message: message, + confirmText: localization.getConstant('okButtonLabel'), + cancelText: localization.getConstant('cancelButtonLabel'), + type: 'warning' + }); + + focusModule?.addFocus?.(focusModule?.getFocusedCell?.()); + if (!confirmResult) { + // User cancelled, prevent the data operation + return false; + } + } + // User confirmed to proceed with losing changes + // Force close edit state without saving + setEditState((prev: EditState) => ({ + ...prev, + originalData: null, + editRowIndex: -1, + editData: !defaultEditSettings.showAddNewRow ? null : prev.editData, + isEdit: defaultEditSettings.showAddNewRow + })); + requestAnimationFrame(() => { + const editStateEvent: CustomEvent = new CustomEvent('editStateChanged', { + detail: { + isEdit: defaultEditSettings.showAddNewRow, + editRowIndex: -1 + } + }); + _gridRef.current?.element?.dispatchEvent(editStateEvent); + }); + // No unsaved changes, operation can proceed + return true; + }, [_gridRef.current, editState.isEdit, editState.originalData, editState.editData, localization, dialogHook]); + + return { + // Edit state + isEdit: editState.isEdit, + editSettings: defaultEditSettings, + editRowIndex: editState.editRowIndex, + editData: editState.editData, // Always return state data for proper form binding + validationErrors: editState.validationErrors, + originalData: editState.originalData, + + // showAddNewRow state + showAddNewRowData: editState.showAddNewRowData, + isShowAddNewRowActive: editState.isShowAddNewRowActive, + isShowAddNewRowDisabled: editState.isShowAddNewRowDisabled, + + // Edit operations + editRow, + saveChanges, + cancelChanges, + + // CRUD operations + addRecord, + deleteRecord, + updateRow, + + // Validation + validateEditForm, + validateField, + + // Real-time edit data updates + updateEditData, + + // Get current edit data + getCurrentEditData, + getCurrentFormRef, + getCurrentFormState, + + // Event handlers for showAddNewRow functionality + handleGridClick, + handleGridDoubleClick, + + // Batch save lost changes confirmation + checkUnsavedChanges, + + // Dialog state and methods for confirmation dialogs + isDialogOpen: dialogHook.isDialogOpen, + dialogConfig: dialogHook.dialogConfig, + onDialogConfirm: dialogHook.onDialogConfirm, + onDialogCancel: dialogHook.onDialogCancel, + nextPrevEditRowInfo, + focusLastField, + escEnterIndex + }; +}; diff --git a/components/grids/src/grid/hooks/useEditDialog.ts b/components/grids/src/grid/hooks/useEditDialog.ts new file mode 100644 index 0000000..fe5d05e --- /dev/null +++ b/components/grids/src/grid/hooks/useEditDialog.ts @@ -0,0 +1,84 @@ +import { useState, useCallback } from 'react'; +import { IL10n } from '@syncfusion/react-base'; +import { ServiceLocator } from '../types/interfaces'; +import { DialogState, ConfirmDialogConfig, UseConfirmDialogResult } from '../types/edit.interfaces'; +/** + * Hook for managing edit confirmation dialogs + * + * This hook provides a React-based dialog system to replace window.confirm + * and window.alert usage in the grid editing functionality. + * + * @private + * @param {ServiceLocator} serviceLocator - Grid service locator + * @returns {UseConfirmDialogResult} Dialog state and control methods + */ +export const useConfirmDialog: (serviceLocator: ServiceLocator) => UseConfirmDialogResult = + (serviceLocator: ServiceLocator): UseConfirmDialogResult => { + const [dialogState, setDialogState] = useState({ + isOpen: false, + config: null, + onConfirm: null, + onCancel: null + }); + const localization: IL10n = serviceLocator?.getService('localization'); + /** + * Show a confirmation dialog + * + * @param {ConfirmDialogConfig} config - Dialog configuration + * @returns {Promise} - Promise that resolves to true if confirmed, false if cancelled + */ + const confirmOnEdit: (config: ConfirmDialogConfig) => Promise = + useCallback((config: ConfirmDialogConfig): Promise => { + return new Promise((resolve: (value: boolean | PromiseLike) => void) => { + setDialogState({ + isOpen: true, + config: { + confirmText: localization?.getConstant('okButtonLabel'), + cancelText: localization?.getConstant('cancelButtonLabel'), + type: 'confirm', + ...config + }, + onConfirm: () => { + setDialogState((prev: DialogState) => ({ ...prev, isOpen: false })); + resolve(true); + }, + onCancel: () => { + setDialogState((prev: DialogState) => ({ ...prev, isOpen: false })); + resolve(false); + } + }); + }); + }, [localization]); + + /** + * Show a delete confirmation dialog + * + * @returns {Promise} - Promise that resolves to true if confirmed, false if cancelled + */ + const confirmOnDelete: () => Promise = + useCallback((): Promise => { + const message: string = localization?.getConstant('confirmDeleteMessage'); + + return confirmOnEdit({ + title: '', + message, + confirmText: localization?.getConstant('okButtonLabel'), + cancelText: localization?.getConstant('cancelButtonLabel'), + type: 'delete' + }); + }, [confirmOnEdit, localization]); + + return { + // Dialog state + isDialogOpen: dialogState.isOpen, + dialogConfig: dialogState.config, + + // Dialog actions + onDialogConfirm: dialogState.onConfirm, + onDialogCancel: dialogState.onCancel, + + // Dialog methods + confirmOnEdit, + confirmOnDelete + }; + }; diff --git a/components/grids/src/grid/hooks/useFilter.ts b/components/grids/src/grid/hooks/useFilter.ts new file mode 100644 index 0000000..a41c567 --- /dev/null +++ b/components/grids/src/grid/hooks/useFilter.ts @@ -0,0 +1,746 @@ +import { useCallback, RefObject, useEffect, useState } from 'react'; +import { FilterEvent, FilterSettings, FilterPredicates, filterModule, FilterProperties, IFilterOperator } from '../types/filter.interfaces'; +import { IValueFormatter } from '../types'; +import { GridRef } from '../types/grid.interfaces'; +import { IColumnBase, ColumnProps } from '../types/column.interfaces'; +import { closest, extend, IL10n, isNullOrUndefined, matches} from '@syncfusion/react-base'; +import { DataManager, DataUtil } from '@syncfusion/react-data'; +import { getActualPropFromColl, iterateArrayOrObject } from '../utils'; +import { ServiceLocator } from '../types/interfaces'; + +/** + * Custom hook to manage filter state and configuration + * + * @private + * @param {RefObject} gridRef - Reference to the grid component + * @param {FilterSettings} filterSetting - Reference to the filter settings + * @param {Function} setGridAction - Function to update grid actions + * @param {ServiceLocator} serviceLocator - Defines the service locator + * @returns {filterModule} An object containing various filter-related state and API + */ +export const useFilter: (gridRef?: RefObject, filterSetting?: FilterSettings, + setGridAction?: (action: Object) => void, + serviceLocator?: ServiceLocator) => filterModule = (gridRef?: RefObject, + filterSetting?: FilterSettings, + setGridAction?: (action: Object) => void, + serviceLocator?: ServiceLocator) => { + const formatter: IValueFormatter = serviceLocator?.getService('valueFormatter'); + const localization: IL10n = serviceLocator?.getService('localization'); + const [filterSettings, setFilterSettings] = useState(filterSetting); + const getFilterProperties: FilterProperties = { + currentTarget: null, + isMultiSort: false, + column: {}, + value: null, + filterByMethod: true, + predicate: 'and', + operator: null, + fieldName: null, + ignoreAccent: null, + caseSensitive: null, + actualPredicate: {}, + values: {}, + cellText: {}, + refresh: true, + contentRefresh: true, + initialLoad: null, + filterStatusMsg: null, + skipStringInput: ['>', '<', '='], + skipNumberInput: ['=', ' ', '!'], + timer: null + }; + + + const filterOperators: IFilterOperator = { + contains: 'contains', endsWith: 'endswith', equal: 'equal', greaterThan: 'greaterthan', greaterThanOrEqual: 'greaterthanorequal', + lessThan: 'lessthan', lessThanOrEqual: 'lessthanorequal', notEqual: 'notequal', startsWith: 'startswith', wildCard: 'wildcard', + isNull: 'isNull', isNotNull: 'isNotNull', like: 'like' + }; + + /** + * update filterSettings properties filterModule + */ + useEffect(() => { + setFilterSettings(filterSetting); + }, [filterSetting]); + + /** + * Initialize Filter Column when filterSetting column changes + */ + useEffect(() => { + if (gridRef.current.getColumns() && filterSettings?.columns?.length) { + getFilterProperties.contentRefresh = false; + getFilterProperties.initialLoad = true; + for (const col of gridRef.current.filterSettings?.columns) { + filterByColumn( + col.field, col.operator, col.value as string, col.predicate, col.caseSensitive, + col.ignoreAccent + ); + } + getFilterProperties.initialLoad = false; + getFilterProperties.contentRefresh = true; + updateFilterMsg(); + } + }, []); + + /** + * Handle grid-level key-press event + */ + const mouseDownHandler: (e: React.MouseEvent) => void = useCallback((e: React.MouseEvent): void => { + const target: Element = e.target as Element; + if (filterSettings?.enabled && filterSettings?.type === 'FilterBar' && + target.closest('th') && target.closest('th').classList.contains('sf-filterbarcell') && + (target.classList.contains('sf-clear-icon') || target.closest('.sf-clear-icon'))) { + const targetText: HTMLInputElement = target.classList.contains('sf-clear-icon') ? + target.previousElementSibling as HTMLInputElement : + target.closest('.sf-clear-icon').previousElementSibling as HTMLInputElement; + removeFilteredColsByField((targetText.classList.contains('sf-datepicker') ? targetText.parentElement : targetText).id.slice(0, -14)); //Length of _filterBarcell = 14 + } + }, [filterSettings, getFilterProperties]); + + /** + * Handle grid-level key-press event + */ + const keyUpHandler: (event: React.KeyboardEvent) => void = useCallback((event: React.KeyboardEvent): void => { + const target: HTMLInputElement = event.target as HTMLInputElement; + if (target && matches(target, '.sf-filterbar input')) { + const closeHeaderEle: Element = closest(target, 'th.sf-filterbarcell'); + getFilterProperties.column = gridRef.current.columns.find((col: ColumnProps) => col.uid === closeHeaderEle.getAttribute('data-mappinguid')); + if (filterSettings?.mode === 'Immediate' || (event.keyCode === 13 && !(getFilterProperties.column && getFilterProperties.column.filterTemplate))) { + getFilterProperties.value = target.value.trim(); + processFilter(event, target); + } + } + }, [filterSettings, getFilterProperties]); + + const processFilter: (e: React.KeyboardEvent, target: HTMLInputElement) => void = ( + e: React.KeyboardEvent, target: HTMLInputElement): void => { + stopTimer(); + startTimer(e, target); + }; + + const startTimer: (e: React.KeyboardEvent, target: HTMLInputElement) => void = ( + e: React.KeyboardEvent, target: HTMLInputElement): void => { + getFilterProperties.timer = window.setInterval( + () => { onTimerTick(target); }, + e.keyCode === 13 ? 0 : filterSettings?.immediateModeDelay); + }; + + const stopTimer: () => void = (): void => { + window.clearInterval(getFilterProperties.timer); + }; + + const grabColumnByUidFromAllCols: (uid: string, field?: string) => ColumnProps = useCallback( + (uid: string, field?: string): ColumnProps => { + let column: ColumnProps; + const gCols: ColumnProps[] = gridRef.current.getColumns(); + for (let i: number = 0; i < gCols?.length; i++) { + if (uid === gCols?.[parseInt(i.toString(), 10)]?.uid || field === gCols?.[parseInt(i.toString(), 10)]?.field) { + column = gCols?.[parseInt(i.toString(), 10)]; + } + } + return column; + }, []); + + /** + * Removes filtered column by field name. + * + * @private + * @param {string} field - DDefines column field name to remove filter. + * @param {boolean} isClearFilterBar - Specifies whether the filter bar value needs to be cleared. + * @returns {void} + */ + const removeFilteredColsByField: (field: string, isClearFilterBar?: boolean) => void = async( + field: string, isClearFilterBar?: boolean): Promise => { + + let fCell: HTMLInputElement; + const cols: FilterPredicates[] = gridRef.current.filterSettings?.columns; + const colUid: string[] = cols.map((f: ColumnProps) => f.uid); + const colField: string[] = cols.map((f: ColumnProps) => f.field); + const filteredColsUid: string[] = colUid.filter((item: string, pos: number) => colUid.indexOf(item) === pos); + const filteredColsFeild: string[] = colField.filter((item: string, pos: number) => colField.indexOf(item) === pos); + for (let i: number = 0, len: number = filteredColsUid.length; i < len; i++) { + cols[parseInt(i.toString(), 10)].uid = cols[parseInt(i.toString(), 10)].uid; + let len: number = cols.length; + const column: ColumnProps = grabColumnByUidFromAllCols( + filteredColsUid[parseInt(i.toString(), 10)], filteredColsFeild[parseInt(i.toString(), 10)]); + if (column.field === field) { + const currentPred: FilterPredicates = gridRef.current.filterSettings?.columns?.filter?.((e: FilterPredicates) => { + return e.uid === column.uid; })[0]; + if (gridRef.current.filterSettings?.type === 'FilterBar' && !isClearFilterBar) { + const selector: string = '[id=\'' + column.field + '_filterBarcell\']'; + fCell = gridRef.current.headerPanelRef.querySelector(selector) as HTMLInputElement; + fCell?.setAttribute?.('value', ''); + delete getFilterProperties?.value; + } + const args: FilterEvent = { + cancel: false, requestType: 'clearFiltering', currentFilterObject: currentPred, + currentFilterColumn: column, action: 'clearFiltering' + }; + if (getFilterProperties.refresh) { + args.type = 'filtering'; + const confirmResult: boolean = await gridRef.current?.editModule?.checkUnsavedChanges?.(); + if (!isNullOrUndefined(confirmResult) && !confirmResult) { + return; + } + gridRef.current.onFilterStart?.(args); + if (args.cancel) { + refreshFilterSettings(); + return; + } + } + while (len--) { + if (cols[parseInt(len.toString(), 10)] && (cols[parseInt(len.toString(), 10)].uid === column.uid || + cols[parseInt(len.toString(), 10)].field === column.field)) { + cols.splice(len, 1); + if (getFilterProperties.refresh) { + if (cols.length === 0) { + setFilterSettings((prevSettings: FilterSettings) => { + return { ...prevSettings, columns: [] }; + }); + args.type = 'actionComplete'; + setGridAction(args); + } else { + setFilterSettings((prevSettings: FilterSettings) => { + return { ...prevSettings, columns: cols || [] }; + }); + args.type = 'actionComplete'; + setGridAction(args); + } + } + } + } + delete getFilterProperties.values[`${field}`]; + break; + } + } + refreshFilterSettings(); + updateFilterMsg(); + }; + + const onTimerTick: (target: HTMLInputElement) => void = (target: HTMLInputElement): void => { + const filterElement: HTMLInputElement = target; + const filterValue: string = JSON.parse(JSON.stringify(filterElement.value)); + getFilterProperties.cellText[getFilterProperties.column.field] = filterElement.value; + stopTimer(); + if (isNullOrUndefined(getFilterProperties.value) || getFilterProperties.value === '') { + removeFilteredColsByField(getFilterProperties.column.field); + return; + } + getFilterProperties.filterByMethod = false; + validateFilterValue(getFilterProperties.value as string); + filterByColumn( + getFilterProperties.column.field, getFilterProperties.operator, getFilterProperties.value, getFilterProperties.predicate, + gridRef.current.filterSettings.caseSensitive, null); + getFilterProperties.filterByMethod = true; + filterElement.value = filterValue; + }; + + const validateFilterValue: (value: string) => void = (value: string): void => { + let skipInput: string[]; + let index: number; + getFilterProperties.caseSensitive = gridRef.current.filterSettings?.caseSensitive; + getFilterProperties.column = gridRef.current.getColumns().find( + (col: ColumnProps) => col.field === getFilterProperties.column.field); + switch (getFilterProperties.column.type) { + case 'number': + if (getFilterProperties.column.filter.operator) { + getFilterProperties.operator = getFilterProperties.column.filter.operator; + } else { + getFilterProperties.operator = filterOperators.equal; + } + skipInput = ['>', '<', '=', '!']; + for (let i: number = 0; i < value.length; i++) { + if (skipInput.indexOf(value[parseInt(i.toString(), 10)]) > -1) { + index = i; + break; + } + } + getOperator(value.substring(index)); + if (index !== 0) { + getFilterProperties.value = value.substring(0, index); + } + if (getFilterProperties.value !== '' && value.length >= 1 && !isNullOrUndefined(getFilterProperties.column.format)) { + getFilterProperties.value = formatter.fromView( + getFilterProperties.value as string, + (getFilterProperties.column as IColumnBase).parseFn, + getFilterProperties.column.type + ); + } else { + getFilterProperties.value = parseFloat(getFilterProperties.value?.toString()); + } + if (isNaN(getFilterProperties.value as number)) { + getFilterProperties.filterStatusMsg = localization?.getConstant('invalidFilterMessage'); + } + break; + case 'date': + case 'datetime': + getFilterProperties.operator = filterOperators.equal; + if (getFilterProperties.value !== '' && !(getFilterProperties.value instanceof Date) && + !isNullOrUndefined(getFilterProperties.column.format)) { + if (isNullOrUndefined(getFilterProperties.column.filter?.filterBarType)) { + getOperator(value); + } + getFilterProperties.value = formatter.fromView( + getFilterProperties.value as string, + (getFilterProperties.column as IColumnBase).parseFn, + getFilterProperties.column.type + ); + } else { + getFilterProperties.value = new Date(getFilterProperties.value as string); + } + if (isNullOrUndefined(getFilterProperties.value)) { + getFilterProperties.filterStatusMsg = localization?.getConstant('invalidFilterMessage'); + } + break; + case 'string': + getFilterProperties.caseSensitive = false; + if (getFilterProperties.column.filter.operator) { + getFilterProperties.operator = getFilterProperties.column.filter.operator; + } else { + if (value.indexOf('*') !== -1 || value.indexOf('?') !== -1 || value.indexOf('%3f') !== -1) { + getFilterProperties.operator = filterOperators.wildCard; + } else if (value.indexOf('%') !== -1) { + getFilterProperties.operator = filterOperators.like; + } else { + getFilterProperties.operator = filterOperators.startsWith; + } + } + break; + case 'boolean': + if (value.toLowerCase() === 'true' || value === '1') { + getFilterProperties.value = true; + } else if (value.toLowerCase() === 'false' || value === '0') { + getFilterProperties.value = false; + } else if ((getFilterProperties.value as string).length) { + getFilterProperties.filterStatusMsg = localization?.getConstant('invalidFilterMessage'); + } + getFilterProperties.operator = filterOperators.equal; + break; + default: + if (getFilterProperties.column.filter.operator) { + getFilterProperties.operator = getFilterProperties.column.filter.operator; + } else { + getFilterProperties.operator = filterOperators.equal; + } + } + }; + + const getOperator: (value: string) => void = (value: string): void => { + const singleOp: string = value.charAt(0); + const multipleOp: string = value.slice(0, 2); + const operators: Object = extend( + { '=': filterOperators.equal, '!': filterOperators.notEqual }, DataUtil.operatorSymbols); + // eslint-disable-next-line no-prototype-builtins + if (operators.hasOwnProperty(singleOp) || operators.hasOwnProperty(multipleOp)) { + getFilterProperties.operator = operators[`${singleOp}`]; + getFilterProperties.value = value.substring(1); + if (!getFilterProperties.operator) { + getFilterProperties.operator = operators[`${multipleOp}`]; + getFilterProperties.value = value.substring(2); + } + } + if (getFilterProperties.operator === filterOperators.lessThan || getFilterProperties.operator === filterOperators.greaterThan) { + if ((getFilterProperties.value as string).charAt(0) === '=') { + getFilterProperties.operator = getFilterProperties.operator + 'orequal'; + getFilterProperties.value = (getFilterProperties.value as string).substring(1); + } + } + }; + + const applyColumnFormat: (filterValue: string | number | Date | boolean) => void = ( + filterValue: string | number | Date | boolean): void => { + const getFlvalue: Date | number | string = (getFilterProperties.column.type === 'date' || getFilterProperties.column.type === 'datetime' || getFilterProperties.column.type === 'dateonly') ? + filterValue === '' ? new Date(null) : new Date(filterValue as string) : parseFloat(filterValue as string); + if ((getFilterProperties.column.type === 'date' || getFilterProperties.column.type === 'datetime' || getFilterProperties.column.type === 'dateonly') && filterValue && + Array.isArray(getFilterProperties.value) && (filterValue as string).split(',').length > 1) { + getFilterProperties.values[getFilterProperties.column.field] = (((filterValue as string)).split(',')).map((val: string) => { + if (val === '') { + val = null; + } + return setFormatForFlColumn(new Date(val), getFilterProperties.column); + }); + } else { + getFilterProperties.values[getFilterProperties.column.field] = setFormatForFlColumn(getFlvalue, getFilterProperties.column); + } + }; + + const setFormatForFlColumn: (value: Date | number, column: ColumnProps) => string = ( + value: Date | number, column: ColumnProps): string => { + return formatter.toView(value, (column as IColumnBase).formatFn)?.toString(); + }; + + + /** + * Filters grid row by column name with the given options. + * + * @param {string} fieldName - Defines the field name of the column. + * @param {string} operator - Defines the operator to filter records. + * @param {string | number | Date | boolean| number[]| string[]| Date[]| boolean[]} filterValue - Defines the value used to filter records. + * @param {string} predicate - Defines the relationship between one filter query and another by using AND or OR predicate. + * @param {boolean} caseSensitive - If match case is set to true, the grid filters the records with exact match. if false, it filters case + * insensitive records (uppercase and lowercase letters treated the same). + * @param {boolean} ignoreAccent - If ignoreAccent set to true, + * then filter ignores the diacritic characters or accents while filtering. + * + * @returns {void} + */ + const filterByColumn: (fieldName: string, operator: string, + filterValue: string | number | Date | boolean| number[]| string[]| Date[]| boolean[], + predicate: string, caseSensitive: boolean, ignoreAccent: boolean + ) => void = (fieldName: string, + operator: string, filterValue: string | number | Date | boolean| number[]| string[]| Date[]| boolean[], + predicate: string, caseSensitive: boolean, ignoreAccent: boolean): void => { + getFilterProperties.column = gridRef.current.getColumns().find((col: ColumnProps) => col.field === fieldName); + let filterCell: HTMLInputElement; + if (operator === 'like' && filterValue && (filterValue as string).indexOf('%') === -1) { + filterValue = '%' + filterValue + '%'; + } + if (!getFilterProperties.column || !getFilterProperties.column.allowFilter || gridRef.current?.filterSettings?.enabled === false) { + return; + } + if (gridRef.current.filterSettings?.type === 'FilterBar') { + filterCell = gridRef.current.headerPanelRef.querySelector('[id=\'' + getFilterProperties.column.field + '_filterBarcell\']') as HTMLInputElement; + } + getFilterProperties.predicate = predicate ? predicate : Array.isArray(filterValue) ? 'or' : 'and'; + getFilterProperties.value = filterValue as string | number | Date | boolean; + getFilterProperties.caseSensitive = caseSensitive || false; + getFilterProperties.ignoreAccent = getFilterProperties.ignoreAccent = !isNullOrUndefined(ignoreAccent) ? + ignoreAccent : gridRef.current.filterSettings?.ignoreAccent; + getFilterProperties.fieldName = fieldName; + getFilterProperties.operator = operator; + filterValue = !isNullOrUndefined(filterValue) ? filterValue.toString() : filterValue; + if (filterValue === '') { + filterValue = null; + } + if (getFilterProperties.column.type === 'number' || getFilterProperties.column.type === 'date') { + getFilterProperties.caseSensitive = true; + } + if (filterCell && gridRef.current.filterSettings?.type === 'FilterBar') { + if ((filterValue && (filterValue as string).length < 1) || (!getFilterProperties.filterByMethod && + checkForSkipInput(getFilterProperties.column, (filterValue as string)))) { + getFilterProperties.filterStatusMsg = (filterValue && (filterValue as string).length < 1) ? '' : localization?.getConstant('invalidFilterMessage'); + updateFilterMsg(); + return; + } + if (filterCell.value !== filterValue) { + filterCell.value = (filterValue as string); + } + } + if (!isNullOrUndefined(getFilterProperties.column.format)) { + applyColumnFormat((filterValue as string)); + if (getFilterProperties.initialLoad && gridRef.current.filterSettings?.type === 'FilterBar') { + filterCell.value = getFilterProperties.values[getFilterProperties.column.field]; + } + } else { + getFilterProperties.values[getFilterProperties.column.field] = filterValue; + } + const predObj: FilterPredicates = { + field: getFilterProperties.fieldName, + predicate: predicate, + caseSensitive: caseSensitive, + ignoreAccent: ignoreAccent, + operator: getFilterProperties.operator, + value: getFilterProperties.value, + type: getFilterProperties.column.type + }; + const filterColumn: FilterPredicates[] = gridRef.current.filterSettings?.columns.filter((fColumn: FilterPredicates) => { + return (fColumn.field === getFilterProperties.fieldName); + }); + if (filterColumn?.length > 1 && !isNullOrUndefined(getFilterProperties.actualPredicate[getFilterProperties.fieldName])) { + getFilterProperties.actualPredicate[getFilterProperties.fieldName].push(predObj); + } else { + getFilterProperties.actualPredicate[getFilterProperties.fieldName] = [predObj]; + } + if (checkAlreadyColFiltered(getFilterProperties.column.field)) { + return; + } + updateModel(); + }; + + const checkForSkipInput: (column: ColumnProps, value: string) => boolean = useCallback( + (column: ColumnProps, value: string): boolean => { + let isSkip: boolean; + if (column.type === 'number') { + if (DataUtil.operatorSymbols[`${value}`] || getFilterProperties.skipNumberInput.indexOf(value) > -1) { + isSkip = true; + } + } else if (column.type === 'string') { + for (const val of value) { + if (getFilterProperties.skipStringInput.indexOf(val) > -1) { + isSkip = true; + } + } + } + return isSkip; + }, [] + ); + + const getFilteredColsIndexByField: (col: ColumnProps) => number = useCallback((col: ColumnProps): number => { + const cols: FilterPredicates[] = gridRef.current.filterSettings?.columns; + for (let i: number = 0, len: number = cols.length; i < len; i++) { + if (cols[parseInt(i.toString(), 10)].uid === col.uid) { + return i; + } + } + return -1; + }, []); + + /** + * To update filterSettings when applying filter. + * + * @returns {void} + */ + const updateModel: () => void = async(): Promise => { + const column: ColumnProps = gridRef.current.getColumns().find((col: ColumnProps) => col.field === getFilterProperties.fieldName); + const filterCol: FilterPredicates[] = extend([], gridRef.current.filterSettings?.columns) as FilterPredicates[]; + const arrayVal: (string | number | Date | boolean)[] = Array.isArray(getFilterProperties.value) && + getFilterProperties.value.length ? getFilterProperties.value : [getFilterProperties.value]; + let currentFilterObject: FilterPredicates; + const filterObjIndex: number = getFilteredColsIndexByField(getFilterProperties.column); + const prevFilterObject: FilterPredicates = filterCol[parseInt(filterObjIndex.toString(), 10)]; + const moduleName : string = (gridRef.current.dataSource as DataManager).adaptor && (<{ getModuleName?: Function }>( + gridRef.current.dataSource as DataManager).adaptor).getModuleName ? (<{ getModuleName?: Function }>( + gridRef.current.dataSource as DataManager).adaptor).getModuleName() : undefined; + for (let i: number = 0, len: number = arrayVal.length; i < len; i++) { + const isMenuNotEqual: boolean = getFilterProperties.operator === 'notequal'; + currentFilterObject = { + field: getFilterProperties.fieldName, uid: column.uid, isForeignKey: false, operator: getFilterProperties.operator, + value: arrayVal[parseInt(i.toString(), 10)], predicate: getFilterProperties.predicate, + caseSensitive: getFilterProperties.caseSensitive, ignoreAccent: getFilterProperties.ignoreAccent, + actualFilterValue: {}, actualOperator: {} + }; + const index: number = getFilteredColsIndexByField(column); + if (index > -1 && !Array.isArray(getFilterProperties.value)) { + filterCol[parseInt(index.toString(), 10)] = currentFilterObject; + } else { + filterCol.push(currentFilterObject); + } + if ((prevFilterObject && (isNullOrUndefined(prevFilterObject.value) + || prevFilterObject.value === '') && (prevFilterObject.operator === 'equal' + || prevFilterObject.operator === 'notequal')) && (moduleName !== 'ODataAdaptor' && moduleName !== 'ODataV4Adaptor')) { + handleExistingFilterCleanup(getFilterProperties.fieldName, filterCol); + } + if (isNullOrUndefined(getFilterProperties.value) && (getFilterProperties.operator === 'equal' || + getFilterProperties.operator === 'notequal') && (moduleName !== 'ODataAdaptor' && moduleName !== 'ODataV4Adaptor')) { + handleExistingFilterCleanup(getFilterProperties.fieldName, filterCol); + if (column.type === 'string') { + filterCol.push({ + field: getFilterProperties.fieldName, ignoreAccent: getFilterProperties.ignoreAccent, + caseSensitive: getFilterProperties.caseSensitive, operator: getFilterProperties.operator, + predicate: isMenuNotEqual ? 'and' : 'or', value: '', uid: column.uid + }); + } + filterCol.push({ + field: getFilterProperties.fieldName, ignoreAccent: getFilterProperties.ignoreAccent, + caseSensitive: getFilterProperties.caseSensitive, operator: getFilterProperties.operator, + predicate: isMenuNotEqual ? 'and' : 'or', value: undefined, uid: column.uid + }); + filterCol.push({ + field: getFilterProperties.fieldName, ignoreAccent: getFilterProperties.ignoreAccent, + caseSensitive: getFilterProperties.caseSensitive, operator: getFilterProperties.operator, + predicate: isMenuNotEqual ? 'and' : 'or', value: null, uid: column.uid + }); + } + } + + const args: FilterEvent = { cancel: false, currentFilterObject: currentFilterObject, + currentFilterColumn: getFilterProperties.column, columns: filterCol, action: 'filtering', requestType: 'filtering' }; + if (getFilterProperties.contentRefresh) { + args.type = 'filtering'; + const confirmResult: boolean = await gridRef.current?.editModule?.checkUnsavedChanges?.(); + if (!isNullOrUndefined(confirmResult) && !confirmResult) { + return; + } + gridRef.current.onFilterStart?.(args); + if (args.cancel) { + return; + } + } + gridRef.current.filterSettings.columns = filterCol; + if (getFilterProperties.contentRefresh) { + setFilterSettings((prevSettings: FilterSettings) => { + return { ...prevSettings, columns: gridRef.current.filterSettings?.columns || [] }; + }); + args.type = 'actionComplete'; + setGridAction(args); + } + refreshFilterSettings(); + updateFilterMsg(); + }; + + const handleExistingFilterCleanup: (field: string, filterCol: FilterPredicates[]) => void = ( + field: string, filterCol: FilterPredicates[]): void => { + for (let i: number = 0; i < filterCol.length; i++) { + if (filterCol[`${i}`].field === field && (filterCol[`${i}`].operator === 'equal' || filterCol[`${i}`].operator === 'notequal') + && isNullOrUndefined(filterCol[`${i}`].value)) { + filterCol.splice(i, 1); + i = i - 1; + } + } + }; + + const refreshFilterSettings: () => void = (): void => { + if (gridRef.current.filterSettings?.type === 'FilterBar') { + const filterColumn: FilterPredicates[] = gridRef.current.filterSettings?.columns; + for (let i: number = 0; i < filterColumn?.length; i++) { + getFilterProperties.column = grabColumnByUidFromAllCols( + filterColumn[parseInt(i.toString(), 10)].uid, filterColumn[parseInt(i.toString(), 10)].field); + let filterValue: string | number | Date | boolean | (string | number | boolean | Date)[] = + filterColumn[parseInt(i.toString(), 10)].value; + filterValue = !isNullOrUndefined(filterValue) && filterValue.toString(); + if (!isNullOrUndefined(getFilterProperties.column.format) && !isNullOrUndefined(getFilterProperties.column.type)) { + applyColumnFormat(filterValue); + } else { + const key: string = filterColumn[parseInt(i.toString(), 10)].field; + getFilterProperties.values[`${key}`] = filterColumn[parseInt(i.toString(), 10)].value; + } + } + } + }; + + const updateFilterMsg: () => void = (): void => { + if (gridRef.current.filterSettings?.type === 'FilterBar') { + const gObj: GridRef = gridRef.current; + let getFormatFlValue: string; + const columns: FilterPredicates[] = gObj.filterSettings?.columns; + let column: ColumnProps; + if (columns.length > 0 && getFilterProperties.filterStatusMsg !== localization?.getConstant('invalidFilterMessage')) { + getFilterProperties.filterStatusMsg = ''; + for (let index: number = 0; index < columns.length; index++) { + column = grabColumnByUidFromAllCols( + columns[parseInt(index.toString(), 10)].uid, columns[parseInt(index.toString(), 10)].field); + if (index) { + getFilterProperties.filterStatusMsg += ' && '; + } + if (!isNullOrUndefined(column.format) && !isNullOrUndefined(column.type)) { + const flValue: Date | number = (column.type === 'date' || column.type === 'datetime' || column.type === 'dateonly') ? + formatter.fromView(getFilterProperties.values[column.field], (column as IColumnBase).parseFn, (column.type === 'dateonly' ? 'date' : column.type)) : getFilterProperties.values[column.field]; + if (!(column.type === 'date' || column.type === 'datetime' || column.type === 'dateonly')) { + getFormatFlValue = formatter.toView(flValue, (column as IColumnBase).parseFn).toString(); + } else { + getFormatFlValue = setFormatForFlColumn(flValue, column); + } + getFilterProperties.filterStatusMsg += column.headerText + ': ' + getFormatFlValue; + } else { + getFilterProperties.filterStatusMsg += column.headerText + ': ' + getFilterProperties.values[column.field]; + } + } + } + if (gridRef.current.pageSettings?.enabled) { + gridRef.current.updatePagerMessage(getFilterProperties.filterStatusMsg); + if (gridRef.current.height === '100%') { + gridRef.current.scrollModule.setPadding(); + } + } + + getFilterProperties.filterStatusMsg = ''; + } + + }; + + const checkAlreadyColFiltered: (field: string) => boolean = (field: string): boolean => { + const columns: FilterPredicates[] = gridRef.current.filterSettings?.columns; + for (const col of columns) { + if (col.field === field && col.value === getFilterProperties.value && + col.operator === getFilterProperties.operator && col.predicate === getFilterProperties.predicate) { + return true; + } + } + return false; + }; + + const getColumnByUid: (uid: string) => ColumnProps = (uid: string): ColumnProps => { + return iterateArrayOrObject(gridRef.current.getColumns(), (item: ColumnProps) => { + if (item.uid === uid) { + return item; + } + return undefined; + })[0]; + }; + + /** + * Clears all the filtered rows of the Grid. + * + * @param {string[]} fields - Defines the Fields + * @returns {void} + */ + const clearFilter: (fields: string[]) => void = async(fields: string[]): Promise => { + const cols: FilterPredicates[] = getActualPropFromColl(gridRef.current.filterSettings?.columns); + if (!isNullOrUndefined(fields)) { + getFilterProperties.refresh = false; + fields.forEach((field: string) => { removeFilteredColsByField(field); }); + const confirmResult: boolean = await gridRef.current?.editModule?.checkUnsavedChanges?.(); + if (!isNullOrUndefined(confirmResult) && !confirmResult) { + return; + } + gridRef.current?.onRefreshStart?.({ + requestType: 'refresh', name: 'onActionBegin' + }); + if (gridRef.current.filterSettings?.columns.length === 0) { + setFilterSettings((prevSettings: FilterSettings) => { + return { ...prevSettings, columns: [] }; + }); + setGridAction({ + requestType: 'refresh', name: 'onActionComplete' + }); + } else { + setFilterSettings((prevSettings: FilterSettings) => { + return { ...prevSettings, columns: gridRef.current.filterSettings?.columns || [] }; + }); + setGridAction({ + requestType: 'refresh', name: 'onActionComplete' + }); + } + getFilterProperties.refresh = true; + return; + } + for (let i: number = 0; i < cols.length; i++) { + cols[parseInt(i.toString(), 10)].uid = cols[parseInt(i.toString(), 10)].uid; + } + const colUid: string[] = cols.map((f: ColumnProps) => f.uid); + const filteredcols: string[] = colUid.filter((item: string, pos: number) => colUid.indexOf(item) === pos); + getFilterProperties.refresh = false; + for (let i: number = 0, len: number = filteredcols.length; i < len; i++) { + removeFilteredColsByField(getColumnByUid(filteredcols[parseInt(i.toString(), 10)]).field); + } + getFilterProperties.refresh = true; + if (filteredcols.length) { + const confirmResult: boolean = await gridRef.current?.editModule?.checkUnsavedChanges?.(); + if (!isNullOrUndefined(confirmResult) && !confirmResult) { + return; + } + gridRef.current?.onRefreshStart?.({ + requestType: 'refresh', name: 'onActionBegin' + }); + if (gridRef.current.filterSettings?.columns.length === 0) { + setFilterSettings((prevSettings: FilterSettings) => { + return { ...prevSettings, columns: [] }; + }); + setGridAction({ + requestType: 'refresh', name: 'onActionComplete' + }); + } else { + setFilterSettings((prevSettings: FilterSettings) => { + return { ...prevSettings, columns: gridRef.current.filterSettings?.columns || [] }; + }); + setGridAction({ + requestType: 'refresh', name: 'onActionComplete' + }); + } + } + getFilterProperties.filterStatusMsg = ''; + refreshFilterSettings(); + updateFilterMsg(); + }; + + return { + filterByColumn, + clearFilter, + removeFilteredColsByField, + keyUpHandler, + mouseDownHandler, + filterSettings, + setFilterSettings + }; +}; diff --git a/components/grids/src/grid/hooks/useFocusStrategy.ts b/components/grids/src/grid/hooks/useFocusStrategy.ts new file mode 100644 index 0000000..95e0a83 --- /dev/null +++ b/components/grids/src/grid/hooks/useFocusStrategy.ts @@ -0,0 +1,1788 @@ +import { + useCallback, useRef, useState, useEffect, RefObject, + MouseEvent, KeyboardEvent, + useMemo +} from 'react'; +import { + CellType, + IRow, + ICell +} from '../types'; +import { GridRef } from '../types/grid.interfaces'; +import { ColumnProps } from '../types/column.interfaces'; +import { isNullOrUndefined } from '@syncfusion/react-base'; +import { IFocusMatrix, FocusStrategyCallbacks, FocusStrategyResult, FocusedCellInfo, CellFocusEvent, Matrix, SwapInfo} from '../types/focus.interfaces'; +// CSS class constants +const CSS_FOCUSED: string = 'sf-focused'; +const CSS_FOCUS: string = 'sf-focus'; + +/** + * IFocusMatrix class for tracking focusable cells + * + * @returns {IFocusMatrix} An object with methods for managing a matrix of focusable cells + * @private + */ +export const createMatrix: () => IFocusMatrix = (): IFocusMatrix => { + const matrix: number[][] = []; + let current: number[] = []; + let columns: number = 0; + let rows: number = 0; + + /** + * Sets a cell's focusable state in the matrix + * + * @param {number} rowIndex - Row index of the cell + * @param {number} columnIndex - Column index of the cell + * @param {boolean} [allow] - Whether the cell is focusable + * @returns {void} + */ + const set: (rowIndex: number, columnIndex: number, allow?: boolean) => void = + (rowIndex: number, columnIndex: number, allow?: boolean): void => { + // Ensure indices are within bounds + rowIndex = Math.max(0, Math.min(rowIndex, rows)); + columnIndex = Math.max(0, Math.min(columnIndex, columns)); + + // Ensure the row array exists + matrix[rowIndex as number] = matrix[rowIndex as number] || []; + + // Set the cell value + matrix[rowIndex as number][columnIndex as number] = allow ? 1 : 0; + }; + + /** + * Checks if a cell value is invalid (0 or undefined) + * + * @param {number} value - Cell value to check + * @returns {boolean} Whether the value is invalid + */ + const inValid: (value: number) => boolean = (value: number): boolean => { + return value === 0 || value === undefined; + }; + + /** + * Finds the first valid cell in a vector + * + * @param {number[]} vector - Array of cell values + * @param {number} index - Starting index + * @param {number[]} navigator - Navigation direction + * @param {boolean} [moveTo] - Whether to move to the first cell + * @param {string} [action] - Navigation action + * @returns {number|null} Index of the first valid cell or null if none found + */ + const first: ( + vector: number[], + index: number, + navigator: number[], + moveTo?: boolean, + action?: string + ) => number = ( + vector: number[], + index: number, + navigator: number[], + moveTo?: boolean, + action?: string + ): number => { + // Check if we're out of bounds or if there are no valid cells, visible state change helping codes + if (((index < 0 || index === vector.length) && inValid(vector[index as number]) + && (action !== 'upArrow' && action !== 'downArrow')) || !vector.some((v: number) => v === 1)) { + return null; + } + + // If current cell is valid, return its index + if (!inValid(vector[index as number])) { + return index; + } + + // Otherwise, recursively find the next valid cell + const nextIndex: number = (['upArrow', 'downArrow', 'shiftUp', 'shiftDown', 'enter', 'shiftEnter'].indexOf(action) !== -1) ? + (moveTo ? 0 : ++index) : index + navigator[1]; + + return first(vector, nextIndex, navigator, false, action); + }; + + /** + * Finds the next or previous valid cell index in the matrix + * + * @param {number[]} checkCellIndex - Current cell index + * @param {boolean} next - Whether to find next (true) or previous (false) cell + * @returns {number[]} Next or previous valid cell index + */ + const findCellIndex: (checkCellIndex: number[], next: boolean) => number[] = (checkCellIndex: number[], next: boolean): number[] => { + const cellIndex: number[] = [...checkCellIndex]; + let currentCellIndexPass: boolean = false; + + if (next) { + // Find next valid cell + for (let i: number = cellIndex[0]; i < matrix.length; i++) { + const rowCell: number[] = matrix[i as number]; + + for (let j: number = 0; rowCell && j < rowCell.length; j++) { + if (currentCellIndexPass && matrix[i as number][j as number] === 1) { + return [i, j]; + } + if (!currentCellIndexPass && i === cellIndex[0] && j === cellIndex[1]) { + currentCellIndexPass = true; + } + } + } + } else { + // Find previous valid cell + for (let i: number = cellIndex[0]; i >= 0; i--) { + const rowCell: number[] = matrix[i as number]; + + for (let j: number = rowCell.length - 1; rowCell && j >= 0; j--) { + if (currentCellIndexPass && matrix[i as number][j as number] === 1) { + return [i, j]; + } + if (!currentCellIndexPass && i === cellIndex[0] && j === cellIndex[1]) { + currentCellIndexPass = true; + } + } + } + } + + return cellIndex; + }; + + /** + * Gets the next valid cell based on navigation parameters + * + * @param {number} rowIndex - Current row index + * @param {number} columnIndex - Current column index + * @param {number[]} navigator - Navigation direction + * @param {string} [action] - Navigation action + * @param {Function} [validator] - Function to validate cell + * @param {Object} [active] - Active matrix info + * @returns {number[]} Next valid cell coordinates + */ + const get: ( + rowIndex: number, + columnIndex: number, + navigator: number[], + action?: string, + validator?: Function, + active?: Object + ) => number[] = ( + rowIndex: number, + columnIndex: number, + navigator: number[], + action?: string, + validator?: Function, + active?: Object + ): number[] => { + const tmp: number = columnIndex; + + // Check if we're trying to navigate before the first row + if (rowIndex + navigator[0] < 0) { + return [rowIndex, columnIndex]; + } + + // Calculate new row index within bounds + rowIndex = Math.max(0, Math.min(rowIndex + navigator[0], rows)); + + // Check if row exists + if (!matrix[rowIndex as number]) { + return null; + } + + // Calculate new column index within bounds + columnIndex = Math.max(0, Math.min(columnIndex + navigator[1], matrix[rowIndex as number].length - 1)); + + // Check if we're trying to navigate past the last column + if (tmp + navigator[1] > matrix[rowIndex as number].length - 1 && validator(rowIndex, columnIndex, action)) { + return [rowIndex, tmp]; + } + + // Find first valid cell in the row + const firstIndex: number = first(matrix[rowIndex as number], columnIndex, navigator, true, action); + columnIndex = firstIndex === null ? tmp : firstIndex; + const val: number = matrix[rowIndex as number]?.[columnIndex as number]; + + // Special handling for down arrow or enter at the last row + if (rowIndex === rows && (action === 'downArrow' || action === 'enter')) { + navigator[0] = -1; + } + + // Recursively find valid cell if current is invalid + return inValid(val) || !validator(rowIndex, columnIndex, action) ? + get(rowIndex, tmp, navigator, action, validator, + active) : [rowIndex, columnIndex]; + }; + + /** + * Selects a cell in the matrix + * + * @param {number} rowIndex - Row index to select + * @param {number} columnIndex - Column index to select + * @returns {void} + */ + const select: (rowIndex: number, columnIndex: number) => void = (rowIndex: number, columnIndex: number): void => { + rowIndex = Math.max(0, Math.min(rowIndex, rows)); + columnIndex = Math.max(0, Math.min(columnIndex, matrix[rowIndex as number]?.length - 1 || 0)); + + // Create a new array instead of modifying the existing one + current = [rowIndex, columnIndex]; + + if (matrix?.[rowIndex as number] && !matrix?.[rowIndex as number]?.[columnIndex as number]) { + matrix[rowIndex as number][columnIndex as number] = 1; + } + }; + + /** + * Generates a matrix from row data + * + * @param {Array>} rowsData - Array of row data + * @param {Function} selector - Function to determine if a cell is focusable + * @param {boolean} [isRowTemplate] - Whether the row is a template + * @returns {Array>} The generated matrix + */ + const generate: ( + rowsData: IRow[], + selector: Function, + isRowTemplate?: boolean + ) => number[][] = ( + rowsData: IRow[], + selector: Function, + isRowTemplate?: boolean + ): number[][] => { + // Update the rows count BEFORE generating the matrix + rows = rowsData.length - 1; + + // Clear existing matrix + matrix.length = 0; + + for (let i: number = 0; i < rowsData.length && Array.isArray(rowsData[i as number]?.cells); i++) { + const cells: ICell[] = rowsData[i as number]?.cells?.filter((c: ICell) => c.isSpanned !== true); + + // Update columns count + columns = Math.max(cells.length - 1, columns || 0); + + let incrementNumber: number = 0; + for (let j: number = 0; j < cells.length; j++) { + incrementNumber++; + + // Set cell focusability + set(i, j, rowsData[i as number].visible === false ? + false : selector(rowsData[i as number], cells[j as number], isRowTemplate)); + } + + columns = Math.max(incrementNumber - 1, columns || 0); + } + + return matrix; + }; + + return { + matrix, + current, + get columns(): number { return columns; }, + set columns(value: number) { columns = value; }, + get rows(): number { return rows; }, + set rows(value: number) { rows = value; }, + set, + get, + select, + generate, + inValid, + first, + findCellIndex + }; +}; + +/** + * Custom hook for managing focus strategy in grid + * Implements matrix-based navigation similar to the original class implementation + * + * @private + * @param {number} headerRowCount - Number of header rows + * @param {number} contentRowCount - Number of content rows + * @param {number} aggregateRowCount - Number of aggregate rows + * @param {ColumnProps} columns - columns state + * @param {RefObject} gridRef - Reference to the grid + * @param {FocusStrategyCallbacks} callbacks - Optional callbacks for focus events + * @returns {FocusStrategyResult} Focus strategy methods and state + */ +export const useFocusStrategy: ( + headerRowCount: number, + contentRowCount: number, + aggregateRowCount: number, + columns: ColumnProps[], + gridRef: RefObject, + callbacks?: FocusStrategyCallbacks +) => FocusStrategyResult = ( + headerRowCount: number, + contentRowCount: number, + aggregateRowCount: number, + columns: ColumnProps[], + gridRef: RefObject, + callbacks?: FocusStrategyCallbacks +) => { + // Create content, header, and aggregate matrices + const contentMatrix: RefObject = useRef(createMatrix()); + const headerMatrix: RefObject = useRef(createMatrix()); + const aggregateMatrix: RefObject = useRef(createMatrix()); + + // State for tracking focused cell - single source of truth + const focusedCell: RefObject = useRef({ + rowIndex: -1, + colIndex: -1, + isHeader: false, + skipAction: false, + outline: true + }); + + // State for tracking grid focus + const [isGridFocused, setIsGridFocused] = useState(false); + const focusByClick: boolean = useRef(false).current; + + // Ref for swap info + const swapInfo: RefObject = useRef({ + swap: false, + toHeader: false, + toMatrix: 'content' + }); + + // Ref for active matrix + const activeMatrix: RefObject = useRef('content'); + + // Ref for previous indexes + const prevIndexes: RefObject<{ rowIndex?: number, cellIndex?: number }> = useRef<{ rowIndex?: number, cellIndex?: number }>({}); + + // Key action mappings + const keyActions: RefObject<{ + [key: string]: [number, number] + }> = useRef({ + 'rightArrow': [0, 1], + 'tab': [0, 1], + 'leftArrow': [0, -1], + 'shiftTab': [0, -1], + 'upArrow': [-1, 0], + 'downArrow': [1, 0], + 'shiftUp': [-1, 0], + 'shiftDown': [1, 0], + 'shiftRight': [0, 1], + 'shiftLeft': [0, -1], + 'enter': [1, 0], + 'shiftEnter': [-1, 0] + }); + + // Key indexes by action + const indexesByKey: (action: string) => number[] = useCallback((action: string): number[] => { + const matrix: IFocusMatrix = getActiveMatrix(); + const opt: { [key: string]: number[] } = { + 'home': [matrix.current[0], -1, 0, 1], + 'end': [matrix.current[0], matrix.columns + 1, 0, -1], + 'ctrlHome': [0, -1, 0, 1], + 'ctrlEnd': [matrix.rows, matrix.columns + 1, 0, -1] + }; + return opt[action as string] || null; + }, []); + + /** + * Get the active matrix + * + * @returns {IFocusMatrix} The active matrix + */ + const getActiveMatrix: () => IFocusMatrix = useCallback(() => { + switch (activeMatrix.current) { + case 'content': return contentMatrix.current; + case 'aggregate': return aggregateMatrix.current; + case 'header': + default: return headerMatrix.current; + } + }, []); + + /** + * Get the aggregate matrix + * + * @returns {IFocusMatrix} The aggregate matrix + */ + const getAggregateMatrix: () => IFocusMatrix = useCallback(() => { + return aggregateMatrix.current; + }, []); + + /** + * Set the active matrix + * + * @param {(Matrix)} matrixType - The matrix type to set as active + * @returns {void} + */ + const setActiveMatrix: (matrixType: Matrix) => void = useCallback((matrixType: Matrix) => { + activeMatrix.current = matrixType; + }, []); + + const firstFocusableHeaderCellIndex: number[] = useMemo(() => { + const matrix: IFocusMatrix = headerMatrix.current; + return matrix.matrix?.[0]?.[0] === 1 ? [0, 0] : matrix.findCellIndex([0, 0], true); + }, [activeMatrix.current, headerMatrix.current, columns, isGridFocused]); + + const lastFocusableHeaderCellIndex: number[] = useMemo(() => { + const matrix: IFocusMatrix = headerMatrix.current; + const lastFocusableHeaderCellIndex: number[] = (matrix.matrix?.[matrix.rows]?.[matrix.columns] === 1 ? + [matrix.rows, matrix.columns] : + matrix.findCellIndex([matrix.rows, matrix.columns], matrix.matrix?.[matrix.rows]?.[matrix.columns] !== 0)); + return lastFocusableHeaderCellIndex; + }, [activeMatrix.current, headerMatrix.current, columns, isGridFocused]); + + const firstFocusableContentCellIndex: number[] = useMemo(() => { + const matrix: IFocusMatrix = contentMatrix.current; + return matrix.matrix?.[0]?.[0] === 1 ? [0, 0] : matrix.findCellIndex([0, 0], true); + }, [activeMatrix.current, contentMatrix.current, columns, isGridFocused, gridRef.current?.contentSectionRef?.children?.length]); + + const lastFocusableContentCellIndex: number[] = useMemo(() => { + const matrix: IFocusMatrix = contentMatrix.current; + return matrix.matrix?.[matrix.rows]?.[matrix.columns] === 1 ? [matrix.rows, matrix.columns] : + matrix.findCellIndex([matrix.rows, matrix.columns], matrix.matrix?.[matrix.rows]?.[matrix.columns] !== 0); + }, [activeMatrix.current, contentMatrix.current, columns, isGridFocused, gridRef.current?.contentSectionRef?.children?.length]); + + const firstFocusableAggregateCellIndex: number[] = useMemo(() => { + const matrix: IFocusMatrix = aggregateMatrix.current; + return matrix.matrix?.[0]?.[0] === 1 ? [0, 0] : matrix.findCellIndex([0, 0], true); + }, [activeMatrix.current, aggregateMatrix.current, columns, isGridFocused, aggregateRowCount]); + + const lastFocusableAggregateCellIndex: number[] = useMemo(() => { + const matrix: IFocusMatrix = aggregateMatrix.current; + const lastFocusableAggregateCellIndex: number[] = (matrix.matrix?.[matrix.rows]?.[matrix.columns] === 1 ? + [matrix.rows, matrix.columns] : + matrix.findCellIndex([matrix.rows, matrix.columns], matrix.matrix?.[matrix.rows]?.[matrix.columns] !== 0)); + return lastFocusableAggregateCellIndex; + }, [activeMatrix.current, aggregateMatrix.current, columns, isGridFocused, aggregateRowCount, + gridRef.current?.getFooterRowsObject?.()]); + + /** + * Validator function for cell navigation + * + * @returns {Function} Validator function + */ + const validator: () => Function = useCallback(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return (_rowIndex: number, _columnIndex: number, _action?: string) => { + return true; + }; + }, []); + + /** + * Get the current from action + * + * @param {string} action - Navigation action + * @param {number[]} navigator - Navigation direction + * @param {boolean} [isPresent] - Whether the action is present + * @param {KeyboardEvent} [_e] - Keyboard event + * @returns {number[]|null} Current cell coordinates + */ + const getCurrentFromAction: ( + action: string, + navigator: number[], + isPresent?: boolean, + _e?: KeyboardEvent + ) => number[] = useCallback(( + action: string, + navigator: number[] = [0, 0], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _isPresent?: boolean, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _e?: KeyboardEvent + ): number[] => { + const matrix: IFocusMatrix = getActiveMatrix(); + + // Get current indexes based on action + const [rowIndex, cellIndex, rN, cN] = indexesByKey(action) || [...matrix.current, ...navigator]; + + // Handle special actions + if (action === 'ctrlHome') { + // First cell of first row + switch (activeMatrix.current) { + case 'content': return firstFocusableContentCellIndex; + case 'aggregate': return firstFocusableAggregateCellIndex; + case 'header': + default: return firstFocusableHeaderCellIndex; + } + } else if (action === 'ctrlEnd') { + // Last cell of last row + switch (activeMatrix.current) { + case 'content': return lastFocusableContentCellIndex; + case 'aggregate': return lastFocusableAggregateCellIndex; + case 'header': + default: return lastFocusableHeaderCellIndex; + } + } else if (action === 'home') { + // First cell of current row + let firstColIndex: number; + switch (activeMatrix.current) { + case 'content': firstColIndex = firstFocusableContentCellIndex[1]; break; + case 'aggregate': firstColIndex = firstFocusableAggregateCellIndex[1]; break; + case 'header': + default: firstColIndex = firstFocusableHeaderCellIndex[1]; break; + } + return [rowIndex, firstColIndex]; + } else if (action === 'end') { + // Last cell of current row + let lastColIndex: number; + switch (activeMatrix.current) { + case 'content': lastColIndex = lastFocusableContentCellIndex[1]; break; + case 'aggregate': lastColIndex = lastFocusableAggregateCellIndex[1]; break; + case 'header': + default: lastColIndex = lastFocusableHeaderCellIndex[1]; break; + } + return [rowIndex, lastColIndex]; + } + + // For tab/shift+tab navigation at boundaries + if (action === 'tab' && ((activeMatrix.current === 'content' && cellIndex >= lastFocusableContentCellIndex[1]) || + (activeMatrix.current === 'header' && cellIndex >= lastFocusableHeaderCellIndex[1]) || + (activeMatrix.current === 'aggregate' && cellIndex >= lastFocusableAggregateCellIndex[1]))) { + // At the end of a row, move to the first cell of the next row + if (rowIndex < matrix.rows) { + let firstColIndex: number; + switch (activeMatrix.current) { + case 'content': firstColIndex = firstFocusableContentCellIndex[1]; break; + case 'aggregate': firstColIndex = firstFocusableAggregateCellIndex[1]; break; + case 'header': + default: firstColIndex = firstFocusableHeaderCellIndex[1]; break; + } + return [rowIndex + 1, firstColIndex]; + } + } else if (action === 'shiftTab' && ((activeMatrix.current === 'content' && cellIndex <= firstFocusableContentCellIndex[1]) || + (activeMatrix.current === 'header' && cellIndex <= firstFocusableHeaderCellIndex[1]) || + (activeMatrix.current === 'aggregate' && cellIndex <= firstFocusableAggregateCellIndex[1]))) { + // At the beginning of a row, move to the last cell of the previous row + if (rowIndex > 0) { + let lastColIndex: number; + switch (activeMatrix.current) { + case 'content': lastColIndex = lastFocusableContentCellIndex[1]; break; + case 'aggregate': lastColIndex = lastFocusableAggregateCellIndex[1]; break; + case 'header': + default: lastColIndex = lastFocusableHeaderCellIndex[1]; break; + } + return [rowIndex - 1, lastColIndex]; + } + } + + // Get next valid cell + const current: number[] = matrix.get( + rowIndex, + cellIndex, + [rN, cN], + action, + validator(), + { matrix: matrix } + ); + + return current; + }, [getActiveMatrix, indexesByKey, validator, + firstFocusableContentCellIndex, + lastFocusableContentCellIndex, + firstFocusableHeaderCellIndex, + lastFocusableHeaderCellIndex, + firstFocusableAggregateCellIndex, + lastFocusableAggregateCellIndex, + activeMatrix.current]); + + /** + * Handle key press event + * + * @param {KeyboardEvent} e - Keyboard event + * @returns {boolean} Whether the key press was handled + */ + const onKeyPress: (e: KeyboardEvent) => boolean = useCallback((e: KeyboardEvent): boolean => { + const isMacLike: boolean = /(Mac)/i.test(navigator.platform); + let action: string = ''; + + // Convert key to action + switch (e.key) { + case 'ArrowRight': action = 'rightArrow'; break; + case 'ArrowLeft': action = 'leftArrow'; break; + case 'ArrowUp': action = e.shiftKey ? 'shiftUp' : 'upArrow'; break; + case 'ArrowDown': action = e.shiftKey ? 'shiftDown' : 'downArrow'; break; + case 'Tab': action = e.shiftKey ? 'shiftTab' : 'tab'; break; + case 'Enter': action = e.shiftKey ? 'shiftEnter' : 'enter'; break; + case 'Home': action = e.ctrlKey || (isMacLike && e.metaKey) ? 'ctrlHome' : 'home'; break; + case 'End': action = e.ctrlKey || (isMacLike && e.metaKey) ? 'ctrlEnd' : 'end'; break; + default: return true; // Not a navigation key + } + + // Handle Mac-specific key combinations + if (isMacLike && e.metaKey && ['downArrow', 'upArrow', 'leftArrow', 'rightArrow'].indexOf(action) !== -1) { + return true; + } + + // Get navigation vectors + const navigators: number[] = keyActions.current[action as string]; + + // Get current position from action + const matrix: IFocusMatrix = getActiveMatrix(); + + const current: number[] = getCurrentFromAction(action, navigators, action in keyActions.current, e); + + if (!current) { return true; } + + // Check if we're at the boundary of the current matrix + const isAtHeaderBottom: boolean = activeMatrix.current === 'header' && + current.toString() === headerMatrix.current.current.toString() && action === 'downArrow'; + const isAtContentTop: boolean = activeMatrix.current === 'content' && + current.toString() === contentMatrix.current.current.toString() && action === 'upArrow'; + const isAtContentBottom: boolean = activeMatrix.current === 'content' && + current.toString() === contentMatrix.current.current.toString() && action === 'downArrow'; + const isAtAggregateTop: boolean = activeMatrix.current === 'aggregate' && + current.toString() === aggregateMatrix.current.current.toString() && action === 'upArrow'; + const isAtAggregateBottom: boolean = activeMatrix.current === 'aggregate' && + current.toString() === aggregateMatrix.current.current.toString() && action === 'downArrow'; + const isAtHeaderRight: boolean = activeMatrix.current === 'header' && + current.toString() === headerMatrix.current.current.toString() && action === 'tab'; + const isAtContentLeft: boolean = activeMatrix.current === 'content' && + current.toString() === contentMatrix.current.current.toString() && action === 'shiftTab'; + const isAtContentRight: boolean = activeMatrix.current === 'content' && + current.toString() === contentMatrix.current.current.toString() && action === 'tab'; + const isAtAggregateLeft: boolean = activeMatrix.current === 'aggregate' && + current.toString() === aggregateMatrix.current.current.toString() && action === 'shiftTab'; + const isAtAggregateRight: boolean = activeMatrix.current === 'aggregate' && + current.toString() === aggregateMatrix.current.current.toString() && action === 'tab'; + + // Handle boundary navigation between header, content, and aggregate + if (isAtHeaderBottom || isAtHeaderRight) { + swapInfo.current = { swap: true, toMatrix: 'content' }; + return false; + } else if (isAtContentTop || isAtContentLeft) { + swapInfo.current = { swap: true, toMatrix: 'header' }; + return false; + } else if ((isAtContentBottom || isAtContentRight) && aggregateRowCount > 0) { + swapInfo.current = { swap: true, toMatrix: 'aggregate' }; + return false; + } else if (isAtAggregateTop || isAtAggregateLeft) { + swapInfo.current = { swap: true, toMatrix: 'content' }; + return false; + } else if (isAtAggregateBottom || isAtAggregateRight) { + // From aggregate, exit the grid (no more matrices below) + return false; + } + + // Update matrix selection - IMPORTANT: Create a new array to ensure React detects the change + matrix.select(current[0], current[1]); + + // This line is key for keyboard navigation to work properly + // We need to create a new array to ensure the reference changes + matrix.current = [...current]; + + return true; + }, [getCurrentFromAction, getActiveMatrix, activeMatrix.current, focusedCell.current]); + + /** + * Clear focus indicator without changing focus state + * Used when focus moves out of grid or during specific actions + * + * @returns {void} + */ + const clearIndicator: () => void = useCallback((): void => { + if (focusedCell.current.element) { + // Remove focus classes directly from DOM + focusedCell.current.element.classList.remove(CSS_FOCUSED, CSS_FOCUS); + focusedCell.current.elementToFocus.classList.remove(CSS_FOCUSED, CSS_FOCUS); + } + }, [focusedCell.current]); + + /** + * Remove focus from current cell - update state and DOM + * + * @returns {void} + */ + const removeFocus: () => void = useCallback((): void => { + if (focusedCell.current.element) { + // Remove focus classes directly from DOM + focusedCell.current.element.classList.remove(CSS_FOCUSED, CSS_FOCUS); + focusedCell.current.element.tabIndex = -1; + } + // Update state + focusedCell.current = { + rowIndex: -1, + colIndex: -1, + isHeader: false, + skipAction: false, + outline: true + }; + }, [focusedCell.current]); + + const removeFocusTabIndex: () => void = (): void => { + // Find the currently focused cell and remove focus + const currentFocusedCell: NodeListOf = gridRef.current?.element?. + querySelectorAll('[tabindex="0"]:not([data-role="page"], [data-role="page"] *, [role="toolbar"], [role="toolbar"] *, .sf-filterbar *, .sf-gridform *)'); + currentFocusedCell?.forEach((cell: HTMLElement) => { + cell.classList.remove(CSS_FOCUSED, CSS_FOCUS); + cell.tabIndex = -1; + }); + }; + + /** + * Add focus to a cell - update state and DOM + * + * @param {FocusedCellInfo} info - Cell info to focus + * @param {KeyboardEvent} [e] - Keyboard event + * @returns {void} + */ + const addFocus: (info: FocusedCellInfo, e?: KeyboardEvent | MouseEvent) => void = + useCallback((info: FocusedCellInfo, e?: KeyboardEvent | MouseEvent): void => { + removeFocusTabIndex(); + + const newInfo: FocusedCellInfo = { + ...info, + outline: info.outline !== false && !info.element?.classList?.contains?.('sf-filterbarcell'), // Default to true if not explicitly set to false + element: info.element, + elementToFocus: info.elementToFocus + }; + + // Update the focused cell state + focusedCell.current = newInfo; + + // Add focus classes directly to DOM + if (newInfo.outline) { + newInfo.element?.classList.add(CSS_FOCUSED); + } + newInfo.elementToFocus?.classList.add(CSS_FOCUS); + + // Set tabIndex - ensure only one element has tabIndex=0 + if (newInfo.element) { + newInfo.element.tabIndex = 0; + } + + // Focus the element using DOM API + requestAnimationFrame(() => { + const firstFocusableElement: HTMLElement = newInfo.elementToFocus?.querySelector( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const container: HTMLDivElement | null = gridRef.current?.contentScrollRef; + const element: HTMLElement | null = + (firstFocusableElement ?? newInfo.elementToFocus)?.closest?.('td.sf-rowcell, th.sf-headercell'); + + if (container && element) { + const containerRect: DOMRect = container.getBoundingClientRect(); + const elementRect: DOMRect = element.getBoundingClientRect(); + + // Scroll left if element is partially hidden on the left + if (elementRect.left < containerRect.left) { + container.scrollLeft = element.offsetLeft; + } + // Scroll right if element is partially hidden on the right + else if (elementRect.right > containerRect.right) { + container.scrollLeft = (element.offsetLeft + element.offsetWidth - container.clientWidth); + } + } + + if (firstFocusableElement) { + firstFocusableElement.focus(); + } else { + newInfo.elementToFocus?.focus(); + } + }); + + // Notify cell focused + const matrix: IFocusMatrix = getActiveMatrix(); + const args: CellFocusEvent = { + element: newInfo.elementToFocus, + parent: newInfo.element, + indexes: matrix.current, + byKey: e ? e.type === 'keydown' : !!e, + byClick: e ? e.type === 'click' : !e, + keyArgs: e, + isJump: swapInfo.current.swap, + container: { + isContent: activeMatrix.current === 'content', + isHeader: activeMatrix.current === 'header' + }, + outline: newInfo.outline, + swapInfo: swapInfo.current, + rowIndex: newInfo.rowIndex, + columnIndex: newInfo.colIndex, + column: gridRef.current.getColumns()[newInfo.colIndex], + rowData: gridRef.current.getRowsObject()[newInfo.rowIndex]?.data, + event: e + }; + + callbacks?.onCellFocus?.(args); + + // Update previous indexes + const [rowIndex, cellIndex]: number[] = matrix.current; + prevIndexes.current = { rowIndex, cellIndex }; + }, [getActiveMatrix, callbacks, focusedCell.current, activeMatrix.current, gridRef]); + + /** + * Handle click event + * + * @param {MouseEvent} e - Mouse event + * @param {boolean} [_force] - Force flag + * @returns {boolean} Whether the click was handled + */ + const onClick: (e: MouseEvent, _force?: boolean) => boolean = + useCallback((e: MouseEvent, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _force?: boolean): boolean => { + const target: HTMLElement = e.target as HTMLElement; + const cellElement: HTMLTableCellElement = target.closest('td, th') as HTMLTableCellElement; + + if (!cellElement) { return false; } + + const rowElement: HTMLTableRowElement = cellElement.closest('tr'); + + const isHeader: boolean = cellElement.tagName.toLowerCase() === 'th'; + const isAggregate: boolean = cellElement.closest('.sf-summaryrow') !== null; + if (isHeader) { + setActiveMatrix('header'); + } else if (isAggregate) { + setActiveMatrix('aggregate'); + } else { + setActiveMatrix('content'); + } + + const matrix: IFocusMatrix = getActiveMatrix(); + let rows: HTMLCollectionOf; + if (isHeader) { + rows = gridRef.current?.getHeaderTable()?.rows; + } else if (isAggregate) { + rows = gridRef.current?.getFooterRows(); + } else { + const contentTable: HTMLTableElement = gridRef.current?.getContentTable(); + const contentRows: HTMLCollectionOf | NodeListOf = gridRef.current?.isEdit && + gridRef.current?.editModule?.isShowAddNewRowActive ? + contentTable?.querySelectorAll?.('tr.sf-row:not(.sf-addedrow)') : contentTable?.rows; + rows = contentRows as HTMLCollectionOf; + } + + if (!rows) { return false; } + + const rowIndex: number = Array.from(rows).indexOf(rowElement); + const cellIndex: number = Array.from(rowElement.cells).indexOf(cellElement); + + if (rowIndex < 0 || cellIndex < 0) { return false; } + + // Before cell focus event + const beforeArgs: CellFocusEvent = { + cancel: false, + byKey: false, + byClick: true, + rowIndex: rowIndex, + columnIndex: cellIndex, + element: cellElement + }; + + callbacks?.beforeCellFocus?.(beforeArgs); + if (beforeArgs.cancel) { return false; } + + // Update the matrix selection + matrix.select(rowIndex, cellIndex); + // Create a new array to ensure the reference changes + matrix.current = [rowIndex, cellIndex]; + + // Create focus info for the clicked cell + const info: FocusedCellInfo = { + rowIndex: rowIndex, + colIndex: cellIndex, + isHeader: isHeader, + isAggregate: isAggregate, + element: cellElement, + elementToFocus: cellElement, + outline: true + }; + + // Add focus to the clicked cell + addFocus(info, e); + + // Notify cell clicked + const args: CellFocusEvent = { + element: cellElement, + parent: cellElement, + indexes: [rowIndex, cellIndex], + byKey: false, + byClick: true, + isJump: false, + container: { + isContent: !isHeader && !isAggregate, + isHeader: isHeader, + isAggregate: isAggregate + }, + outline: true, + rowIndex: rowIndex, + columnIndex: cellIndex, + column: gridRef.current.getColumns()[parseInt(cellIndex.toString(), 10)], + rowData: gridRef.current.getRowsObject()[parseInt(rowIndex.toString(), 10)]?.data, + event: e + }; + callbacks?.onCellClick?.(args); + + return true; + }, [getActiveMatrix, setActiveMatrix, gridRef, callbacks, addFocus, removeFocus, activeMatrix.current]); + + /** + * Get focus info for the current cell + * + * @returns {FocusedCellInfo} Focus info + */ + const getFocusInfo: () => FocusedCellInfo = useCallback((): FocusedCellInfo => { + const info: FocusedCellInfo = { + rowIndex: 0, + colIndex: 0, + isHeader: false + }; + const matrix: IFocusMatrix = getActiveMatrix(); + const [rowIndex, cellIndex]: number[] = matrix.current; + + matrix.current = [rowIndex, cellIndex]; + + const isContent: boolean = activeMatrix.current === 'content'; + const isAggregate: boolean = activeMatrix.current === 'aggregate'; + let table: HTMLTableElement | undefined; + if (isContent) { + table = gridRef.current?.getContentTable(); + } else if (isAggregate) { + table = gridRef.current?.getFooterTable(); + } else { + table = gridRef.current?.getHeaderTable(); + } + const rows: HTMLCollectionOf | NodeListOf = gridRef.current?.isEdit && + gridRef.current?.editModule?.isShowAddNewRowActive ? table?.querySelectorAll?.('tr.sf-row:not(.sf-addedrow)') : + table?.rows; + if (!table || !rows || rowIndex >= rows.length) { + return info; + } + + const row: HTMLTableRowElement | undefined = rows[rowIndex as number]; + if (!row) { return info; } + + info.element = row.cells[cellIndex as number] as HTMLElement; + + if (!info.element) { + return info; + } + + info.elementToFocus = info.element; + info.outline = true; + info.uid = row.getAttribute('data-uid'); + info.isHeader = !isContent && !isAggregate; + info.isAggregate = isAggregate; + info.rowIndex = rowIndex; + info.colIndex = cellIndex; + + return info; + }, [getActiveMatrix, gridRef, activeMatrix.current]); + + /** + * Focus a cell + * + * @param {KeyboardEvent} [e] - Keyboard event + * @returns {void} + */ + const focus: (e?: KeyboardEvent) => void = useCallback((e?: KeyboardEvent): void => { + // Get the current matrix + const matrix: IFocusMatrix = getActiveMatrix(); + + // Get the current position from the matrix + const [rowIndex, cellIndex]: number[] = matrix.current; + + // Get the table based on active matrix + let table: HTMLTableElement | null; + switch (activeMatrix.current) { + case 'content': + table = gridRef.current?.getContentTable(); + break; + case 'aggregate': + table = gridRef.current?.getFooterTable(); + break; + case 'header': + default: + table = gridRef.current?.getHeaderTable(); + break; + } + + // Create focus info object + const info: FocusedCellInfo = { + rowIndex, + colIndex: cellIndex, + isHeader: activeMatrix.current === 'header', + isAggregate: activeMatrix.current === 'aggregate', + outline: true + }; + + const rows: HTMLCollectionOf | NodeListOf = gridRef.current?.isEdit && + gridRef.current?.editModule?.isShowAddNewRowActive ? table?.querySelectorAll?.('tr.sf-row:not(.sf-addedrow)') : + table?.rows; + // Find the element in the DOM + if (table && rows.length > rowIndex && + rows[rowIndex as number] && + rows[rowIndex as number].cells.length > cellIndex) { + + info.element = rows[rowIndex as number].cells[cellIndex as number] as HTMLElement; + info.elementToFocus = info.element; + + // If we found an element, add focus to it + addFocus(info, e); + return; + } + }, [getFocusInfo, getActiveMatrix, activeMatrix.current, gridRef, addFocus, activeMatrix.current]); + + /** + * Add outline to the focused cell + * Used by Alt+W shortcut + * + * @returns {void} + */ + const addOutline: () => void = useCallback(() => { + const info: FocusedCellInfo = getFocusInfo(); + info.element?.classList.add(CSS_FOCUSED); + info.elementToFocus?.classList.add(CSS_FOCUS); + }, [getFocusInfo]); + + /** + * Handle keyboard navigation + * + * @param {KeyboardEvent} event - Keyboard event + * @returns {void} + */ + const handleKeyDown: (event: KeyboardEvent) => void = useCallback((event: KeyboardEvent): void => { + // Enhanced edit mode detection and handling + const isGridInEditMode: boolean = gridRef.current?.isEdit || false; + + // Enhanced detection for edit context elements + const activeElement: HTMLElement | null = document.activeElement as HTMLElement; + const isInEditCell: boolean | Element = activeElement && ( + activeElement.closest('.sf-edit-cell') || + activeElement.closest('.sf-editedrow') || + activeElement.closest('.sf-addedrow') + ); + const isInFilterBar: boolean | Element = activeElement?.closest('.sf-filterbarcell'); + + // Enhanced edit mode handling for proper Tab navigation between fields + // Prevent grid navigation from interfering with edit field focus + // Based on the information in your clipboard, this ensures Tab navigation works properly within edit forms + if ((isGridInEditMode && isInEditCell)) { + if (event.key === 'Tab') { + // Allow Tab navigation between edit fields + // Don't prevent default - let the EditForm component handle field navigation + // This enables the proper Tab navigation exit behavior where continuous Tab/Shift+Tab + // will eventually save the form and focus the saved row's first visible cell + return; + } else if (event.key === 'Enter') { + // Allow Enter to save/commit edit - let it bubble up to EditCell + // Don't prevent default to allow proper form submission + return; + } else if (event.key === 'Escape') { + // Allow Escape to cancel edit - let it bubble up to EditCell + // Don't prevent default to allow proper edit cancellation + return; + } else if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key)) { + // Allow text cursor movement within inputs during editing + // Don't block these keys - let them work normally within input fields + // This is essential for proper text editing experience + return; + } else if (event.key.length === 1 || ['Backspace', 'Delete', 'Insert'].includes(event.key)) { + // Allow all typing and editing keys to work normally + // This includes letters, numbers, symbols, and basic editing keys + return; + } else { + // Allow all other keys for normal input behavior (Ctrl+C, Ctrl+V, etc.) + return; + } + } + + // Enhanced filter bar handling - allow normal input behavior + if (isInFilterBar) { + if (event.key !== 'Tab') { + // Allow Tab navigation in filter bar + return; + } + } + + // Only set grid as focused if it's not already and we're not in edit mode + // This prevents focus interference during editing + if (!isGridFocused && !isGridInEditMode) { + setIsGridFocused(true); + } + + // Skip if not a navigation key or grid's inputs controls non tab keys behavior + if ((!isNavigationKey(event) && !(getNavigationDirection(event) === 'escape' + || getNavigationDirection(event) === 'ctrlPlusA' || getNavigationDirection(event) === 'space')) || (!event.key.includes('Tab') + && document.activeElement.tagName === 'INPUT' + && document.activeElement.closest('.sf-filterbarcell'))) { return; } + + // Prevent default browser behavior for navigation keys + event.preventDefault(); + + // Before cell focus event + const beforeArgs: CellFocusEvent = { + cancel: false, + byKey: true, + byClick: false, + keyArgs: event, + rowIndex: focusedCell.current.rowIndex, + columnIndex: focusedCell.current.colIndex + }; + + callbacks?.beforeCellFocus?.(beforeArgs); + if (beforeArgs.cancel) { return; } + + // Process key press + const result: boolean | undefined = onKeyPress(event); + + if (result === false) { + // Handle boundary navigation + if (swapInfo.current.swap) { + // Determine target matrix from swap info + const targetMatrix: Matrix = swapInfo.current.toMatrix || 'content'; + setActiveMatrix(targetMatrix); + + // Get the appropriate matrix after switching + const matrix: IFocusMatrix = getActiveMatrix(); + + // Determine the action from the key event + const action: string = getNavigationDirection(event); + + if (targetMatrix === 'header') { + // Moving to header - find appropriate header cell based on navigation direction + if (matrix.matrix?.length > 0 && columns?.length > 0) { + if (action === 'upArrow' || action === 'shiftTab') { + // When moving up to header or shift+tab to header, go to the last cell in the header + const lastHeaderRow: number = matrix.matrix?.length - 1; + const lastHeaderCol: number = action === 'upArrow' && (contentRowCount > 0 || aggregateRowCount > 0) ? + focusedCell.current.colIndex : + (action === 'upArrow' ? firstFocusableHeaderCellIndex[1] : lastFocusableHeaderCellIndex[1]); + matrix.select(lastHeaderRow, lastHeaderCol); + matrix.current = [lastHeaderRow, lastHeaderCol]; + } + } + } else if (targetMatrix === 'content') { + // Moving to content - find appropriate content cell based on navigation direction + if (contentRowCount > 0 && columns?.length > 0) { + if (action === 'downArrow' || action === 'tab') { + // When moving down to content or tab to content, go to the first cell in content + const firstContentCol: number = action === 'downArrow' ? focusedCell.current.colIndex : + (firstFocusableContentCellIndex[1]); + matrix.select(0, firstContentCol); + matrix.current = [0, firstContentCol]; + } else if (action === 'upArrow' || action === 'shiftTab') { + // When moving up to content from aggregate or shift+tab from aggregate + const lastContentRow: number = matrix.matrix?.length - 1 || 0; + const lastContentCol: number = action === 'upArrow' ? focusedCell.current.colIndex : + lastFocusableContentCellIndex[1]; + matrix.select(lastContentRow, lastContentCol); + matrix.current = [lastContentRow, lastContentCol]; + } + } + } else if (targetMatrix === 'aggregate') { + // Moving to aggregate - find appropriate aggregate cell based on navigation direction + if (aggregateRowCount > 0 && columns?.length > 0) { + if (action === 'downArrow' || action === 'tab') { + // When moving down to aggregate or tab to aggregate, go to the first cell in aggregate + const firstAggregateCol: number = action === 'downArrow' ? focusedCell.current.colIndex : + firstFocusableAggregateCellIndex[1]; + matrix.select(0, firstAggregateCol); + matrix.current = [0, firstAggregateCol]; + } + } + } + focus(event); + // Reset swap info + swapInfo.current = { swap: false, toHeader: false, toMatrix: 'content' }; + } + return; + } + + // Focus the cell + focus(event); + }, [isGridFocused, setIsGridFocused, focusedCell.current, onKeyPress, setActiveMatrix, getActiveMatrix, focus, callbacks, + headerRowCount, contentRowCount, aggregateRowCount, columns?.length, activeMatrix.current, addFocus]); + + /** + * Handle grid-level click event + * + * @param {MouseEvent} event - Mouse event + * @returns {void} + */ + const handleGridClick: (event: MouseEvent) => void = useCallback((event: MouseEvent): void => { + if (!isGridFocused) { + setIsGridFocused(true); + } + onClick(event); + }, [onClick, setIsGridFocused, isGridFocused]); + + const generateHeaderFilterRow: (rows?: IRow[]) => void = (rows?: IRow[]): void => { + const length: number = headerMatrix.current.matrix.length; + if (gridRef.current.filterSettings?.enabled && gridRef.current.filterSettings?.type === 'FilterBar') { + headerMatrix.current.rows = ++headerMatrix.current.rows; + const cells: ICell[] = rows[0]?.cells; + let incrementNumber: number = 0; + for (let i: number = 0; i < cells?.length; i++) { + headerMatrix.current.set( + length, incrementNumber, + cells[parseInt(i.toString(), 10)].visible && cells[parseInt(i.toString(), 10)].column.allowFilter !== false); + incrementNumber++; + } + } + }; + + /** + * Initialize matrices when row or column count changes + */ + useEffect(() => { + // Initialize content matrix + const isRowTemplate: boolean = !isNullOrUndefined(gridRef.current.rowTemplate); + if (contentRowCount > 0 && columns?.length > 0) { + // Create proper row models similar to the original implementation + // Use Array.from with index parameter to avoid unused variables + const rows: IRow[] = gridRef.current.getRowsObject(); + + // Set the rows count explicitly before generating + contentMatrix.current.rows = contentRowCount - 1; + contentMatrix.current.columns = columns?.length - 1; + + // Generate matrix with proper selector function + contentMatrix.current.generate(rows, (row: IRow, cell: ICell) => { + return (row.isDataRow && cell.visible && (cell.isDataCell)) || + (cell.column && cell.visible) + || (cell.cellType === CellType.CommandColumn); + }, isRowTemplate); + + // Initialize current position to first valid cell + const firstValidCell: number[] = contentMatrix.current?.matrix?.[0]?.[0] === 1 ? [0, 0] : + contentMatrix.current.findCellIndex([0, 0], true); + contentMatrix.current.current = [...firstValidCell]; + } else { + contentMatrix.current.matrix[0] = [1]; // empty no records cell [1] + } + + // Initialize header matrix + if (headerRowCount > 0 && columns?.length > 0) { + // Create proper header row models + // Use Array.from with index parameter to avoid unused variables + const rows: IRow[] = gridRef.current.getHeaderRowsObject(); + + // Set the rows count explicitly before generating + headerMatrix.current.rows = headerRowCount - 1; + headerMatrix.current.columns = columns?.length - 1; + + // Generate matrix with proper selector function for headers + // Use destructuring to ignore the first parameter + headerMatrix.current.generate(rows, (_unusedRow: IRow, cell: ICell) => { + return cell.visible && (cell.column.field !== undefined || cell.isTemplate || + cell.column.template !== undefined || + cell.column.type === 'checkbox' || cell.cellType === CellType.StackedHeader); + }, isRowTemplate); + generateHeaderFilterRow(rows); + + // Initialize current position to first valid cell + const firstValidCell: number[] = headerMatrix.current.matrix?.[0]?.[0] === 1 ? [0, 0] : + headerMatrix.current.findCellIndex([0, 0], true); + headerMatrix.current.current = [...firstValidCell]; + } + + // Initialize aggregate matrix + if (aggregateRowCount > 0 && columns?.length > 0) { + // Create proper aggregate row models + const rows: IRow[] = gridRef.current.getFooterRowsObject ? gridRef.current.getFooterRowsObject() : []; + + // Set the rows count explicitly before generating + aggregateMatrix.current.rows = aggregateRowCount - 1; + aggregateMatrix.current.columns = columns?.length - 1; + + // Generate matrix with proper selector function for aggregates + aggregateMatrix.current.generate(rows, (row: IRow, cell: ICell) => { + return row.isAggregateRow && cell.visible; + }, isRowTemplate); + + // Initialize current position to first valid cell + const firstValidCell: number[] = aggregateMatrix.current?.matrix?.[0]?.[0] === 1 ? [0, 0] : + aggregateMatrix.current.findCellIndex([0, 0], true); + aggregateMatrix.current.current = [...firstValidCell]; + } + + setFirstFocusableTabIndex(); + if (focusedCell.current.isHeader && focusedCell.current.rowIndex !== -1 + && focusedCell.current.colIndex !== -1) { + headerMatrix.current.current = [focusedCell.current.rowIndex, focusedCell.current.colIndex]; + } + }, [headerRowCount, contentRowCount, aggregateRowCount, columns?.length, columns]); + useEffect(() => { + if (isGridFocused && focusedCell.current.rowIndex === -1 && focusedCell.current.colIndex === -1 && + activeMatrix.current === 'content') { + setLastContentCellTabIndex(); + } + }, [isGridFocused]); + /** + * Set the first focusable element's tabIndex to 0 + * This is used to allow users to tab into the grid + * + * @returns {void} + */ + const setFirstFocusableTabIndex: () => void = useCallback(() => { + if (!gridRef.current || !gridRef.current.element) { return; } + + // Set grid element tabIndex to -1 + gridRef.current.element.tabIndex = -1; + + // Clear any existing tabIndex=0 and focus classes + const currentFocusableCell: HTMLElement | null = gridRef.current.element.querySelector('th[tabindex="0"]'); + if (currentFocusableCell) { + (currentFocusableCell as HTMLElement).tabIndex = -1; + currentFocusableCell.classList.remove(CSS_FOCUSED, CSS_FOCUS); + } + + // For basic grid, set first visible header cell tabIndex to 0 + if (columns?.length > 0 && gridRef.current.allowKeyboard) { + const headerTable: HTMLTableElement = gridRef.current.getHeaderTable(); + if (headerTable && headerTable.rows.length > 0) { + // Set active matrix to header + setActiveMatrix('header'); + + // Get the active matrix (which should now be the header matrix) + const matrix: IFocusMatrix = getActiveMatrix(); + const firstFocusableActiveCellIndex: number[] = firstFocusableHeaderCellIndex; + // Use the first focusable cell index from the matrix + if (firstFocusableActiveCellIndex && + firstFocusableActiveCellIndex.length === 2 && + firstFocusableActiveCellIndex[0] >= 0 && + firstFocusableActiveCellIndex[1] >= 0 && + firstFocusableActiveCellIndex[0] < headerTable.rows.length && + headerTable.rows[firstFocusableActiveCellIndex[0]] && + firstFocusableActiveCellIndex[1] < headerTable.rows[firstFocusableActiveCellIndex[0]].cells.length) { + + const firstHeaderCell: HTMLElement = + headerTable.rows[firstFocusableActiveCellIndex[0]].cells[firstFocusableActiveCellIndex[1]]; + + if (firstHeaderCell && !firstHeaderCell.classList.contains('sf-hide')) { + // Set tabIndex to 0 for first cell + firstHeaderCell.tabIndex = 0; + + // Update the matrix current position + matrix.select(firstFocusableActiveCellIndex[0], firstFocusableActiveCellIndex[1]); + matrix.current = [...firstFocusableActiveCellIndex]; // Create a new array to ensure React detects the change + return; + } + } + + // Fallback to first visible header cell if the calculated one is hidden or invalid + const firstVisibleHeaderCell: HTMLElement = headerTable.querySelector('.sf-headercell:not(.sf-hide)') as HTMLElement; + if (firstVisibleHeaderCell) { + firstVisibleHeaderCell.tabIndex = 0; + + // Find the row and cell index of this element + const row: HTMLTableRowElement | null = firstVisibleHeaderCell.closest('tr'); + const rowIndex: number = Array.from(headerTable.rows).indexOf(row as HTMLTableRowElement); + const cellIndex: number = Array.from(row.cells).indexOf(firstVisibleHeaderCell as HTMLTableCellElement); + + // Update the matrix current position + matrix.select(rowIndex, cellIndex); + matrix.current = [rowIndex, cellIndex]; + } + } + } + }, [gridRef, columns?.length, setActiveMatrix, getActiveMatrix, + firstFocusableContentCellIndex, + firstFocusableHeaderCellIndex + ]); + + /** + * Check if a key event is for navigation + * + * @param {KeyboardEvent} event - Keyboard event + * @returns {boolean} Whether the key is for navigation + */ + const isNavigationKey: (event: KeyboardEvent) => boolean = useCallback((event: KeyboardEvent): boolean => { + return ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab', 'Home', 'End', 'Enter'].includes(event.key); + }, []); + + /** + * Get navigation direction from key event + * + * @param {KeyboardEvent} event - Keyboard event + * @returns {string|null} Navigation direction + */ + const getNavigationDirection: (event: KeyboardEvent) => string = useCallback((event: KeyboardEvent): string => { + if (event.code === 'Space') { + return 'space'; + } + switch (event.key) { + case 'ArrowUp': return event.shiftKey ? 'shiftUp' : 'upArrow'; + case 'ArrowDown': return event.shiftKey ? 'shiftDown' : 'downArrow'; + case 'ArrowLeft': return 'leftArrow'; + case 'ArrowRight': return 'rightArrow'; + case 'Tab': return event.shiftKey ? 'shiftTab' : 'tab'; + case 'Home': return event.ctrlKey ? 'ctrlHome' : 'home'; + case 'End': return event.ctrlKey ? 'ctrlEnd' : 'end'; + case 'Enter': return event.shiftKey ? 'shiftEnter' : 'enter'; + case 'Escape': return 'escape'; + case 'a': return event.ctrlKey ? 'ctrlPlusA' : 'a'; + default: return null; + } + }, []); + + /** + * Set the last content or aggregate cell's tabIndex to 0 + * This helps with backward tabbing from elements after the grid + * + * @returns {void} + */ + const setLastContentCellTabIndex: () => void = useCallback(() => { + // Clear any existing tabIndex=0 in content or aggregate cells + const currentFocusableContentCell: HTMLElement | null = gridRef.current.getContentTable()?.querySelector('[tabindex="0"]'); + if (currentFocusableContentCell && !gridRef.current.isEdit) { + (currentFocusableContentCell as HTMLElement).tabIndex = -1; + currentFocusableContentCell.classList.remove(CSS_FOCUSED, CSS_FOCUS); + } + + const currentFocusableAggregateCell: HTMLElement | null = gridRef.current.getFooterTable?.()?.querySelector('[tabindex="0"]'); + if (currentFocusableAggregateCell && !gridRef.current.isEdit) { + (currentFocusableAggregateCell as HTMLElement).tabIndex = -1; + currentFocusableAggregateCell.classList.remove(CSS_FOCUSED, CSS_FOCUS); + } + + // Use aggregate if available, otherwise use content + if (aggregateRowCount > 0 && columns?.length > 0 && gridRef.current.allowKeyboard) { + const aggregateTable: HTMLTableElement | null = gridRef.current.getFooterTable(); + if (aggregateTable && aggregateTable.rows.length > 0) { + const lastFocusableActiveCellIndex: number[] = lastFocusableAggregateCellIndex; + // Use the last focusable cell index from the matrix + if (lastFocusableActiveCellIndex && lastFocusableActiveCellIndex.length === 2) { + const [rowIndex, colIndex] = lastFocusableActiveCellIndex; + + // Ensure the indices are valid + if (rowIndex >= 0 && rowIndex < aggregateTable.rows.length && + colIndex >= 0 && aggregateTable.rows[rowIndex as number] && + colIndex < aggregateTable.rows[rowIndex as number].cells.length) { + + const cell: HTMLTableCellElement = aggregateTable.rows[rowIndex as number].cells[colIndex as number]; + + if (cell && !cell.classList.contains('sf-hide')) { + // Set tabIndex to 0 for last aggregate cell + cell.tabIndex = 0; + + // Update the aggregate matrix to reflect this cell as the current one + aggregateMatrix.current.select(rowIndex, colIndex); + aggregateMatrix.current.current = [rowIndex, colIndex]; + + // Set active matrix to aggregate + setActiveMatrix('aggregate'); + return; + } + } + } + } + } + + // Fallback to content if aggregate is not available + const contentTable: HTMLTableElement | null = gridRef.current.getContentTable(); + if (contentTable && contentTable.rows.length > 0 && gridRef.current.allowKeyboard) { + const lastFocusableActiveCellIndex: number[] = lastFocusableContentCellIndex; + // Use the last focusable cell index from the matrix + if (lastFocusableActiveCellIndex && lastFocusableActiveCellIndex.length === 2) { + const [rowIndex, colIndex] = lastFocusableActiveCellIndex; + + // Ensure the indices are valid + if (rowIndex >= 0 && rowIndex < contentTable.rows.length && + colIndex >= 0 && contentTable.rows[rowIndex as number] && + colIndex < contentTable.rows[rowIndex as number].cells.length) { + + const cell: HTMLTableCellElement = contentTable.rows[rowIndex as number].cells[colIndex as number]; + + if (cell && !cell.classList.contains('sf-hide')) { + // Set tabIndex to 0 for last content cell + cell.tabIndex = 0; + + // Update the content matrix to reflect this cell as the current one + contentMatrix.current.select(rowIndex, colIndex); + contentMatrix.current.current = [rowIndex, colIndex]; + + // Set active matrix to content + setActiveMatrix('content'); + return; + } + } + } + } + }, [gridRef, + lastFocusableContentCellIndex, + lastFocusableHeaderCellIndex, + lastFocusableAggregateCellIndex, + contentMatrix, aggregateMatrix, setActiveMatrix, aggregateRowCount, columns?.length + ]); + + /** + * Set grid focus state + * + * @param {boolean} focused - Whether the grid is focused + * @returns {void} + */ + const setGridFocus: (focused: boolean) => void = useCallback((focused: boolean): void => { + if (!gridRef.current?.allowKeyboard) { return; } + // Check if grid is in edit mode before changing focus + const isGridInEditMode: boolean = gridRef.current?.isEdit || false; + + // Update the grid focus state + setIsGridFocused(focused); + + if (isGridInEditMode) { + return; + } + // Enhanced focus management for Tab navigation exit behavior + // Based on the information in your clipboard, this handles the case where focus moves out of grid + // and then user presses Tab/Shift+Tab to return to the grid + if (!focused) { + // Only reset focus state if NOT in edit mode + // Use requestAnimationFrame to ensure DOM is ready + requestAnimationFrame(() => { + // 1. Remove focus from current cell + removeFocus(); + + // 2. Reset active matrix to header for proper Tab navigation return behavior + setActiveMatrix('header'); + + // 3. Set tabIndex for proper Tab navigation when returning to the grid + // This ensures that when focus returns to grid via Tab/Shift+Tab, + // it goes to the appropriate cell (first visible for Tab, last visible for Shift+Tab) + setFirstFocusableTabIndex(); + setLastContentCellTabIndex(); + + // 4. Reset previous indexes to ensure clean state + prevIndexes.current = {}; + + // 5. Clear any focus indicators + clearIndicator(); + }); + } else { + // When grid gains focus, ensure proper tabIndex setup + // This handles the case where user tabs back into the grid + // Set up proper focus targets for Tab navigation return + setFirstFocusableTabIndex(); + setLastContentCellTabIndex(); + } + }, [removeFocus, setActiveMatrix, setFirstFocusableTabIndex, setLastContentCellTabIndex, clearIndicator, gridRef]); + + /** + * Navigate to a specific cell + * + * @param {number} rowIndex - Row index + * @param {number} colIndex - Column index + * @param {(Matrix)} [matrixType] - Matrix type for the cell + * @returns {void} + */ + const navigateToCell: (rowIndex: number, colIndex: number, matrixType?: Matrix) => void = + useCallback((rowIndex: number, colIndex: number, matrixType: Matrix = 'content') => { + if (!gridRef.current?.allowKeyboard) { return; } + // Set the active matrix + setActiveMatrix(matrixType); + + // Get the active matrix + const matrix: IFocusMatrix = getActiveMatrix(); + + // Check if the cell is valid + if (rowIndex >= 0 && colIndex >= 0) { + // Before cell focus event + const beforeArgs: CellFocusEvent = { + cancel: false, + byKey: false, + byClick: false, + rowIndex: rowIndex, + columnIndex: colIndex + }; + + callbacks?.beforeCellFocus?.(beforeArgs); + if (beforeArgs.cancel) { return; } + + // Update the matrix selection + matrix.select(rowIndex, colIndex); + // Create a new array to ensure the reference changes + matrix.current = [rowIndex, colIndex]; + + // Get the table based on matrix type + let table: HTMLTableElement | undefined; + switch (matrixType) { + case 'header': + table = gridRef.current?.getHeaderTable(); + break; + case 'aggregate': + table = gridRef.current?.getFooterTable(); + break; + case 'content': + default: + table = gridRef.current?.getContentTable(); + break; + } + const rows: HTMLCollectionOf | NodeListOf = gridRef.current?.isEdit && + gridRef.current?.editModule?.isShowAddNewRowActive ? table?.querySelectorAll?.('tr.sf-row:not(.sf-addedrow)') : + table?.rows; + if (table && rows.length > rowIndex) { + const row: HTMLTableRowElement = rows[rowIndex as number]; + if (row && row.cells.length > colIndex) { + // Get the cell element + const cellElement: HTMLElement = row.cells[colIndex as number] as HTMLElement; + + // Create focus info + const info: FocusedCellInfo = { + rowIndex: rowIndex, + colIndex: colIndex, + isHeader: matrixType === 'header', + isAggregate: matrixType === 'aggregate', + element: cellElement, + elementToFocus: cellElement, + outline: true + }; + + // Add focus to the cell + addFocus(info); + } + } + } + }, [setActiveMatrix, getActiveMatrix, gridRef, addFocus, callbacks]); + + /** + * Navigate to first cell, specifically useful for no parent sibling available case. + * + * @returns {void} + */ + const navigateToFirstCell: () => void = useCallback(() => { + const startInHeader: boolean = headerRowCount > 0; + setActiveMatrix(startInHeader ? 'header' : 'content'); + const matrix: IFocusMatrix = getActiveMatrix(); + // Find first valid cell using findCellIndex + const firstCell: number[] = matrix.matrix[0][0] === 1 ? [0, 0] : matrix.findCellIndex([0, 0], true); + + matrix.select(firstCell[0], firstCell[1]); + matrix.current = [...firstCell]; + focus(); + }, [headerRowCount, setActiveMatrix, getActiveMatrix, focus]); + + /** + * Navigate to last cell (prioritizing aggregate if available, otherwise content) + * + * @returns {void} + */ + const navigateToLastCell: () => void = useCallback(() => { + // Use aggregate if available, otherwise use content + if (aggregateRowCount > 0) { + setActiveMatrix('aggregate'); + const matrix: IFocusMatrix = getActiveMatrix(); + const [rowIndex, colIndex] = lastFocusableAggregateCellIndex; + + // Update the matrix selection + matrix.select(rowIndex, colIndex); + matrix.current = [rowIndex, colIndex]; + } else { + // Fallback to content area for last cell + setActiveMatrix('content'); + const matrix: IFocusMatrix = getActiveMatrix(); + const [rowIndex, colIndex] = lastFocusableContentCellIndex; + + // Update the matrix selection + matrix.select(rowIndex, colIndex); + matrix.current = [rowIndex, colIndex]; + } + + // Set the tabIndex for the last cell + setLastContentCellTabIndex(); + + // Focus the cell + focus(); + return; + }, [contentRowCount, aggregateRowCount, setActiveMatrix, getActiveMatrix, focus, + lastFocusableContentCellIndex, + lastFocusableHeaderCellIndex, + lastFocusableAggregateCellIndex, + setLastContentCellTabIndex + ]); + + /** + * Navigate to next cell based on direction + * + * @param {'up'|'down'|'left'|'right'|'nextCell'|'prevCell'} direction - Navigation direction + * @returns {void} + */ + const navigateToNextCell: (direction: 'up' | 'down' | 'left' | 'right' | 'nextCell' | 'prevCell') => void = + useCallback((direction: 'up' | 'down' | 'left' | 'right' | 'nextCell' | 'prevCell') => { + const matrix: IFocusMatrix = getActiveMatrix(); + let action: string; + switch (direction) { + case 'up': action = 'upArrow'; break; + case 'down': action = 'downArrow'; break; + case 'left': action = 'leftArrow'; break; + case 'right': action = 'rightArrow'; break; + case 'nextCell': action = 'tab'; break; + case 'prevCell': action = 'shiftTab'; break; + } + + const navigators: [number, number] = keyActions.current[action as string]; + const current: number[] = getCurrentFromAction(action, navigators, true); + + if (current) { + matrix.select(current[0], current[1]); + // Create a new array to ensure the reference changes + matrix.current = [...current]; + focus(); + } + }, [getActiveMatrix, getCurrentFromAction, focus, activeMatrix.current]); + + /** + * Focus the content area of the grid + * Used by Alt+W shortcut + * + * @returns {void} + */ + const focusContent: () => void = useCallback(() => { + // Set active matrix to content + setActiveMatrix('content'); + + // Reset focus to first cell in content + const matrix: IFocusMatrix = getActiveMatrix(); + const firstCell: number[] = matrix.matrix[0][0] === 1 ? [0, 0] : matrix.findCellIndex([0, 0], true); + matrix.select(firstCell[0], firstCell[1]); + matrix.current = [...firstCell]; + setIsGridFocused(true); + focus(); + }, [setActiveMatrix, getActiveMatrix, focus]); + + return { + // State + getFocusedCell: () => focusedCell.current, + isGridFocused, + focusByClick, + setGridFocus, + + // IFocusMatrix access + getContentMatrix: () => contentMatrix.current, + getHeaderMatrix: () => headerMatrix.current, + getAggregateMatrix, + getActiveMatrix, + setActiveMatrix, + + // Focus methods + focus, + removeFocus, + removeFocusTabIndex, + addFocus, + getFocusInfo, + setFirstFocusableTabIndex, + focusContent, + addOutline, + clearIndicator, + + // Event handlers + handleKeyDown, + handleGridClick, + + // Navigation methods + navigateToCell, + navigateToNextCell, + navigateToFirstCell, + navigateToLastCell, + + // Utility methods + isNavigationKey, + getNavigationDirection, + + // Previous state tracking + getPrevIndexes: () => prevIndexes.current, + + // Boundary indices + firstFocusableContentCellIndex, + firstFocusableHeaderCellIndex, + lastFocusableContentCellIndex, + lastFocusableHeaderCellIndex, + firstFocusableAggregateCellIndex, + lastFocusableAggregateCellIndex + }; +}; + +export default useFocusStrategy; diff --git a/components/grids/src/grid/hooks/useGrid.tsx b/components/grids/src/grid/hooks/useGrid.tsx new file mode 100644 index 0000000..ffc4a91 --- /dev/null +++ b/components/grids/src/grid/hooks/useGrid.tsx @@ -0,0 +1,1510 @@ +import { + CSSProperties, + ReactElement, + RefObject, + useCallback, + useEffect, + useId, + useMemo, + useRef, + useState, + MouseEvent, + FocusEvent +} from 'react'; +import { + Browser, + closest, + isNullOrUndefined, + formatUnit, + getValue, + IL10n, + L10n, + removeClass, + useProviderContext, + SanitizeHtmlHelper, + createElement, + preRender +} from '@syncfusion/react-base'; +import { useValueFormatter, createServiceLocator } from '../services'; +import { Query, DataManager, DataResult, DataOptions, QueryOptions, ReturnType as DataReturnType } from '@syncfusion/react-data'; +import { + MutableGridBase, + IValueFormatter, + DataRequestEvent, + GridLine, + IRow, + ClipMode, + DataChangeRequestEvent, + PendingState, + FilterPredicates +} from '../types'; +import { selectionModule, SelectionSettings } from '../types/selection.interfaces'; +import { SortDescriptorModel, SortSettings, SortModule } from '../types/sort.interfaces'; +import { GridRef, TextWrapSettings, RowInfo, IGrid, IGridBase } from '../types/grid.interfaces'; +import { filterModule, FilterSettings } from '../types/filter.interfaces'; +import { editModule } from '../types/edit.interfaces'; +import { ColumnProps } from '../types/column.interfaces'; +import { AggregateRowProps } from '../types/aggregate.interfaces'; +import { CellFocusEvent, FocusedCellInfo, IFocusMatrix } from '../types/focus.interfaces'; +import { PageSettings } from '../types/page.interfaces'; +import { searchModule, SearchSettings } from '../types/search.interfaces'; +import { ToolbarAPI } from '../types/toolbar.interfaces'; +import { ServiceLocator, UseDataResult, GridResult } from '../types/interfaces'; +import { + useAggregates, useColumns, useSelection, useSort, useSearch, useEdit, useToolbar, + useFocusStrategy, useFilter +} from './index'; +import { useData } from '../models'; +import { iterateArrayOrObject } from '../utils'; +import { ITooltip } from '@syncfusion/react-popups'; + +/** + * Default localization strings for the grid + */ +const defaultLocale: Record = { + noRecordsMessage: 'No records to display', + filterBarTooltip: '\'s filter bar cell', + invalidFilterMessage: 'Invalid filter data', + booleanTrueLabel: 'true', + booleanFalseLabel: 'false', + addButtonLabel: 'Add', + editButtonLabel: 'Edit', + cancelButtonLabel: 'Cancel', + updateButtonLabel: 'Update', + deleteButtonLabel: 'Delete', + searchButtonLabel: 'Search', + unsavedChangesConfirmation: 'Unsaved changes will be lost. Are you sure you want to continue?', + noRecordsEditMessage: 'No records selected for edit operation', + noRecordsDeleteMessage: 'No records selected for delete operation', + okButtonLabel: 'OK', + confirmDeleteMessage: 'Are you sure you want to delete the record?' +}; + +/** + * CSS class names used in the Grid component + */ +const CSS_CLASS_NAMES: Record = { + CONTROL: 'sf-control', + GRID: 'sf-grid', + RTL: 'sf-rtl', + GRID_HOVER: 'sf-gridhover', + MAC_SAFARI: 'sf-mac-safari', + MIN_HEIGHT: 'sf-grid-min-height', + HIDE_LINES: 'sf-hidelines' +}; + +const KEY_CODES: Record = { + ALT_J: 74, + ALT_W: 87, + ENTER: 13 +}; + +/** + * Custom hook to manage grid state and configuration + * + * @private + * @param {Partial} props - Grid component properties + * @param {RefObject} gridRef - Reference object for rendering interactions + * @param {RefObject} ellipsisTooltipRef - Tooltip reference + * @returns {GridResult} An object containing various grid-related state and API + */ +export const useGridComputedProps: (props: Partial, gridRef?: RefObject, + ellipsisTooltipRef?: RefObject) => GridResult = ( + props: Partial, + gridRef?: RefObject, + ellipsisTooltipRef?: RefObject +): GridResult => { + const baseProvider: { + locale: string; + dir: string; + ripple: boolean; + } = useProviderContext(); + + const locale: string = useMemo(() => + props.locale || baseProvider.locale, [props.locale, baseProvider.locale]); + const localeObj: IL10n = useMemo(() => { + const l10n: IL10n = L10n('grid', defaultLocale, locale); + l10n.setLocale(locale); + return l10n; + }, [locale]); + const valueFormatterService: IValueFormatter = useValueFormatter(locale); + const serviceLocator: ServiceLocator = useMemo(() => { + const locator: ServiceLocator = createServiceLocator(); + locator.register('localization', localeObj); + locator.register('valueFormatter', valueFormatterService); + return locator; + }, [localeObj, valueFormatterService]); + const dataSource: DataManager | DataResult = useMemo(() => { + if (props.dataSource instanceof DataManager) { + return props.dataSource; + } + else if (Array.isArray(props.dataSource)) { + return new DataManager(props.dataSource); + } + else if (props.dataSource && props.dataSource.result) { + return props.dataSource; + } + return new DataManager([]); + }, [props.dataSource]); + + const query: Query = useMemo(() => props.query instanceof Query ? props.query : new Query(), [props.query]); + // Trigger load event on initial render + useMemo(() => { + if (props.onGridRenderStart) { + props.onGridRenderStart(); + } + }, []); + const [isInitialLoad, setInitialLoad] = useState(true); + const isInitialBeforePaint: RefObject = useRef(true); + const tooltipContent: RefObject = useRef(''); + const aggregates: AggregateRowProps[] = useAggregates(props, gridRef); + + const dataState: RefObject = useRef({isPending: false, resolver: undefined, isEdit: false}); + + const { columns: preparedColumns, children, headerRowDepth, colElements, uiColumns } = + useColumns({ ...props }, gridRef, dataState, isInitialBeforePaint); + + // Initialize search settings based on props or use default values + const defaultSearchSettings: SearchSettings = { + enabled: props.searchSettings?.enabled || false, + fields: props.searchSettings?.fields || [], + value: props.searchSettings?.value || '', + operator: props.searchSettings?.operator || 'contains', + caseSensitive: props.searchSettings?.caseSensitive ?? true, + ignoreAccent: props.searchSettings?.ignoreAccent || false + }; + + const searchSettings: SearchSettings = useMemo(() => + defaultSearchSettings, [props.searchSettings]); + + // Initialize filter settings based on props or use default values + const defaultFilterSettings: FilterSettings = { + enabled: props.filterSettings?.enabled || false, + columns: props.filterSettings?.columns || [], + type: props.filterSettings?.type || 'FilterBar', + mode: props.filterSettings?.mode || 'Immediate', + immediateModeDelay: props.filterSettings?.immediateModeDelay || 1500, + ignoreAccent: props.filterSettings?.ignoreAccent || false, + operators: props.filterSettings?.operators || null, + caseSensitive: props.filterSettings?.caseSensitive || false + }; + + const filterSettings: FilterSettings = useMemo(() => { + if (!props.filterSettings?.enabled) { + defaultFilterSettings.columns = []; + } + return defaultFilterSettings; + }, [props.filterSettings?.enabled, props.filterSettings]); + + const sortSettings: SortSettings = useMemo(() => { + const combinedSortColumn: SortDescriptorModel[] = []; + if (props.sortSettings?.columns) { + if (props?.sortSettings?.enabled) { + for (let i: number = 0; i < props.sortSettings?.columns?.length; i++) { + combinedSortColumn.push(props.sortSettings?.columns[parseInt(i.toString(), 10)]); + } + } + } + // Initialize sort settings based on props or use default values + const defaultSortSettings: SortSettings = { + enabled: props.sortSettings?.enabled || false, + mode: props?.sortSettings?.mode !== 'single' || isNullOrUndefined(props?.sortSettings?.mode) ? 'multiple' : 'single', + columns: combinedSortColumn || [], + allowUnsort: props.sortSettings?.allowUnsort !== false + }; + return defaultSortSettings; + }, [props?.sortSettings?.enabled, props?.sortSettings?.mode, props.sortSettings]); + + // Update the `currentPage` state value with the `pageSettings` changes + useEffect(() => { + if (props.pageSettings?.currentPage && currentPage !== props.pageSettings?.currentPage) { + gridRef.current.goToPage?.(props.pageSettings?.currentPage); + } + }, [props.pageSettings]); + + const [currentPage, setCurrentPage] = useState(props.pageSettings?.currentPage || 1); + const [totalRecordsCount, setTotalRecordsCount] = useState(0); + + // Update the `currentPage` state value with the `pageSettings.enabled` changes + useEffect(() => { + if (!props.pageSettings?.enabled && currentPage !== 1) { + setCurrentPage(1); + } + }, [props.pageSettings?.enabled]); + + // Initialize page settings based on props or use default values + const defaultPageSettings: PageSettings = { + enabled: props.pageSettings?.enabled || false, + pageSize: props.pageSettings?.pageSize || 12, + pageCount: props.pageSettings?.pageCount || 0, + currentPage: currentPage, + template: props.pageSettings?.template || null, + totalRecordsCount: totalRecordsCount + }; + const stableRest: RefObject> = useRef(props); + const generatedId: string = useId().replace(/:/g, ''); + const id: string = useMemo(() => props.id || `grid_${generatedId}`, [props.id, generatedId]); + + const columns: ColumnProps[] = useMemo(() => + preparedColumns, [preparedColumns]); + + const clipMode: ClipMode | string = useMemo(() => { + return props.clipMode; + }, [props.clipMode]); + + const height: string | number = useMemo(() => + props.height || 'auto', [props.height]); + const width: string | number = useMemo(() => + props.width || 'auto', [props.width]); + const gridLines: GridLine | string = useMemo(() => + props.gridLines || 'Default', [props.gridLines]); + const enableRtl: boolean = useMemo(() => + (props.enableRtl ?? baseProvider.dir === 'rtl') || false, [props.enableRtl, baseProvider.dir]); + const enableHover: boolean = useMemo(() => + props.enableHover !== false, [props.enableHover]); + const allowKeyboard: boolean = useMemo(() => + props.allowKeyboard !== false, [props.allowKeyboard]); + const selectionSettings: SelectionSettings = useMemo(() => + ({ + ... { enabled: true, mode: 'Single', type: 'Row', enableToggle: true }, + ...(props.selectionSettings || {}) + }), [props.selectionSettings]); + const pageSettings: PageSettings = useMemo(() => + defaultPageSettings, [props.pageSettings]); + const textWrapSettings: TextWrapSettings = useMemo(() => { + if (gridRef.current?.textWrapSettings?.wrapMode === props.textWrapSettings?.wrapMode) { + return gridRef.current?.textWrapSettings; + } + return {... { wrapMode: 'Both' }, ...(props.textWrapSettings || {})}; + }, [props.textWrapSettings]); + const enableHtmlSanitizer: boolean = useMemo(() => + props.enableHtmlSanitizer || false, [props.enableHtmlSanitizer]); + const enableStickyHeader: boolean = useMemo(() => + props.enableStickyHeader || false, [props.enableStickyHeader]); + const rowHeight: number | null = useMemo(() => + props.rowHeight || null, [props.rowHeight]); + const enableAltRow: boolean = useMemo(() => + props.enableAltRow ?? true, [props.enableAltRow]); + const emptyRecordTemplate: string | Function | ReactElement = useMemo(() => + props.emptyRecordTemplate || null, [props.emptyRecordTemplate]); + const rowTemplate: string | Function | ReactElement = useMemo(() => + props.rowTemplate || null, [props.rowTemplate]); + + const [currentViewData, setCurrentViewData] = useState([]); + + const [responseData, setResponseData] = useState({}); + + const [gridAction, setGridAction] = useState({}); + + const cssClass: string = useMemo(() => { + return props.className || ''; + }, [props.className]); + + /** + * Compute CSS class names for the grid + */ + const className: string = useMemo(() => { + const baseClasses: string[] = [ + CSS_CLASS_NAMES.CONTROL, + CSS_CLASS_NAMES.GRID + ]; + + if (enableRtl) { + baseClasses.push(CSS_CLASS_NAMES.RTL); + } + + if (textWrapSettings?.enabled && textWrapSettings.wrapMode === 'Both') { + baseClasses.push('sf-wrap'); + } + + if (gridLines !== 'Default' && gridLines !== 'None') { + baseClasses.push(`sf-${gridLines.toLowerCase()}lines`); + } else if (gridLines === 'None') { + baseClasses.push(CSS_CLASS_NAMES.HIDE_LINES); + } + + if (enableHover) { + baseClasses.push(CSS_CLASS_NAMES.GRID_HOVER); + } + + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent) || Browser.isSafari()) { + baseClasses.push(CSS_CLASS_NAMES.MAC_SAFARI); + } + + if (rowHeight) { + baseClasses.push(CSS_CLASS_NAMES.MIN_HEIGHT); + } + + if (cssClass) { + baseClasses.push(...cssClass.split(' ')); + } + + return baseClasses.join(' '); + }, [enableRtl, enableHover, rowHeight, gridLines, cssClass, + filterSettings?.enabled, selectionSettings, textWrapSettings?.enabled, textWrapSettings, + enableHtmlSanitizer, enableStickyHeader]); + + /** + * Compute CSS styles for the grid container + */ + const styles: CSSProperties = useMemo(() => ({ + width: formatUnit(width) + }), [width]); + + /** + * Gets a Column by column name. + * + * @param {string} field - Specifies the column name. + * + * @returns {ColumnProps} Returns the column + */ + const getColumnByField: (field: string) => ColumnProps = useCallback((field: string): ColumnProps => { + return iterateArrayOrObject(columns, (item: ColumnProps) => { + if (item.field === field) { + return item; + } + return undefined; + })[0]; + }, [columns]); + + /** + * Retrieves all records from the Grid based on the current settings. + * + * The `getData` method returns an array of data objects reflecting the applied paging, filters, sorting, searching and grouping settings. + * For a remote data source, it returns only the current view data. + * + * @param {boolean} skipPage - If `true`, excludes pagination information from the returned data. + * @param {boolean} requiresCount - If `true`, then the service returns result and count. + * + * @returns {Object[] | Promise} Returns an array of records based on current settings in grid. + * + * @example + * ```tsx + * gridRef.current.getData(); + * ``` + */ + const getData: (skipPage?: boolean, requiresCount?: boolean) => Object[] | Promise = + useCallback((skipPage?: boolean, requiresCount?: boolean): Object[] | Promise => { + const query: Query = dataModule.generateQuery(); + if (requiresCount) { + query.requiresCount(); + } + if (skipPage) { + query.queries = query.queries.filter((query: QueryOptions) => query.fn !== 'onPage'); + } + if (dataSource && dataModule.isRemote() && dataSource instanceof DataManager) { + // Especially usefull for edit update whole data based aggregate + return dataOperations?.getData?.(dataSource as DataOptions, query) as Promise; + } else { + if (dataSource instanceof DataManager) { + return (dataSource as DataManager).executeLocal(query); + } else { + return new DataManager(dataSource as DataManager, query).executeLocal(query); + } + } + }, [dataSource, currentViewData]); + + /** + * Retrieves an array of all hidden columns in the Grid. + * + * The `getHiddenColumns` method returns an array containing all the column configuration objects that are currently hidden in the Grid. + * + * @returns {ColumnProps[]} Returns an array of `ColumnProps` objects representing all the currently hidden columns. + * + * @example + * ```tsx + * gridRef.current.getHiddenColumns(); + * ``` + */ + const getHiddenColumns: () => ColumnProps[] = useCallback((): ColumnProps[] => { + const cols: ColumnProps[] = []; + for (const col of (columns)) { + if (col.visible === false) { + cols.push(col); + } + } + return cols as ColumnProps[]; + }, [columns]); + + /** + * Retrieves row information based on a target cell element. + * + * The `getRowInfo` method returns detailed information about the row that contains the specified target element. + * + * @param {Element | EventTarget} target - The cell element or event target used to identify the corresponding row. + * + * @returns {RowInfo} Returns a `RowInfo` object containing details about the associated row. + * + * @example + * ```tsx + * gridRef.current.getRowInfo(event.target); + * ``` + */ + const getRowInfo: (target: Element | EventTarget) => RowInfo = useCallback((target: Element | EventTarget): RowInfo => { + const ele: Element = target as Element; + let args: Object = { target: target }; + if (!isNullOrUndefined(target)) { + const cell: Element = closest(ele, '.sf-rowcell'); + if (!cell) { + const row: Element = closest(ele, '.sf-row'); + if (!isNullOrUndefined(row) && !row.classList.contains('sf-addedrow')) { + const rowObj: IRow = gridRef.current.getRowObjectFromUID(row.getAttribute('data-uid')); + const rowIndex: number = parseInt(row.getAttribute('aria-rowindex'), 10) - 1; + args = { row: row, rowData: rowObj.data, rowIndex: rowIndex }; + } + return args; + } + const cellIndex: number = parseInt(cell.getAttribute('aria-colindex'), 10) - 1; + const row: Element = closest(cell, '.sf-row'); + if (!isNullOrUndefined(cell) && !isNaN(cellIndex) && !isNullOrUndefined(row)) { + const rowIndex: number = parseInt(row.getAttribute('aria-rowindex'), 10) - 1; + const rows: Element[] = Array.from(gridRef?.current.getRows() || []); + const index: number = cellIndex; + const rowsObject: Element[] = rows.filter((r: Element) => r.getAttribute('data-uid') === row.getAttribute('data-uid')); + let rowData: Object = {}; + let column: ColumnProps; + if (Object.keys(rowsObject).length) { + const rowObject: IRow = gridRef?.current.getRowObjectFromUID(rowsObject[0].getAttribute('data-uid')); + rowData = rowObject.data; + column = rowObject.cells[parseInt(index.toString(), 10)].column as ColumnProps; + } + args = { + cell: cell, cellIndex: cellIndex, columnIndex: cellIndex, row: row, rowIndex: rowIndex, + rowData: rowData, column: column, target: target + }; + } + } + return args; + }, []); + + /** + * Get primary key field names from columns + */ + const getPrimaryKeyFieldNames: () => string[] = useCallback((): string[] => { + const primaryKeys: string[] = []; + // Add null check for grid.columns to prevent runtime errors + if (columns) { + for (const column of columns) { + if (column.isPrimaryKey && column.field) { + primaryKeys.push(column.field); + } + } + } + return primaryKeys.length > 0 ? primaryKeys : ['id']; // Default to 'id' if no primary key found + }, [columns]); + + /** + * @returns {ColumnProps[]} returns array of column models + */ + const getVisibleColumns: () => ColumnProps[] = useCallback((): ColumnProps[] => { + const cols: ColumnProps[] = []; + const gridCols: ColumnProps[] = uiColumns ?? columns; + for (const col of gridCols) { + if (col.visible) { + cols.push(col); + } + } + return cols; + }, [columns, uiColumns]); + + /** + * Gets a column by UID. + * + * @param {string} uid - Specifies the column UID. + * + * @returns {ColumnProps} Returns the column + */ + const getColumnByUid: (uid: string) => ColumnProps = useCallback((uid: string): ColumnProps => { + const gridCols: ColumnProps[] = uiColumns ?? columns; + for (const col of gridCols) { + if (col.uid === uid) { + return col; + } + } + return undefined; + }, [columns, uiColumns]); + + /** + * Get the parent element + */ + const getParentElement: () => HTMLElement = useCallback((): HTMLElement => { + return gridRef?.current?.element as HTMLElement; + }, [gridRef?.current?.element]); + + /** + * Updates and refresh the particular row values based on the given primary key value. + * Primary key column must be specified using columns.isPrimaryKey property. + * + * @param {string| number} key - Specifies the PrimaryKey value of dataSource. + * @param {Object} rowData - To update new data for the particular row. + * + * @returns {void} + */ + const setRowData: (key: string | number, rowData?: Object, isDataSourceChangeRequired?: boolean) => void = + useCallback(async(key: string | number, rowData?: Object, isDataSourceChangeRequired: boolean = false): Promise => { + const rowuID: string = 'uid'; + const pkName: string = gridRef.current?.getPrimaryKeyFieldNames()[0]; + const selectedRow: IRow = gridRef.current?.getRowsObject().filter((r: IRow<{}>) => + getValue(pkName, r.data) === key)[0] as IRow; + if (selectedRow === undefined || selectedRow === null) { + return; + } + const selectRowEle: Element[] = selectedRow ? [].slice.call( + gridRef.current?.element.querySelectorAll('[data-uid=' + selectedRow[`${rowuID}`] + ']')) : undefined; + try { + if (isDataSourceChangeRequired) { + await dataOperations.getData({ + requestType: 'update', + data: rowData + }); + } + if (!isNullOrUndefined(selectedRow) && selectRowEle.length) { + const rowObjectData: Object = {...selectedRow.data, ...rowData}; + selectedRow.setRowObject({...selectedRow, data: rowObjectData}); + } else { + return; // if updated cell not inside the current view + } + } catch (error) { + // Trigger actionFailure event on error + // This provides consistent error handling similar to other grid operations + gridRef.current?.onError({ + error: error as Error + }); + return; + } + }, [gridRef.current]); + + /** + * Updates particular cell value based on the given primary key value. + * Primary key column must be specified using columns.isPrimaryKey property. + * + * @param {string| number} key - Specifies the PrimaryKey value of dataSource. + * @param {string } field - Specifies the field name which you want to update. + * @param {string | number | boolean | Date} value - To update new value for the particular cell. + * + * @returns {void} + */ + const setCellValue: (key: string | number, field: string, value: string | number | boolean | Date | null, + isDataSourceChangeRequired?: boolean) => void = + useCallback(async (key: string | number, field: string, value: string | number | boolean | Date | null, + isDataSourceChangeRequired?: boolean) => { + const rowuID: string = 'uid'; + const pkName: string = gridRef.current?.getPrimaryKeyFieldNames()[0]; + const selectedRow: IRow = gridRef.current?.getRowsObject().filter((r: IRow<{}>) => + getValue(pkName, r.data) === key)[0] as IRow; + if (selectedRow === undefined || selectedRow === null) { + return; + } + const selectRowEle: Element[] = selectedRow ? [].slice.call( + gridRef.current?.element.querySelectorAll('[data-uid=' + selectedRow[`${rowuID}`] + ']')) : undefined; + const changedRowData: Object = { ...selectedRow.data, [field]: value }; + try { + if (isDataSourceChangeRequired) { + await dataOperations.getData({ + requestType: 'update', + data: changedRowData + }); + } + if (!isNullOrUndefined(selectedRow) && selectRowEle.length) { + const rowObjectData: Object = { ...selectedRow.data, ...changedRowData }; + selectedRow.setRowObject({ ...selectedRow, data: rowObjectData }); + } else { + return; // if updated cell not inside the current view + } + } catch (error) { + // Trigger actionFailure event on error + // This provides consistent error handling similar to other grid operations + gridRef.current?.onError({ + error: error as Error + }); + return; + } + }, [gridRef.current]); + + /** + * Get the columns directive element + */ + const columnsDirective: ReactElement = useMemo(() => { + return children as ReactElement; + }, [children]); + + // Get header, content, and aggregate row counts for focus strategy + const headerRowCount: number = useMemo(() => headerRowDepth, [headerRowDepth]); + const contentRowCount: number = useMemo(() => currentViewData?.length || 0, [currentViewData]); + const aggregateRowCount: number = useMemo(() => aggregates?.length || 0, [aggregates]); + + const filterModule: filterModule = useFilter(gridRef, filterSettings, setGridAction, serviceLocator); + + const searchModule: searchModule = useSearch(gridRef, searchSettings, setGridAction); + + const selectionModule: selectionModule = useSelection(gridRef); + + const sortModule: SortModule = useSort(gridRef, sortSettings, setGridAction); + + useMemo(() => { + const sortedColumns: SortDescriptorModel[] = sortModule.sortSettings.columns; + if (sortedColumns.length) { + const validColumns: SortDescriptorModel[] = sortedColumns.filter((sortedColumn: SortDescriptorModel) => { + const column: ColumnProps = columns.find((col: ColumnProps) => col.field === sortedColumn.field); + return column?.allowSort; + }); + if (sortedColumns.length !== validColumns.length) { + sortModule.setSortSettings((prev: SortSettings) => ({ ...prev, columns: validColumns })); + } + } + + const filteredColumns: FilterPredicates[] = filterModule.filterSettings.columns; + if (filteredColumns.length) { + const validColumns: FilterPredicates[] = filteredColumns.filter((filteredColumn: FilterPredicates) => { + const column: ColumnProps = columns.find((col: ColumnProps) => col.field === filteredColumn.field); + return column?.allowFilter; + }); + if (filteredColumns.length !== validColumns.length) { + filterModule.setFilterSettings((prev: FilterSettings) => ({ ...prev, columns: validColumns })); + } + } + }, [columns]); + + // Initialize focus strategy - single source of truth for focus state + const focusModule: ReturnType = useFocusStrategy( + headerRowCount, + contentRowCount, + aggregateRowCount, + uiColumns ?? columns, + gridRef, + { + onCellFocus: (args: CellFocusEvent) => { + if (props.onCellFocus) { + const eventArgs: CellFocusEvent = { + column: args.column, + columnIndex: args.columnIndex, + event: args.event, + rowData: args.rowData, + rowIndex: args.rowIndex + }; + props.onCellFocus(eventArgs); + } + protectedAPI.selectionModule.onCellFocus(args); + }, + onCellClick: (args: CellFocusEvent) => { + if (props.onCellClick) { + const eventArgs: CellFocusEvent = { + column: args.column, + columnIndex: args.columnIndex, + event: args.event, + rowData: args.rowData, + rowIndex: args.rowIndex + }; + props.onCellClick(eventArgs); + } + }, + beforeCellFocus: (args: CellFocusEvent) => { + if (props.onCellFocusStart) { + props.onCellFocusStart(args); + } + } + } + ); + + const keyDownHandler: (e: React.KeyboardEvent | KeyboardEvent) => void = useCallback((e: React.KeyboardEvent | KeyboardEvent) => { + if (e.altKey) { + if (e.keyCode === KEY_CODES.ALT_J) { + const currentInfo: FocusedCellInfo = focusModule?.getFocusInfo(); + if (currentInfo && currentInfo.element) { + removeClass([currentInfo.element, currentInfo.elementToFocus], + ['sf-focused', 'sf-focus']); + currentInfo.element.tabIndex = -1; + } + gridRef.current?.element.focus(); + } + if (e.keyCode === KEY_CODES.ALT_W) { + // First ensure we're in content mode + focusModule.setActiveMatrix('content'); + + // Focus the content area + focusModule.focusContent(); + + // Add outline to the focused cell + focusModule.addOutline(); + + // Prevent default browser behavior + e.preventDefault(); + } + } + }, [focusModule, gridRef.current?.currentViewData]); + + + // Initialize data operations following original Data class pattern + // The original Data class is initialized with grid instance and service locator + // We need to pass the grid instance to useData, not just the dataSource + const gridInstance: { + dataSource: DataManager | DataResult; + query: Query; + columns: ColumnProps[]; + aggregates: AggregateRowProps[]; + sortSettings: SortSettings; + filterSettings: FilterSettings; + searchSettings: SearchSettings; + pageSettings: PageSettings; + getPrimaryKeyFieldNames: () => string[]; + onDataRequest: (args: DataRequestEvent) => void; + onDataChangeRequest: (args: DataChangeRequestEvent) => void; + } = useMemo(() => ({ + dataSource, + query, + columns: uiColumns ?? columns, + aggregates, + sortSettings: sortModule?.sortSettings, + filterSettings: filterModule?.filterSettings, + searchSettings: searchModule?.searchSettings, + pageSettings, + currentPage, + getPrimaryKeyFieldNames, + onDataRequest: props.onDataRequest, + onDataChangeRequest: props.onDataChangeRequest + }), [props.dataSource, query, sortSettings?.enabled, filterModule?.filterSettings?.enabled, + pageSettings?.enabled, sortModule?.sortSettings, searchModule?.searchSettings?.enabled, uiColumns, + columns, filterModule?.filterSettings, searchModule?.searchSettings, pageSettings, currentPage]); + + const dataOperations: UseDataResult = useData(gridInstance, gridAction, dataState); + const dataModule: UseDataResult = dataOperations; + + const editModule: editModule = useEdit( + gridRef, + serviceLocator, + uiColumns ?? columns, + currentViewData, + dataModule, + focusModule, + props.editSettings, + setGridAction, + setCurrentPage, + setResponseData + ); + + // Initialize toolbar module if toolbar is configured + // Pass modules directly to avoid context provider issues during initial rendering + const toolbarModule: ToolbarAPI | null = useToolbar( + { + toolbar: props.toolbar, + gridId: id, + onToolbarItemClick: props.onToolbarItemClick, + className: cssClass + }, + editModule, + selectionModule, + currentViewData, + searchSettings?.enabled + ); + + const isStopPropagationPreventDefault: + (e: MouseEvent | React.KeyboardEvent | FocusEvent) => boolean = + useCallback((e: MouseEvent | React.KeyboardEvent | FocusEvent) => { + return e.defaultPrevented && e.isPropagationStopped(); + }, []); + + const handleGridClick: (e: MouseEvent) => void = useCallback(async (e: MouseEvent) => { + props?.onClick?.(e); + const toolbarAction: boolean = props?.toolbar?.length + && (e.target as HTMLElement)?.closest('.sf-toolbar')?.parentElement === gridRef.current.element; + const datePicker: boolean = (e.target as HTMLElement)?.closest('.sf-datepicker')?.classList.contains('sf-popup-open'); + // Ensure grid is fully initialized before handling clicks + // This fixes the initial rendering click issue + if (isInitialLoad || !gridRef.current?.element || !currentViewData?.length || toolbarAction || datePicker || + editModule?.isDialogOpen) { + if (toolbarAction) { + focusModule.setGridFocus(false); + } + return; + } + + editModule?.handleGridClick?.(e); + + if (e.defaultPrevented || e.isPropagationStopped()) { + return; + } + // Handle selection FIRST and IMMEDIATELY, regardless of focus state + // This ensures row selection happens on the first click, even when coming from outside grid focus + selectionModule.handleGridClick(e); + + // Set grid focus AFTER selection to avoid interference + // This prevents focus management from disrupting the selection process + if (!focusModule.isGridFocused) { + focusModule.setGridFocus(true); + } + if (allowKeyboard) { + // Then handle focus management (but don't let it interfere with selection) + focusModule.handleGridClick(e); + } + + // Finally handle sorting (if applicable) + sortModule?.handleGridClick?.(e); + }, [focusModule, selectionModule, sortModule, editModule, isInitialLoad, gridRef, currentViewData, props.editSettings]); + + /** + * Handle grid-level double-click event for editing + * Single event handler at grid level instead of per-row handlers + */ + const handleGridDoubleClick: (e: MouseEvent) => void = useCallback((e: MouseEvent) => { + props?.onDoubleClick?.(e); + // Ensure grid is fully initialized before handling double-clicks + if (isInitialLoad || !gridRef.current?.element || !currentViewData?.length || isStopPropagationPreventDefault(e)) { + return; + } + const target: Element = e.target as Element; + const clickedCell: HTMLTableCellElement = target.closest('td[role="gridcell"], th[role="columnheader"]') as HTMLTableCellElement; + // Only proceed if we clicked on a valid cell + if (!clickedCell) { + return; + } + editModule?.handleGridDoubleClick(e); + const rowInfo: RowInfo = gridRef.current?.getRowInfo(clickedCell); + props.onRowDoubleClick?.({ + target: target, + cell: rowInfo.cell, + columnIndex: rowInfo.columnIndex, + row: rowInfo.row, + rowIndex: rowInfo.rowIndex, + rowData: rowInfo.rowData, + column: rowInfo.column + }); + }, [editModule, isInitialLoad, gridRef, currentViewData, props.editSettings]); + + const isEllipsisTooltip: boolean = useMemo((): boolean => { + const col: ColumnProps[] = uiColumns ?? columns; + if (clipMode === 'EllipsisWithTooltip') { + return true; + } + for (let i: number = 0; i < col.length; i++) { + if (col[parseInt(i.toString(), 10)].clipMode === 'EllipsisWithTooltip') { + return true; + } + } + return false; + }, [clipMode, uiColumns, columns]); + + /** + * To create table for ellipsiswithtooltip + * + * @param {Element} table - Defines the table + * @param {string} tag - Defines the tag + * @param {string} type - Defines the type + * @returns {HTMLDivElement} Returns the HTML div ELement + * @private + */ + const createTable: (table: Element, tag: string, type: string) => HTMLDivElement = + useCallback((table: Element, tag: string, type: string) => { + const myTableDiv: HTMLDivElement = createElement('div') as HTMLDivElement; + myTableDiv.className = gridRef.current?.element.className; + myTableDiv.style.cssText = 'display: inline-block;visibility:hidden;position:absolute'; + const mySubDiv: HTMLDivElement = createElement('div') as HTMLDivElement; + mySubDiv.className = tag; + const myTable: HTMLTableElement = createElement('table') as HTMLTableElement; + myTable.className = table.className; + myTable.style.cssText = 'table-layout: auto;width: auto'; + const ele: string = (type === 'header') ? 'th' : 'td'; + const myTr: HTMLTableRowElement = createElement('tr', { attrs: { role: 'row' } }) as HTMLTableRowElement; + const mytd: HTMLElement = createElement(ele) as HTMLElement; + myTr.appendChild(mytd); + myTable.appendChild(myTr); + mySubDiv.appendChild(myTable); + myTableDiv.appendChild(mySubDiv); + document.body.appendChild(myTableDiv); + return myTableDiv; + }, []); + + const ellipsisTooltipEvaluateInfo: { + htable: HTMLDivElement, ctable: HTMLDivElement, + create: () => void; + destroy: () => void + } = useMemo(() => { + let htable: HTMLDivElement; + let ctable: HTMLDivElement; + const create: () => void = () => { + const headerTable: Element = gridRef.current?.getHeaderTable?.() ?? + gridRef.current?.element?.querySelector('.sf-gridheader table'); + const headerDivTag: string = 'sf-gridheader'; + if (headerTable && !ellipsisTooltipEvaluateInfo?.htable) { + ellipsisTooltipEvaluateInfo.htable = createTable(headerTable, headerDivTag, 'header'); + } + if (headerTable && !ellipsisTooltipEvaluateInfo?.ctable) { + ellipsisTooltipEvaluateInfo.ctable = createTable(headerTable, headerDivTag, 'content'); + } + }; + const destroy: () => void = () => { + if (document.body.contains(ellipsisTooltipEvaluateInfo.htable)) { + document.body.removeChild(ellipsisTooltipEvaluateInfo.htable); + ellipsisTooltipEvaluateInfo.htable = null; + } + if (document.body.contains(ellipsisTooltipEvaluateInfo.ctable)) { + document.body.removeChild(ellipsisTooltipEvaluateInfo.ctable); + ellipsisTooltipEvaluateInfo.ctable = null; + } + }; + return { htable, ctable, create, destroy }; + }, [currentViewData, editModule?.isEdit, clipMode, uiColumns]); + + /** + * To evaluate sf-ellipsistooltip class required or not + * + * @param {HTMLElement} element - Defines the original cell reference element + * @returns {boolean} Define whether sf-ellipsistooltip class required for cell or not. + * @private + */ + const evaluateTooltipStatus: (element: HTMLElement) => boolean = + useCallback((element: HTMLElement): boolean => { + if (!ellipsisTooltipEvaluateInfo.htable) { + ellipsisTooltipEvaluateInfo.create(); + } + const table: HTMLDivElement = element?.classList?.contains?.('sf-headercell') ? ellipsisTooltipEvaluateInfo.htable : + ellipsisTooltipEvaluateInfo.ctable; + if (!table) { + return false; + } + const ele: string = element?.classList?.contains?.('sf-headercell') ? 'th' : 'tr'; + table.querySelector(ele).className = element?.className + 'ellipsis-tooltip-overflow-ensure'; + const targetElement: HTMLElement = table.querySelector(ele); + targetElement.innerHTML = ''; + Array.from(element?.childNodes).forEach((child: ChildNode) => { + targetElement.appendChild(child.cloneNode(true)); + }); + const width: number = table.querySelector(ele).getBoundingClientRect().width; + if (width > element?.getBoundingClientRect?.()?.width) { + return true; + } + return false; + }, [ellipsisTooltipEvaluateInfo]); + + const getEllipsisTooltipContent: () => string = useCallback(() => { + return tooltipContent.current; + }, [tooltipContent.current, uiColumns]); + + const handleGridMouseMove: (e: MouseEvent) => void = useCallback((e: MouseEvent) => { + if (isEllipsisTooltip) { + const element: HTMLElement = (e.target as Element)?.closest('.sf-ellipsistooltip') as HTMLElement; + if (!element) { + return; + } + if ((element || (e.relatedTarget as Element)?.closest?.('.sf-ellipsistooltip')) && e.type === 'mouseout' && + (ellipsisTooltipRef.current?.target?.current !== element || + element !== (e.relatedTarget as Element)?.closest?.('.sf-ellipsistooltip') as HTMLElement)) { + ellipsisTooltipRef.current?.closeTooltip?.(); + } + const tagName: string = (e.target as Element).tagName; + const elemNames: string[] = ['A', 'BUTTON', 'INPUT']; + if (element && e.type !== 'mouseout' && !(Browser.isDevice && elemNames.indexOf(tagName) !== -1)) { + if (element?.getElementsByClassName?.('sf-headertext')?.length) { + const innerElement: HTMLElement = element.getElementsByClassName('sf-headertext')[0] as HTMLElement; + tooltipContent.current = SanitizeHtmlHelper.sanitize(innerElement.innerText); + } else { + tooltipContent.current = SanitizeHtmlHelper.sanitize(element?.innerText); + } + if (element !== ellipsisTooltipRef.current?.target?.current) { + ellipsisTooltipRef.current?.openTooltip?.(element); + requestAnimationFrame(() => { + const tooltipPopup: HTMLElement = document.body.querySelector('.sf-gridellipsis-tooltip.sf-popup-close'); + if (tooltipPopup) { + tooltipPopup.classList.remove('sf-popup-close'); // seems tooltip maintain class on rapid hover due to our element childNode text length detection delay. + tooltipPopup.classList.add('sf-popup-open'); + } + }); + } + } + } + }, [isEllipsisTooltip]); + + const handleGridMouseOut: (e: MouseEvent) => void = useCallback((e: MouseEvent) => { + props?.onMouseOut?.(e); + if (isStopPropagationPreventDefault(e)) { return; } + handleGridMouseMove(e); + }, [isEllipsisTooltip]); + const handleGridMouseOver: (e: MouseEvent) => void = useCallback((e: MouseEvent) => { + props?.onMouseOver?.(e); + if (isStopPropagationPreventDefault(e)) { return; } + handleGridMouseMove(e); + }, [isEllipsisTooltip]); + + const handleGridMouseDown: (e: MouseEvent) => void = useCallback((e: MouseEvent) => { + props?.onMouseDown?.(e); + if (isStopPropagationPreventDefault(e)) { return; } + focusModule.focusByClick = true; + if ((e.target as Element).closest('.sf-gridcontent,.sf-gridheader') && (e.shiftKey || e.ctrlKey)) { + e.preventDefault(); + } + filterModule?.mouseDownHandler?.(e); + }, [focusModule, filterModule]); + + const handleGridFocus: (e: FocusEvent) => void = useCallback((e: FocusEvent) => { + props?.onFocus?.(e); + if ((pageSettings?.enabled && e.target?.closest('.sf-pager') && e.target.closest('.sf-pager').parentElement === gridRef.current.element) + || (props?.toolbar?.length && e.target?.closest('.sf-toolbar')?.parentElement === gridRef.current.element) || isStopPropagationPreventDefault(e) + || e.target.closest('#' + id + 'EditAlert')) { + return; + } + // Check if grid is in edit mode to prevent focus interference + const isGridInEditMode: boolean = editModule?.isEdit || false; + + // If grid is in edit mode, don't interfere with edit focus management + // This prevents the focus from jumping to header cell when edit form regains focus + if (isGridInEditMode) { + // Just set grid focus state but don't move focus around + if (focusModule && !focusModule.isGridFocused) { + focusModule.setGridFocus(true); + } + return; + } + // When the grid receives focus, set grid focus state and focus first cell if needed + if (focusModule && !focusModule.isGridFocused) { + focusModule.setGridFocus(true); + + // Determine if focus is coming from before or after the grid + const relatedTarget: HTMLElement = e.relatedTarget as HTMLElement; + const gridElement: HTMLElement = gridRef.current.element; + + // Check if we can determine the focus direction + let isForwardTabbing: boolean = true; + + if (relatedTarget) { + // Try to determine if we're tabbing forward or backward + // This is a heuristic and may not be 100% accurate in all cases + const allFocusableElements: Element[] = Array.from(document.querySelectorAll( + 'div.sf-grid, button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + )); + + const gridIndex: number = allFocusableElements.indexOf(gridElement); + const relatedIndex: number = allFocusableElements.indexOf(relatedTarget); + const targetIndex: number = allFocusableElements.indexOf(e.target); + + if (gridIndex > -1 && relatedIndex > -1) { + isForwardTabbing = relatedIndex < gridIndex; + if (gridElement.contains(relatedTarget) && targetIndex > -1) { + isForwardTabbing = relatedIndex < targetIndex; + } + } + } + if (focusModule.focusByClick) { + focusModule.focusByClick = false; + return; + } else { + focusModule.focusByClick = false; + } + // Only navigate to a cell if no cell is currently focused + const { getFocusedCell } = focusModule; + const focusedCell: FocusedCellInfo = getFocusedCell(); + if (focusedCell.rowIndex === -1 && focusedCell.colIndex === -1 && gridRef.current.allowKeyboard) { + if (!isForwardTabbing) { + focusModule.setActiveMatrix(aggregates?.length ? 'aggregate' : 'content'); + const matrix: IFocusMatrix = focusModule.getActiveMatrix(); + let lastCell: number[] = [matrix.rows, matrix.columns]; + if (matrix.matrix[lastCell[0]][lastCell[1]] === 0) { + lastCell = matrix.findCellIndex(lastCell, false); + } + matrix.current = lastCell; + focusModule.focus(); + return; + } else { + // When tabbing forward into grid, focus first header cell + // But only if we have header content, otherwise focus first content cell + if (headerRowCount > 0) { + // Use requestAnimationFrame to ensure the DOM is ready + requestAnimationFrame(() => { + // Focus the first cell when tabbing forward into the grid + focusModule.navigateToFirstCell(); + }); + } else { + // No header, focus first content cell + focusModule.setActiveMatrix('content'); + requestAnimationFrame(() => { + focusModule.focus(); + }); + } + } + } + } else if (focusModule && focusModule.focusByClick) { + focusModule.focusByClick = false; + } + }, [focusModule, editModule, headerRowCount]); + + const handleGridBlur: (e: FocusEvent) => void = useCallback((e: FocusEvent) => { + props?.onBlur?.(e); + if ((props?.toolbar?.length && e.target?.closest('.sf-toolbar')?.parentElement === gridRef.current.element) || + isStopPropagationPreventDefault(e)) { + return; + } + // Check if grid is in edit mode to prevent focus interference + const isGridInEditMode: boolean = editModule?.isEdit || false; + + // If grid is in edit mode, don't interfere with edit focus management + // This prevents the focus from jumping to header cell when edit form regains focus + if (isGridInEditMode) { + // Just set grid focus state but don't move focus around + if (focusModule && !focusModule.isGridFocused) { + focusModule.setGridFocus(true); + } + return; + } + + // When the grid loses focus, update grid focus state + // Only if focus is truly moving outside the grid + if (focusModule && focusModule.isGridFocused) { + // Check if focus is staying within the grid or related elements + const relatedTarget: HTMLElement = e.relatedTarget as HTMLElement; + + // Don't remove focus if: + // 1. Focus is moving to another element within the grid + // 2. Focus is moving to a grid popup + // 3. Focus is moving to a specific element that should maintain grid focus + let isStayingInGrid: boolean | Element = (e.target && (e.target as HTMLElement).closest('#' + id + '_toolbar')) || + e.target?.closest('.sf-datepicker') || + // Focus moving to another element within the grid + (e.currentTarget.contains(relatedTarget) || + // Focus moving to a grid popup + (relatedTarget && relatedTarget.closest('.sf-grid-popup')) || + // Focus still within the grid (using document.activeElement) + document.activeElement && document.activeElement.closest('.sf-grid')) as boolean; + isStayingInGrid = relatedTarget && relatedTarget.closest('.sf-pager') ? false : isStayingInGrid; + if (!isStayingInGrid) { + // Clear focus completely when leaving the grid + focusModule.clearIndicator(); + focusModule.removeFocus(); + focusModule.setGridFocus(false); + } + } + }, [focusModule]); + + const handleGridKeyUp: (e: React.KeyboardEvent) => void = useCallback((e: React.KeyboardEvent) => { + props?.onKeyUp?.(e); + if (isStopPropagationPreventDefault(e)) { + return; + } + if (e.keyCode !== 13) { + filterModule?.keyUpHandler?.(e as React.KeyboardEvent); + } + }, [filterModule]); + + const handleGridKeyDown: (e: React.KeyboardEvent) => void = useCallback((e: React.KeyboardEvent) => { + props?.onKeyDown?.(e); + // Check for cancellation or specific dropdown open condition + const target: Element = e.target as Element; + const isDropdownOpenCondition: boolean = editModule?.isEdit && + target?.closest('.sf-gridform') && + (target?.closest('.sf-ddl') || target?.closest('.sf-datepicker')) && + e.altKey && + e.code === 'ArrowDown'; + if (isStopPropagationPreventDefault(e) || isDropdownOpenCondition || editModule?.isDialogOpen) { + e.preventDefault(); + e.stopPropagation(); + return; // Early return to prevent further processing + } + sortModule?.keyUpHandler?.(e as React.KeyboardEvent); + if (sortModule && e.keyCode === 13 && closest(e.target as Element, '.sf-headercell')) { + return; + } + const pageAction: boolean = pageSettings?.enabled && (e.target as HTMLElement)?.closest('.sf-pager') + && (e.target as HTMLElement).closest('.sf-pager').parentElement === gridRef.current.element; + const toolbarAction: boolean = props?.toolbar?.length && + (e.target as HTMLElement)?.closest('.sf-toolbar')?.parentElement === gridRef.current.element; + if ((e.key === 'Shift' && e.shiftKey) || (e.key === 'Control' && e.ctrlKey) || pageAction || toolbarAction) { return; } + + // Enhanced keyboard action handling based on original TypeScript implementation + // This implements comprehensive keyboard actions including Insert and Delete keys + const isMacLike: boolean = /(Mac)/i.test(navigator.platform); + + const editForm: HTMLElement | null = (e.target as HTMLElement)?.closest('.sf-gridform'); + // Handle edit-specific keyboard events first + if (props.editSettings?.allowEdit || props.editSettings?.allowAdd || props.editSettings?.allowDelete) { + + // Insert key or Mac Cmd+Enter to add record + if ((e.key === 'Insert' || (isMacLike && e.metaKey && e.key === 'Enter')) && + props.editSettings?.allowAdd && !editModule?.isEdit) { + e.preventDefault(); + editModule?.addRecord?.(); + return; + } + + // Delete key to delete selected record + if (e.key === 'Delete' && props.editSettings?.allowDelete && !editModule?.isEdit) { + const target: HTMLElement = e.target as HTMLElement; + // Safety checks: ignore if focus is on input elements (except checkboxes) + const isInputFocused: boolean = target.tagName === 'INPUT' && !target.classList.contains('sf-checkselect'); + const isDialogOpen: Element = document.querySelector('.sf-popup-open.sf-edit-dialog'); + + if (!isInputFocused && !isDialogOpen) { + e.preventDefault(); + editModule?.deleteRecord?.(); + return; + } + } + + // F2 key to start editing + if (e.key === 'F2' && !editModule?.isEdit) { + e.preventDefault(); + editModule?.editRow?.(); + return; + } + + // Enter key to save changes (when in edit mode) + if (e.key === 'Enter' && editModule?.isEdit) { + const target: HTMLElement = e.target as HTMLElement; + // Only handle if not in input field or specific grid context + if (!target.closest('.sf-unboundcelldiv') && + (target.closest('.sf-gridcontent') || target.closest('.sf-headerContent')) && editForm) { + e.preventDefault(); + editModule.escEnterIndex.current = parseInt((e.target as HTMLElement)?.closest('td')?.getAttribute('aria-colindex'), 10) - 1; + (editModule?.saveChanges as Function)?.(undefined, undefined, 'Key'); + return; + } + } + + // Escape key to cancel editing + if (e.key === 'Escape' && editModule?.isEdit && editForm) { + e.preventDefault(); + editModule.escEnterIndex.current = parseInt((e.target as HTMLElement)?.closest('td')?.getAttribute('aria-colindex'), 10) - 1; + (editModule?.cancelChanges as Function)?.('Key'); + return; + } + } + + const isGridInEditMode: boolean = editModule?.isEdit || false; + if (isGridInEditMode && e.key === 'Tab' && editForm) { + if (editForm) { + const tabEvent: CustomEvent = new CustomEvent('editCellTab', { + detail: { + field: getColumnByUid((e.target as HTMLElement)?.closest('td')?.getAttribute('data-mappinguid')).field, + direction: e.shiftKey ? 'backward' : 'forward', + originalEvent: e + } + }); + editForm?.dispatchEvent(tabEvent); + } + return; + } + + // Handle keyboard navigation + filterModule?.keyUpHandler?.(e as React.KeyboardEvent); + const { getFocusInfo } = focusModule; + const focusedCell: FocusedCellInfo = getFocusInfo(); + // Check if we're on the first header cell and pressing Shift+Tab + const isFirstHeaderCell: boolean = focusedCell.isHeader && + focusedCell.rowIndex === focusModule.firstFocusableHeaderCellIndex?.[0] && + focusedCell.colIndex === focusModule.firstFocusableHeaderCellIndex?.[1]; + const isShiftTab: boolean = e.key === 'Tab' && e.shiftKey; + + // Check if we're on the last content cell and pressing Tab + const isLastContentCell: boolean = !focusedCell.isHeader && !aggregates?.length && + focusedCell.rowIndex === focusModule.lastFocusableContentCellIndex?.[0] && + focusedCell.colIndex === focusModule.lastFocusableContentCellIndex?.[1]; + const isLastAggregateCell: boolean = focusedCell.isAggregate && + focusedCell.rowIndex === focusModule.lastFocusableAggregateCellIndex?.[0] && + focusedCell.colIndex === focusModule.lastFocusableAggregateCellIndex?.[1]; + const isTab: boolean = e.key === 'Tab' && !e.shiftKey; + + // If we're on the first header cell and pressing Shift+Tab, or + // on the last content cell and pressing Tab, let the default behavior happen + if ((isFirstHeaderCell && isShiftTab) || ((isLastContentCell || isLastAggregateCell) && isTab)) { + // Clear focus completely + focusModule.clearIndicator(); + focusModule.removeFocus(); + focusModule.setGridFocus(false); + + // Don't prevent default to allow natural tab navigation + return; + } + + // Otherwise, handle navigation normally + focusModule.handleKeyDown(e); + }, [focusModule, filterModule, props.editSettings, editModule]); + + useEffect(() => { + if (allowKeyboard) { + document.body.addEventListener('keydown', keyDownHandler); + } + return () => { + if (allowKeyboard) { + document.body.removeEventListener('keydown', keyDownHandler); + } + }; + }, [allowKeyboard]); + + // Initialize grid and handle cleanup + useEffect(() => { + // Set up focus management when grid mounts + // Set the first focusable element's tabIndex to 0 + focusModule.setFirstFocusableTabIndex(); + preRender('grid'); + if (props.onGridInit) { + props.onGridInit(); // trigger only once on initial render, once Dom element mounted. + } + isInitialBeforePaint.current = false; + return () => { + props.onGridDestroy?.(); + setCurrentViewData(null); + isInitialBeforePaint.current = null; + setInitialLoad(null); + }; + }, []); + + useEffect(() => { + isInitialBeforePaint.current = false; + }, [columnsDirective]); + + // Only update the ref if props has meaningfully changed + useEffect(() => { + stableRest.current = props; + }, [props]); // we might use a custom comparison for props here to avoid re-render. + + /** + * Private API for internal grid operations + */ + const privateAPI: GridResult['privateAPI'] = useMemo(() => ({ + styles, + isEllipsisTooltip, + setCurrentViewData, + setInitialLoad, + handleGridClick, + handleGridDoubleClick, + handleGridMouseDown, + handleGridMouseOut, + handleGridMouseOver, + getEllipsisTooltipContent, + handleGridFocus, + handleGridBlur, + handleGridKeyDown, + handleGridKeyUp, + setCurrentPage, + setTotalRecordsCount, + setGridAction + }), [styles, setCurrentViewData, handleGridClick, handleGridDoubleClick, setCurrentPage, setTotalRecordsCount, + setGridAction, handleGridMouseDown, handleGridMouseOut, handleGridMouseOver, getEllipsisTooltipContent]); + + /** + * Public API exposed to consumers of the grid + * Always keep memorized public APIs for Grid component context provider + * This will prevent unnecessary re-rendering of child components + * These are for readonly purpose - if a property needs to be updated, + * it should not be included here but in the protected API + */ + const publicAPI: IGrid = useMemo(() => ({ + ...stableRest.current, + getVisibleColumns, + getColumnByUid, + getColumnByField, + getData, + getHiddenColumns, + getRowInfo, + getPrimaryKeyFieldNames, + setRowData, + setCellValue, + serviceLocator, + className, + dataSource: dataOperations.dataManager, + id, + height, + children, + clipMode, + width, + enableRtl, + enableHover, + selectionSettings, + gridLines, + filterSettings: filterModule?.filterSettings, + sortSettings: sortModule?.sortSettings, + searchSettings: searchModule?.searchSettings, + pageSettings, + textWrapSettings, + enableHtmlSanitizer, + enableStickyHeader, + rowHeight, + enableAltRow, + columns, + locale, + query, + emptyRecordTemplate, + rowTemplate, + aggregates, + editSettings: props.editSettings, + allowKeyboard + } as IGrid), [ + getVisibleColumns, + getColumnByUid, + getColumnByField, + getData, + getHiddenColumns, + getRowInfo, + getPrimaryKeyFieldNames, + setRowData, + setCellValue, + serviceLocator, + className, + dataOperations.dataManager, + id, + height, + children, + clipMode, + width, + enableRtl, + enableHover, + selectionSettings, + gridLines, + filterModule?.filterSettings, + sortModule?.sortSettings, + searchModule?.searchSettings, + pageSettings, + textWrapSettings, + enableHtmlSanitizer, + enableStickyHeader, + rowHeight, + enableAltRow, + columns, + locale, + query, + emptyRecordTemplate, + rowTemplate, + aggregates, + allowKeyboard, + props + ]); + + /** + * Protected API for internal grid components + */ + const protectedAPI: Partial = useMemo(() => ({ + currentViewData, + columnsDirective, + headerRowDepth, + colElements, + isInitialLoad, + focusModule, + selectionModule, + getParentElement, + evaluateTooltipStatus, + sortModule, + searchModule, + filterModule, + editModule, + toolbarModule, + currentPage, + totalRecordsCount, + gridAction, + isInitialBeforePaint, + uiColumns, + cssClass, + responseData, + setResponseData, + dataModule + }), [currentViewData, columnsDirective, headerRowDepth, colElements, isInitialLoad, focusModule, selectionModule, getParentElement, + sortModule, searchModule, filterModule, editModule, sortSettings, searchSettings, evaluateTooltipStatus, uiColumns, + currentPage, totalRecordsCount, gridAction, isInitialBeforePaint, cssClass, responseData, setResponseData, dataModule]); + + useEffect(() => { + gridRef.current = { + ...gridRef.current, + ...publicAPI, + // Ensure currentViewData is always up-to-date in gridRef + currentViewData: currentViewData + }; + ellipsisTooltipEvaluateInfo.destroy(); + }, [publicAPI, currentViewData, ellipsisTooltipEvaluateInfo]); + + return { privateAPI, publicAPI, protectedAPI }; +}; diff --git a/components/grids/src/grid/hooks/useRender.tsx b/components/grids/src/grid/hooks/useRender.tsx new file mode 100644 index 0000000..d2879e9 --- /dev/null +++ b/components/grids/src/grid/hooks/useRender.tsx @@ -0,0 +1,727 @@ +import { + CSSProperties, + useCallback, + useEffect, + useMemo, + useState, + ReactNode, + ReactElement, + JSX, + Children, + isValidElement, + RefObject, + useRef +} from 'react'; +import { + IValueFormatter, + PendingState, MutableGridSetter, UseRenderResult +} from '../types/interfaces'; +import { + IGrid, + IGridBase, + GridRef } from '../types/grid.interfaces'; +import { ColumnProps, IColumnBase, PrepareColumns } from '../types/column.interfaces'; +import { AggregateRowProps, AggregateColumnProps } from '../types/aggregate.interfaces'; +import { DateFormatOptions, extend, formatUnit, isNullOrUndefined, NumberFormatOptions } from '@syncfusion/react-base'; +import { DataManager, DataResult, ReturnType } from '@syncfusion/react-data'; +import { Column, ColumnBase } from '../components'; +import { + useGridComputedProvider, + useGridMutableProvider +} from '../contexts'; +import { defaultColumnProps } from '../hooks'; +import { Columns, RenderBase, Aggregates } from '../views'; +import { addLastRowBorder, compareSelectedProperties, getObject, setFormatter } from '../utils'; +import { FilterEvent, PageEvent, SearchEvent, SortEvent } from '../types'; + +/** + * CSS class names used in the component + */ +const CSS_CLASS_NAMES: Record = { + VISIBLE: '', + HIDDEN: 'none' +}; + +/** + * Custom hook to manage rendering state and data for the grid + * + * @private + * @returns {UseRenderResult} Object containing APIs for grid rendering + */ +export const useRender: () => UseRenderResult = (): UseRenderResult => { + const grid: Partial & Partial = useGridComputedProvider(); + const { setCurrentViewData, setInitialLoad, setTotalRecordsCount, aggregates, pageSettings, + height, contentPanelRef, contentTableRef } = grid; + const { currentViewData, currentPage, gridAction, uiColumns, isInitialLoad, + setResponseData, dataModule, totalRecordsCount } = useGridMutableProvider(); + + const [isLayoutRendered, setIsLayoutRendered] = useState(false); + const [isContentBusy, setIsContentBusy] = useState(true); + const isColTypeDef: RefObject = useRef(false); + + /** + * Get data operations from the grid's dataModule + * This ensures single source of truth for DataManager across all components + */ + const dataManager: DataManager | DataResult = dataModule?.dataManager; + const generateQuery: Function = dataModule?.generateQuery; + /** + * Compute content styles based on grid height + */ + const contentStyles: CSSProperties = useMemo(() => ({ + height: formatUnit(grid.height as string | number), + overflowY: grid.height === 'auto' ? 'auto' : 'scroll' + }), [grid.height]); + + const updateColumnTypes: (data: Object) => void = useCallback((data: Object) => { + let value: string | number | boolean | Object; + (uiColumns ?? grid.columns).map((newColumn: Partial) => { + if (isNullOrUndefined(newColumn.field)) { + return newColumn; + } + // update column type, format, parser, and other first dataSource based properties here + value = getObject(newColumn.field, data); + if (!isNullOrUndefined(value)) { + isColTypeDef.current = true; + if (!newColumn.type) { + newColumn.type = value instanceof Date && value.getDay ? (value.getHours() > 0 || value.getMinutes() > 0 || + value.getSeconds() > 0 || value.getMilliseconds() > 0 ? 'datetime' : 'date') : typeof (value); + } + } else { + newColumn.type = newColumn.type || null; + } + const valueFormatter: IValueFormatter = grid.serviceLocator?.getService('valueFormatter'); + if (newColumn.format && ((newColumn.format as DateFormatOptions).skeleton + || ((newColumn.format as DateFormatOptions).format && + typeof (newColumn.format as DateFormatOptions).format === 'string'))) { + // Store the formatter and parser functions directly on the new object + newColumn.formatFn = valueFormatter.getFormatFunction(extend({}, newColumn.format as DateFormatOptions)); + newColumn.parseFn = valueFormatter.getParserFunction(newColumn.format as DateFormatOptions); + } + if (newColumn.onSortComparer) { + let a: Function = newColumn.onSortComparer; + newColumn.onSortComparer = (x: number | string, y: number | string, xObj?: Object, yObj?: Object) => { + if (typeof a === 'string') { + a = getObject(a, window) as Function; + } + if (newColumn.sortDirection === 'Descending') { + const z: number | string = x as number | string; + x = y; + y = z; + const obj: Object = xObj; + xObj = yObj; + yObj = obj; + } + return a(x, y, xObj, yObj, newColumn.sortDirection); + }; + } + if (typeof (newColumn.format) === 'string') { + setFormatter(grid.serviceLocator, newColumn); + } else if (!newColumn.format && newColumn.type === 'number') { + newColumn.parseFn = valueFormatter.getParserFunction({ format: 'n2' } as NumberFormatOptions); + } + if (newColumn.type === 'dateonly' && !newColumn.format) { + newColumn.format = 'yMd'; + setFormatter(grid.serviceLocator, newColumn); + } + return newColumn; + }); + }, [grid.columns, uiColumns]); + + /** + * Handle successful data retrieval + */ + const dataManagerSuccess: (response: Response | ReturnType) => void = useCallback((response: Response | ReturnType): void => { + const data: ReturnType = response as ReturnType; + if (!data?.result?.length && data.count && grid.pageSettings?.enabled + && gridAction.requestType !== 'paging') { + if (Object.keys(gridAction).length) { + delete gridAction.cancel; + if (gridAction.requestType === 'filtering' || gridAction.requestType === 'clearFiltering') { + gridAction.type = 'filtered'; + grid.onFilter?.(gridAction); + } else if (gridAction.requestType === 'searching') { + gridAction.type = 'searched'; + grid.onSearch?.(gridAction); + } + } + grid.goToPage(Math.ceil(data.count / grid.pageSettings.pageSize)); + return; + } + if (grid.pageSettings?.enabled) { + grid.pagerModule?.goToPage(currentPage); + setTotalRecordsCount(data.count); + } + setResponseData(data); + + if (grid.onDataLoadStart) { + grid.onDataLoadStart(data); + } + grid.clearSelection(); + setCurrentViewData(data.result as Object[]); + if (!isColTypeDef.current && data.result.length > 0) { + updateColumnTypes(data.result[0]); + } + setIsLayoutRendered(true); + }, [grid.onDataLoadStart, setCurrentViewData, gridAction]); + + /** + * Handle data retrieval failure + */ + const dataManagerFailure: (error: Error) => void = useCallback((error: Error): void => { + setIsContentBusy(false); + grid.onError?.({ error }); + }, [grid.onError]); + + /** + * Show the loading spinner + */ + const showSpinner: () => void = useCallback(() => { + setIsContentBusy(true); + }, []); + + /** + * Hide the loading spinner + */ + const hideSpinner: () => void = useCallback(() => { + setIsContentBusy(false); + }, []); + + /** + * Refresh data from the data manager + */ + const refreshDataManager: () => void = useCallback((): void => { + setIsContentBusy(true); + showSpinner(); + if (dataModule.dataState.current.isPending) { + dataModule.dataState.current.resolver(dataManager); + if (dataModule.dataState.current.isEdit) { + dataManagerSuccess(dataManager as ReturnType); + } + dataModule.dataState.current = { isPending: false, resolver: undefined, isEdit: false }; + } else { + const dataManagerPromise: Promise = dataModule.getData(gridAction, generateQuery().requiresCount()); + dataManagerPromise.then(dataManagerSuccess).catch(dataManagerFailure); + } + }, [dataManager, grid.query, dataManagerSuccess, dataManagerFailure, grid.showSpinner, currentPage, + aggregates, gridAction, grid.filterSettings, grid.sortSettings, grid.searchSettings, pageSettings.pageSize]); + + // Initial data load + useMemo(() => { + refreshDataManager(); + }, [dataManager, grid.query, grid.columns, currentPage, aggregates, pageSettings?.enabled, + grid.filterSettings, grid.sortSettings, grid.searchSettings, pageSettings.pageSize]); + + useMemo(() => { + updateColumnTypes(grid?.getCurrentViewRecords?.()?.[0]); + }, [uiColumns]); + + // Handle layout rendered state + useEffect(() => { + if (isLayoutRendered) { + hideSpinner(); + if (grid.onDataLoad) { + grid.onDataLoad(); + } + if (height !== 'auto' && (contentPanelRef?.firstElementChild as HTMLElement)?.offsetHeight > contentTableRef?.scrollHeight) { + addLastRowBorder(contentTableRef, grid.editSettings); + } + if (isInitialLoad) { + grid?.onGridRenderComplete?.(); + } + if (Object.keys(gridAction).length) { + delete gridAction.cancel; + if (gridAction.requestType === 'filtering' || gridAction.requestType === 'clearFiltering') { + gridAction.type = 'filtered'; + const eventArgs: FilterEvent = { + action: (gridAction as FilterEvent).action, + columns: (gridAction as FilterEvent).columns, + currentFilterColumn: (gridAction as FilterEvent).currentFilterColumn, + currentFilterObject: (gridAction as FilterEvent).currentFilterObject + }; + grid.onFilter?.(eventArgs); + } else if (gridAction.requestType === 'sorting' || gridAction.requestType === 'clearSorting') { + gridAction.type = 'sorted'; + const eventArgs: SortEvent = { + direction: (gridAction as SortEvent).direction, + field: (gridAction as SortEvent).field, + target: (gridAction as SortEvent).target, + action: gridAction.requestType + }; + grid.onSort?.(eventArgs); + } else if (gridAction.requestType === 'searching') { + gridAction.type = 'searched'; + const eventArgs: SearchEvent = { + value: (gridAction as SearchEvent).value + }; + grid.onSearch?.(eventArgs); + } else if (gridAction.requestType === 'paging') { + gridAction.type = 'pageChanged'; + const eventArgs: PageEvent = { + currentPage: (gridAction as PageEvent).currentPage, + previousPage: (gridAction as PageEvent).previousPage, + totalRecordsCount: totalRecordsCount + }; + grid.onPageChange?.(eventArgs); + } else if (gridAction.requestType === 'refresh') { + gridAction.type = 'refreshed'; + grid.onRefresh?.(); + } + gridAction.type = 'actionComplete'; + } + const actionCompleteEvent: CustomEvent = new CustomEvent('actionComplete'); + grid.element.dispatchEvent(actionCompleteEvent); + setIsContentBusy(false); + setInitialLoad(false); + } + }, [isLayoutRendered, currentViewData]); + + // Cleanup on unmount + useEffect(() => { + return () => { + setIsContentBusy(false); + setIsLayoutRendered(null); // Reset state on unmount + }; + }, []); + + // Memoize APIs to prevent unnecessary re-renders + const publicRenderAPI: Partial = useMemo(() => ({ ...grid }), [grid]); + + const privateRenderAPI: UseRenderResult['privateRenderAPI'] = useMemo(() => ({ + contentStyles, + isLayoutRendered, + isContentBusy + }), [contentStyles, isLayoutRendered, isContentBusy]); + + const protectedRenderAPI: UseRenderResult['protectedRenderAPI'] = useMemo(() => ({ + refresh: refreshDataManager, + showSpinner, + hideSpinner + }), [refreshDataManager]); + + return { + publicRenderAPI, + privateRenderAPI, + protectedRenderAPI + }; +}; + +/** + * Generate a unique key for a column + * + * @param {ColumnProps} columnProps - Column properties + * @param {string} index - Index path for uniqueness + * @param {string} prefix - Optional prefix for the key + * @returns {string} Unique key for the column + */ +const generateUniqueKey: (columnProps: ColumnProps, index: string, prefix?: string) => string = + (columnProps: ColumnProps, index: string, prefix: string = ''): string => { + // Use field if available, otherwise use headerText, or fallback to index + const baseKey: string = columnProps.field || columnProps.headerText || 'col'; + // Add a unique suffix based on the index path to ensure uniqueness + return `${prefix}${baseKey}-${index}`; + }; + +/** + * Type definition for keys to compare in column objects + * Improves type safety and provides better auto-completion + */ +type ColumnCompareKeys = Array; +type AggregateColumnCompareKeys = Array; + +/** + * Get relevant column properties that should trigger change detection + * This allows for better performance by only comparing properties that matter + * + * @returns {ColumnCompareKeys} - Data Affecting column properties comparison keys + */ +function getDataColumnCompareKeys(): ColumnCompareKeys { + return [ + 'allowSort', 'allowFilter', 'allowSearch', 'columns' + ]; +} + +/** + * Get relevant column properties that should trigger change detection + * This allows for better performance by only comparing properties that matter + * + * @returns {ColumnCompareKeys} - UI Affecting column properties comparison keys + */ +function getUIColumnCompareKeys(): ColumnCompareKeys { + return [ + 'textAlign', 'headerTextAlign', 'disableHtmlEncode', 'clipMode', 'customAttributes', 'format', 'displayAsCheckBox', 'allowEdit', + 'templateSettings', 'edit', 'width', 'visible', 'headerText', 'template', 'headerTemplate', 'editTemplate', + 'onValueAccessor' + ]; +} + +/** + * Get relevant aggregate column properties that should trigger change detection + * This allows for better performance by only comparing properties that matter + * + * @returns {AggregateColumnCompareKeys} - Data Affecting aggregate column properties comparison keys + */ +function getAggregateColumnCompareKeys(): AggregateColumnCompareKeys { + return [ + 'type', 'format', 'columnName', 'field' + ]; +} + +/** + * Prepare columns from children or column definitions + * + * @param {Object[]} children - Child elements or column definitions + * @param {number} parentDepth - Current depth in the column hierarchy + * @param {string} parentIndex - Index path for uniqueness + * @param {ColumnProps[]} prevColumns - previous columns which is used to compare old and new and detect whether customer changed state is related to column or not. + * @returns {Object} Object containing columns, depth, children, and column group elements + */ +const prepareColumns: ( + children: ReactNode | (ColumnProps | ReactElement)[], + parentDepth?: number, + parentIndex?: string, + prevColumns?: ColumnProps[] +) => { + columns: ColumnProps[]; + depth: number; + children: ReactNode; + colGroup: JSX.Element[]; + isColumnChanged: boolean; + isUIColumnpropertiesChanged: boolean; +} = ( + children: ReactNode | (ColumnProps | ReactElement)[], + parentDepth: number = 0, + parentIndex: string = '', + prevColumns?: ColumnProps[] +): { + columns: ColumnProps[]; + depth: number; + children: ReactNode; + colGroup: JSX.Element[]; + isColumnChanged: boolean; + isUIColumnpropertiesChanged: boolean; +} => { + let maxDepth: number = parentDepth; + let isColumnChanged: boolean = false; // currently used/handled always column state changed manner even unrelated state change props.children changed. + let isUIColumnpropertiesChanged: boolean = false; + const columns: ColumnProps[] = []; + const adjustedChildren: ReactNode[] = []; + const colGroup: JSX.Element[] = []; + const childArray: ReactElement[] = Array.isArray(children) + ? children as ReactElement[] + : Children.toArray(children) as ReactElement[]; + + for (let i: number = 0; i < childArray.length; i++) { + const child: ReactElement = childArray[i as number]; + const currentIndex: string = parentIndex ? `${parentIndex}-${i}` : `${i}`; + + if (isValidReactElement(child) && ( + child.type === ColumnBase || + child.type === RenderBase || + child.type === Columns || + child.type === Column + )) { + const columnProps: ColumnProps = defaultColumnProps(child.props as ColumnProps); + // Generate a unique key for the column + const columnKey: string = generateUniqueKey(columnProps, currentIndex); + + if (child.type === ColumnBase || child.type === Column) { + // Check for and process nested columns + if ((child.props as { children: ReactNode })?.children) { + const childContents: { + columns: ColumnProps[]; + depth: number; + children: ReactNode; + colGroup: JSX.Element[]; + isColumnChanged: boolean; + isUIColumnpropertiesChanged: boolean; + } = prepareColumns( + (child.props as { children: ReactElement })?.children, + parentDepth + 1, + currentIndex, + prevColumns + ); + isColumnChanged = childContents.isColumnChanged; + isUIColumnpropertiesChanged = childContents.isUIColumnpropertiesChanged; + columns.push({ ...columnProps, columns: childContents.columns }); // Nest child columns + colGroup.push(...childContents.colGroup); // Gather col elements from child columns + maxDepth = Math.max(maxDepth, childContents.depth); + } else { + if (prevColumns?.[i as number]?.field === columnProps.field) { + // Only compare specific properties that should trigger a change + const hasChanged: boolean = isColumnChanged || !compareSelectedProperties( + prevColumns?.[i as number], + columnProps, + getDataColumnCompareKeys() + ); + // Update isColumnChanged if any changes detected + isColumnChanged = isColumnChanged || hasChanged; + const hasUIChanged: boolean = isColumnChanged || !compareSelectedProperties( + prevColumns?.[i as number], + columnProps, + getUIColumnCompareKeys() + ); + isUIColumnpropertiesChanged = isUIColumnpropertiesChanged || hasUIChanged; + } + columns.push(columnProps); + + // Only create col elements for leaf columns + colGroup.push( + + ); + } + + adjustedChildren.push( + + {(child.props as { children: ReactElement })?.children} + + ); + } else if (child.type === RenderBase || child.type === Columns) { + const { + columns: childColumns, + depth, + colGroup: childColGroup, + children, + isColumnChanged: isChildrenColumnsChanged, + isUIColumnpropertiesChanged: isChildrenColumnsUIChanged + } = prepareColumns( + (child.props as { children: ReactElement })?.children, + parentDepth, + currentIndex, + prevColumns + ); + isColumnChanged = isChildrenColumnsChanged; + isUIColumnpropertiesChanged = isChildrenColumnsUIChanged; + columns.push(...childColumns); + colGroup.push(...childColGroup); + adjustedChildren.push( + ((children as ReactElement).props as { children: ReactElement[] })?.children + ); + maxDepth = Math.max(maxDepth, depth); + } + } else if (isColumnObject(child)) { + const columnObject: ColumnProps = defaultColumnProps(child); + const columnKey: string = generateUniqueKey(columnObject, currentIndex, 'obj-'); + + if (prevColumns?.[i as number]?.field === columnObject.field) { + // Only compare specific properties that should trigger a change + const hasChanged: boolean = isColumnChanged || !compareSelectedProperties( + prevColumns?.[i as number], + columnObject, + getDataColumnCompareKeys() + ); + // Update isColumnChanged if any changes detected + isColumnChanged = isColumnChanged || hasChanged; + const hasUIChanged: boolean = isColumnChanged || !compareSelectedProperties( + prevColumns?.[i as number], + columnObject, + getUIColumnCompareKeys() + ); + isUIColumnpropertiesChanged = isUIColumnpropertiesChanged || hasUIChanged; + } + columns.push(columnObject); + adjustedChildren.push(); + + // Generate col element for object definitions + colGroup.push( + + ); + } + } + + if (maxDepth === parentDepth) { + maxDepth++; + } + + return { + columns, + depth: maxDepth, + children: {adjustedChildren}, + colGroup, + isColumnChanged, + isUIColumnpropertiesChanged + }; +}; + +/** + * Helper function to check if an element is a valid React element + * + * @param {ReactNode} element - Element to check + * @returns {boolean} true if the element is a valid React element + */ +const isValidReactElement: (element: ReactNode) => element is ReactElement = (element: ReactNode): element is ReactElement => { + return isValidElement(element); +}; + +/** + * Helper function to check if an object is a column model + * + * @param {ColumnProps | ReactNode} child - Object to check + * @returns {boolean} true if the object is a column model + */ +function isColumnObject(child: ColumnProps | ReactNode): child is ColumnProps { + return !isValidReactElement(child as ReactElement) && + typeof child === 'object' && + child !== null && + 'field' in child; +} + +/** + * Custom hook to process columns from props + * + * @param {Partial} props - Grid properties + * @param {RefObject} gridRef - Grid reference object properties + * @param {RefObject} dataState - Data state object properties + * @param {RefObject} isInitialBeforePaint - UI column properties changes not trigger event purpose boolean + * @returns {Partial} Updated grid properties with processed columns + */ +export const useColumns: (props: Partial, gridRef: RefObject, dataState?: RefObject, + isInitialBeforePaint?: RefObject) => +Partial & { uiColumns: ColumnProps[] } = + (props: Partial, gridRef: RefObject, dataState?: RefObject, + isInitialBeforePaint?: RefObject): Partial & { uiColumns: ColumnProps[] } => { + const prevPrepareColumns: RefObject = useRef({} as PrepareColumns); + const isNoColumnRemoteData: boolean = useMemo(() => { + return !props.columns && !props.children && props.dataSource instanceof DataManager && props.dataSource.dataSource.url + && Array.isArray(gridRef.current?.currentViewData) && gridRef.current?.currentViewData?.length > 0; + }, [props.children, props.columns, props.dataSource, gridRef.current?.currentViewData]); + const { children, depth: headerRowDepth, columns, colGroup, uiColumns } = useMemo(() => { + if (dataState.current.isPending) { + return prevPrepareColumns.current; + } + const result: PrepareColumns = prepareColumns( + props.columns ?? + props.children ?? + ((Array.isArray(props.dataSource) && (props.dataSource as Object[]).length > 0) + ? Object.keys((props.dataSource as Object[])[0]) + .map((key: string) => ({ + field: key, + headerText: key + })) + : ((Array.isArray(gridRef.current?.currentViewData) && gridRef.current?.currentViewData?.length > 0) + ? Object.keys(gridRef.current?.currentViewData[0]) + .map((key: string) => ({ + field: key, + headerText: key + })) + : undefined) + ), null, null, gridRef.current?.columns + ); + if (!result.isColumnChanged && gridRef.current?.columns) { + if (result.isUIColumnpropertiesChanged || prevPrepareColumns.current?.columns?.length !== result.columns?.length) { + isInitialBeforePaint.current = true; + return { + ...prevPrepareColumns.current, + uiColumns: result.columns, + children: result.children, + colGroup: result.colGroup + }; + } else { + return prevPrepareColumns.current; + } + } + prevPrepareColumns.current = result; + return result; // content refresh with dataManager request and triggering events. + }, [props.children, props.columns, props.dataSource, isNoColumnRemoteData]); + + return useMemo(() => ({ + columns, + uiColumns, + headerRowDepth, + children, + colElements: colGroup + }), [columns, uiColumns, headerRowDepth]); + }; + +const generateDirectiveAggregates: (props: { children?: ReactNode }) => AggregateRowProps[] = + (props: { children?: ReactNode }): AggregateRowProps[] => { + const aggregates: AggregateRowProps[] = []; + const rowArray: ReactElement[] = Array.isArray(props.children) + ? props.children as ReactElement[] + : Children.toArray(props.children) as ReactElement[]; + for (let i: number = 0; i < rowArray.length; i++) { + const aggregateRow: AggregateRowProps = { columns: [] }; + const childRow: AggregateRowProps = rowArray[parseInt(i.toString(), 10)].props; + if (childRow.columns) { + aggregateRow.columns = childRow.columns; + } else if (childRow.children) { + const aggregateColumns: AggregateColumnProps[] = []; + const columnArray: ReactElement[] = Array.isArray(childRow.children) + ? childRow.children as ReactElement[] + : Children.toArray(childRow.children) as ReactElement[]; + for (let j: number = 0; j < columnArray.length; j++) { + const column: AggregateColumnProps = columnArray[parseInt(j.toString(), 10)].props; + aggregateColumns.push({...column}); + } + aggregateRow.columns = aggregateColumns; + } + aggregates.push(aggregateRow); + } + return aggregates; + }; + +const prepareAggregates: (aggregates: AggregateRowProps[], gridRef: RefObject) => boolean = +(aggregates: AggregateRowProps[], gridRef: RefObject): boolean => { + let isAggregateColumnsChanged: boolean = false; + for (let i: number = 0; i < aggregates?.length; i++) { + const columns: AggregateColumnProps[] = aggregates[parseInt(i.toString(), 10)].columns; + for (let j: number = 0; j < columns.length; j++) { + if (!columns[parseInt(j.toString(), 10)].columnName) { + if (gridRef.current?.aggregates?.[i as number]?.columns?.[j as number]?.columnName === columns[j as number].columnName + ) { + // Only compare specific properties that should trigger a change + const hasChanged: boolean = !compareSelectedProperties( + gridRef.current?.aggregates?.[i as number]?.columns?.[j as number], + columns[j as number], + getAggregateColumnCompareKeys() + ); + // Update isColumnChanged if any changes detected + isAggregateColumnsChanged = isAggregateColumnsChanged || hasChanged; + } + columns[parseInt(j.toString(), 10)].columnName = columns[parseInt(j.toString(), 10)].field; + } + } + } + return isAggregateColumnsChanged; +}; + +export const useAggregates: (props: Partial, gridRef?: RefObject) => AggregateRowProps[] = + (props: Partial, gridRef?: RefObject): AggregateRowProps[] => { + let aggregates: AggregateRowProps[] = []; + let isAggregateColumnsChanged: boolean = false; + const childArray: ReactElement[] = Array.isArray(props.children) + ? props.children as ReactElement[] + : Children.toArray(props.children) as ReactElement[]; + const directiveAggregates: ReactElement = childArray.find((child: ReactElement) => { + return child && child.type === Aggregates; + }); + if (props.aggregates) { + aggregates = useMemo(() => props.aggregates, [props.aggregates]); + } else if (directiveAggregates) { + aggregates = useMemo(() => generateDirectiveAggregates(directiveAggregates.props), [props.children]); + } + isAggregateColumnsChanged = prepareAggregates(aggregates, gridRef); + return useMemo(() => { + if (isAggregateColumnsChanged) { + return aggregates; + } else { + return gridRef.current?.aggregates; + } + }, [isAggregateColumnsChanged]); + }; diff --git a/components/grids/src/grid/hooks/useScroll.tsx b/components/grids/src/grid/hooks/useScroll.tsx new file mode 100644 index 0000000..98c2ab9 --- /dev/null +++ b/components/grids/src/grid/hooks/useScroll.tsx @@ -0,0 +1,314 @@ +import { CSSProperties, useCallback, useLayoutEffect, useRef, UIEvent, useMemo, useState, useEffect, RefObject } from 'react'; +import { Browser, isNullOrUndefined } from '@syncfusion/react-base'; +import { useGridComputedProvider, useGridMutableProvider } from '../contexts'; +import { IGrid } from '../types/grid.interfaces'; +import { MutableGridSetter, UseScrollResult, ScrollElements, ScrollCss } from '../types/interfaces'; + +/** + * Custom hook to manage scroll synchronization between header and content panels + * + * @private + * @returns {UseScrollResult} Scroll-related APIs and functions + */ +export const useScroll: () => UseScrollResult = (): UseScrollResult => { + const grid: Partial & Partial = useGridComputedProvider(); + const { height, enableRtl, enableStickyHeader } = grid; + const { getParentElement } = useGridMutableProvider(); + const [scrollStyles, setScrollStyles] = useState<{ headerPadding: CSSProperties; headerContentBorder: CSSProperties; }>({ + headerPadding: {}, + headerContentBorder: {} + }); + + // Use ref to maintain references to DOM elements + const elementsRef: RefObject = useRef({ + headerScrollElement: null, + contentScrollElement: null, + footerScrollElement: null + }); + + /** + * Determine CSS properties based on RTL/LTR mode + * + * @returns {ScrollCss} CSS properties for scroll customization + */ + const getCssProperties: ScrollCss = useMemo((): ScrollCss => { + return { + border: enableRtl ? 'borderLeftWidth' : 'borderRightWidth', + padding: enableRtl ? 'paddingLeft' : 'paddingRight' + }; + }, [enableRtl]); + + /** + * Get browser-specific threshold for scrollbar calculations + * + * @returns {number} Threshold value + */ + const getThreshold: () => number = useCallback((): number => { + // Safely access Browser.info with multiple fallbacks + if (!Browser?.info) { return 1; } + const browserName: string = Browser.info.name; + return browserName === 'mozilla' ? 0.5 : 1; + }, []); + + /** + * Calculate scrollbar width + * + * @returns {number} Width of the scrollbar + */ + const getScrollBarWidth: () => number = useCallback((): number => { + const { contentScrollElement } = elementsRef.current; + return (contentScrollElement.offsetWidth - contentScrollElement.clientWidth) | 0; + }, []); + + /** + * Set padding based on scrollbar width to ensure header and content alignment + */ + const setPadding: () => void = useCallback((): void => { + + const scrollWidth: number = getScrollBarWidth() - getThreshold(); + const cssProps: ScrollCss = getCssProperties; + + const paddingValue: string = scrollWidth > 0 ? `${scrollWidth}px` : '0px'; + const borderValue: string = scrollWidth > 0 ? '1px' : '0px'; + + setScrollStyles({ + headerPadding: { [cssProps.padding]: paddingValue }, + headerContentBorder: { [cssProps.border]: borderValue } + }); + }, [getScrollBarWidth, getThreshold, getCssProperties]); + + const setSticky: (headerEle: HTMLElement, top?: number, width?: number, left?: number, isAddStickyHeader?: boolean) => void = + useCallback((headerEle: HTMLElement, top?: number, width?: number, left?: number, isAddStickyHeader?: boolean): void => { + if (isAddStickyHeader) { + headerEle.classList.add('sf-sticky'); + } else { + headerEle.classList.remove('sf-sticky'); + } + headerEle.style.width = width != null ? width + 'px' : ''; + headerEle.style.top = top != null ? top + 'px' : ''; + headerEle.style.left = left !== null ? left + 'px' : ''; + }, []); + + /** + * Complete implementation of makeStickyHeader following original component logic exactly + * This matches the original scroll.ts makeStickyHeader method line by line + */ + const makeStickyHeader: () => void = useCallback(() => { + const { contentScrollElement, headerScrollElement } = elementsRef.current; + if (!getParentElement() || !contentScrollElement) { + return; + } + + const gridElement: HTMLElement = getParentElement(); + const contentRect: DOMRect = contentScrollElement.getBoundingClientRect(); + + if (!contentRect) { + return; + } + + // Handle window scale for proper positioning + const windowScale: number = window.devicePixelRatio; + const headerEle: HTMLElement = headerScrollElement?.parentElement; + const toolbarEle: HTMLElement | null = gridElement.querySelector('.sf-toolbar'); + + if (!headerEle) { + return; + } + + // Calculate total height including all sticky elements (exact original logic) + const height: number = headerEle.offsetHeight + + (toolbarEle ? toolbarEle.offsetHeight : 0); + + const parentTop: number = gridElement.getBoundingClientRect().top; + let top: number = contentRect.top - (parentTop < 0 ? 0 : parentTop); + const left: number = contentRect.left; + + // Handle window scale adjustment (from original) + if (windowScale !== 1) { + top = Math.ceil(top); + } + + // Apply sticky positioning when scrolled (exact original logic) + if (top < height && contentRect.bottom > 0) { + headerEle.classList.add('sf-sticky'); + let elemTop: number = 0; + + // Handle toolbar sticky positioning (from original) + if (toolbarEle) { + setSticky(toolbarEle, elemTop, contentRect.width, left, true); + elemTop += toolbarEle.getBoundingClientRect().height; + } + + // Handle main header sticky positioning (from original) + setSticky(headerEle, elemTop, contentRect.width, left, true); + + } else { + // Remove sticky positioning when not needed (exact original logic) + if (headerEle.classList.contains('sf-sticky')) { + setSticky(headerEle, null, null, null, false); + + if (toolbarEle) { + setSticky(toolbarEle, null, null, null, false); + } + } + } + }, [setSticky, getParentElement]); + + const addEventListener: () => void = useCallback((): void => { + const scrollableParent: HTMLElement = getScrollbleParent(getParentElement().parentElement); + if (scrollableParent) { + window.addEventListener('scroll', makeStickyHeader); + } + }, [getParentElement, makeStickyHeader]); + + const removeEventListener: () => void = useCallback((): void => { + window.removeEventListener('scroll', makeStickyHeader); + }, [makeStickyHeader]); + + const getScrollbleParent: (node: HTMLElement) => HTMLElement = useCallback((node: HTMLElement): HTMLElement => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parent: HTMLElement = isNullOrUndefined(node.tagName) ? (node as any).scrollingElement : node; + const overflowY: string = document.defaultView.getComputedStyle(parent, null).overflowY; + if (parent.scrollHeight > parent.clientHeight && overflowY !== 'visible' || + node.tagName === 'HTML' || node.tagName === 'BODY') { + return node; + } else { + return getScrollbleParent(node.parentNode as HTMLElement); + } + }, []); + + // Update padding when height or RTL mode changes + useLayoutEffect(() => { + if (elementsRef.current.contentScrollElement) { + setPadding(); + } + }, [height, enableRtl, setPadding]); + + useEffect(() => { + if (enableStickyHeader) { + addEventListener(); + } + return () => { + if (enableStickyHeader) { + removeEventListener(); + } + }; + }, [enableStickyHeader]); + + /** + * Set reference to header scroll element + * + * @param {HTMLElement | null} element - Header scroll DOM element + */ + const setHeaderScrollElement: (element: HTMLElement | null) => void = useCallback((element: HTMLElement | null): void => { + elementsRef.current.headerScrollElement = element; + }, []); + + /** + * Set reference to content scroll element + * + * @param {HTMLElement | null} element - Content scroll DOM element + */ + const setContentScrollElement: (element: HTMLElement | null) => void = useCallback((element: HTMLElement | null): void => { + elementsRef.current.contentScrollElement = element; + }, []); + + /** + * Set reference to footer scroll element + * + * @param {HTMLElement | null} element - Footer element + */ + const setFooterScrollElement: (element: HTMLElement | null) => void = useCallback((element: HTMLElement | null): void => { + elementsRef.current.footerScrollElement = element; + }, []); + + /** + * Handle content scroll events and synchronize header scroll position + * Optimized for immediate synchronization to prevent gridline misalignment + * + * @param {UIEvent} args - Scroll event arguments + */ + const onContentScroll: (args: UIEvent) => void = useCallback((args: UIEvent): void => { + const { headerScrollElement, footerScrollElement } = elementsRef.current; + + const target: HTMLDivElement = args.target as HTMLDivElement; + const left: number = target.scrollLeft; + + // IMMEDIATE synchronization - no requestAnimationFrame delay to prevent gridline misalignment + headerScrollElement.scrollLeft = left; + if (footerScrollElement) { + footerScrollElement.scrollLeft = left; + } + }, []); + + /** + * Handle header scroll events and synchronize content scroll position + * This is especially important for keyboard navigation (tabbing) + * Optimized for immediate synchronization to prevent gridline misalignment + * + * @param {UIEvent} args - Scroll event arguments + */ + const onHeaderScroll: (args: UIEvent) => void = useCallback((args: UIEvent): void => { + const { contentScrollElement } = elementsRef.current; + + const target: HTMLDivElement = args.target as HTMLDivElement; + const left: number = target.scrollLeft; + + // IMMEDIATE synchronization - no requestAnimationFrame delay to prevent gridline misalignment + contentScrollElement.scrollLeft = left; + }, []); + + /** + * Handle footer scroll events and synchronize content scroll position + * This maintains consistency between footer and content scroll positions + * Optimized for immediate synchronization to prevent gridline misalignment + * + * @param {UIEvent} args - Scroll event arguments + */ + const onFooterScroll: (args: UIEvent) => void = useCallback((args: UIEvent): void => { + const { contentScrollElement } = elementsRef.current; + + const target: HTMLDivElement = args.target as HTMLDivElement; + const left: number = target.scrollLeft; + + // IMMEDIATE synchronization - no requestAnimationFrame delay to prevent gridline misalignment + contentScrollElement.scrollLeft = left; + }, []); + + // Clean up resources on unmount to prevent memory leaks + useEffect(() => { + return () => { + // Clear references to DOM elements + elementsRef.current = { + headerScrollElement: null, + contentScrollElement: null, + footerScrollElement: null + }; + }; + }, []); + + // Memoize API objects to prevent unnecessary re-renders + const publicScrollAPI: Partial = useMemo(() => ({ ...grid }), [grid]); + + const privateScrollAPI: UseScrollResult['privateScrollAPI'] = useMemo(() => ({ + getCssProperties, + headerContentBorder: scrollStyles.headerContentBorder, + headerPadding: scrollStyles.headerPadding, + onContentScroll, + onHeaderScroll, + onFooterScroll + }), [getCssProperties, scrollStyles.headerContentBorder, scrollStyles.headerPadding, onContentScroll, onHeaderScroll, onFooterScroll]); + + const protectedScrollAPI: UseScrollResult['protectedScrollAPI'] = useMemo(() => ({ + setPadding + }), [setPadding]); + + return { + publicScrollAPI, + privateScrollAPI, + protectedScrollAPI, + setHeaderScrollElement, + setContentScrollElement, + setFooterScrollElement + }; +}; diff --git a/components/grids/src/grid/hooks/useSearch.ts b/components/grids/src/grid/hooks/useSearch.ts new file mode 100644 index 0000000..b69117b --- /dev/null +++ b/components/grids/src/grid/hooks/useSearch.ts @@ -0,0 +1,94 @@ +import { RefObject, useCallback, useEffect, useState } from 'react'; +import { SearchEvent, SearchSettings } from '../types/search.interfaces'; +import { isNullOrUndefined} from '@syncfusion/react-base'; +import { GridRef } from '../types/grid.interfaces'; +import { SearchAPI } from '../types/search.interfaces'; + +/** + * Custom hook to manage Search configuration + * + * @private + * @param {RefObject} gridRef - Reference to the grid component + * @param {SearchSettings} searchSetting - Reference to the search settings + * @param {Function} setGridAction - Function to update grid actions + * @returns {SearchAPI} An object containing various sort-related state and API + */ +export const useSearch: (gridRef?: RefObject, searchSetting?: SearchSettings, + setGridAction?: (action: Object) => void) => SearchAPI = (gridRef?: RefObject, searchSetting?: SearchSettings, + setGridAction?: (action: Object) => void) => { + + const [searchSettings, setSearchSetting] = useState(searchSetting); + + /** + * update searchSettings properties searchModule + */ + useEffect(() => { + setSearchSetting(searchSetting); + }, [searchSetting]); + + /** + * Checks if the input string contains non-numeric characters. + * + * @param {string} searchString - The string to be checked for non-numeric characters. + * @returns {boolean} - `true` if the input string contains non-numeric characters, `false` otherwise. + */ + const hasNonNumericCharacters: (searchString: string) => boolean = useCallback((searchString: string): boolean => { + let decimalFound: boolean = false; + for (const char of searchString) { + if ((char < '0' || char > '9') && char !== '.') { + return true; + } + if (char === '.') { + if (decimalFound) { + // If decimal is found more than once, it's not valid + return true; + } + decimalFound = true; + } + } + return false; + }, []); + + /** + * Searches Grid records by given key. + * + * @param {string} searchString - Defines the key. + * @returns {void} + */ + const search: (searchString: string) => void = async(searchString: string): Promise => { + searchString = isNullOrUndefined(searchString) ? '' : searchString; + let searchValue: string | number; + if (gridRef.current?.searchSettings?.enabled === false) { return; } + if (searchString !== gridRef.current.searchSettings.value) { + // Check searchString is number and parseFloat to remove trailing zeros + if (searchString !== '' && !hasNonNumericCharacters(searchString)) { + if (searchString === '.' || (searchString.indexOf('.') === -1)) { + searchValue = searchString.toString(); + } else { + searchValue = parseFloat(searchString).toString(); + } + } else { + searchValue = searchString.toString(); + } + const args: SearchEvent = { cancel: false, requestType: 'searching', value: searchValue }; + args.type = 'searching'; + const confirmResult: boolean = await gridRef.current?.editModule?.checkUnsavedChanges?.(); + if (!isNullOrUndefined(confirmResult) && !confirmResult) { + return; + } + gridRef.current.onSearchStart?.(args); + if (args.cancel) { + return; + } + gridRef.current.searchSettings.value = searchValue; + setSearchSetting((prevSettings: SearchSettings) => { + return { ...prevSettings, key: searchValue as string }; + }); + args.type = 'actionComplete'; + setGridAction(args); + } + }; + + + return { search, searchSettings, setSearchSetting }; +}; diff --git a/components/grids/src/grid/hooks/useSelection.ts b/components/grids/src/grid/hooks/useSelection.ts new file mode 100644 index 0000000..b57ad3f --- /dev/null +++ b/components/grids/src/grid/hooks/useSelection.ts @@ -0,0 +1,584 @@ + +import { RefObject, useCallback, useRef, KeyboardEvent } from 'react'; +import { IRow } from '../types'; +import { RowSelectEvent, RowSelectingEvent, SelectionModel } from '../types/selection.interfaces'; +import { ColumnProps } from '../types/column.interfaces'; +import { closest, isNullOrUndefined } from '@syncfusion/react-base'; +import { CellFocusEvent } from '../types/focus.interfaces'; +import { GridRef } from '../types/grid.interfaces'; + +/** + * Custom hook to manage selection state and API + * + * @private + * @param {RefObject} gridRef - Reference to the grid component + * @returns {SelectionModel} An object containing selection-related state and API + */ +export const useSelection: (gridRef?: RefObject) => SelectionModel = (gridRef?: RefObject): SelectionModel => { + + const selectedRowIndexes: RefObject = useRef([]); + const selectedRecords: RefObject = useRef([]); + const prevRowIndex: RefObject = useRef(null); + const activeTarget: RefObject = useRef(null); + const isMultiShiftRequest: RefObject = useRef(false); + const isMultiCtrlRequest: RefObject = useRef(false); + const isRowSelected: RefObject = useRef(false); + + /** + * Adds or removes selection classes from row cells + */ + const addRemoveSelectionClasses: (row: Element, isAdd: boolean) => void = useCallback((row: Element, isAdd: boolean): void => { + const cells: Element[] = Array.from(row.getElementsByClassName('sf-rowcell')); + for (let i: number = 0; i < cells.length; i++) { + if (isAdd) { + cells[parseInt(i.toString(), 10)].classList.add('sf-active'); + cells[parseInt(i.toString(), 10)].setAttribute('aria-selected', 'true'); + } else { + cells[parseInt(i.toString(), 10)].classList.remove('sf-active'); + cells[parseInt(i.toString(), 10)].removeAttribute('aria-selected'); + } + } + }, []); + + const getRowObj: (row: Element | number) => IRow = useCallback((row: Element | number): IRow => { + if (isNullOrUndefined(row)) { return {} as IRow; } + if (typeof row === 'number') { + row = gridRef?.current?.getRowByIndex(row); + } + if (row) { + return gridRef?.current.getRowObjectFromUID(row.getAttribute('data-uid')) || {} as IRow; + } + return {} as IRow; + }, []); + + /** + * Updates row selection state + */ + const updateRowSelection: (selectedRow: Element, rowIndex: number) => void = + useCallback((selectedRow: Element, rowIndex: number): void => { + selectedRowIndexes?.current.push(rowIndex); + selectedRecords?.current.push(selectedRow); + const rowObj: IRow = getRowObj(selectedRow); + rowObj.isSelected = true; + selectedRow.setAttribute('aria-selected', 'true'); + addRemoveSelectionClasses(selectedRow, true); + + // Dispatch custom event for toolbar refresh + const gridElement: HTMLDivElement | null | undefined = gridRef?.current?.element; + const selectionEvent: CustomEvent = new CustomEvent('selectionChanged', { + detail: { selectedRowIndexes: selectedRowIndexes?.current } + }); + gridElement?.dispatchEvent?.(selectionEvent); + }, [gridRef, selectedRowIndexes?.current, addRemoveSelectionClasses]); + + /** + * Deselects the currently selected rows. + * + * @returns {void} + */ + const clearSelection: () => void = useCallback((): void => { + if (isRowSelected?.current) { + const rows: Element[] = Array.from(gridRef?.current.getRows() || []); + const data: Object[] = []; + const row: Element[] = []; + const rowIndexes: number[] = []; + for (let i: number = 0, len: number = selectedRowIndexes?.current.length; i < len; i++) { + const currentRow: Element = rows[selectedRowIndexes?.current[parseInt(i.toString(), 10)]]; + const rowObj: IRow = getRowObj(currentRow) as IRow; + if (rowObj) { + data.push(rowObj.data); + row.push(currentRow); + rowIndexes.push(selectedRowIndexes?.current[parseInt(i.toString(), 10)]); + rowObj.isSelected = false; + } + } + const args: RowSelectingEvent = { + data: data, + rowIndexes: rowIndexes, + isCtrlPressed: isMultiCtrlRequest?.current, + isShiftPressed: isMultiShiftRequest?.current, + row: row, + target: activeTarget?.current, + cancel: false + }; + // Trigger the onRowDeselecting event + if (gridRef?.current?.onRowDeselecting) { + gridRef?.current?.onRowDeselecting(args); + if (args.cancel) { return; } // If canceled, don't proceed with deselection + } + const element: HTMLElement[] = [].slice.call((rows as Element[]).filter((record: HTMLElement) => record.hasAttribute('aria-selected'))); + for (let j: number = 0; j < element.length; j++) { + element[parseInt(j.toString(), 10)].removeAttribute('aria-selected'); + addRemoveSelectionClasses(element[parseInt(j.toString(), 10)], false); + } + selectedRowIndexes.current = []; + selectedRecords.current = []; + isRowSelected.current = false; + if (gridRef?.current?.onRowDeselect) { + const deselectedArgs: RowSelectEvent = { + data: data, + rowIndexes: rowIndexes, + row: row, + target: activeTarget?.current + }; + gridRef?.current?.onRowDeselect(deselectedArgs); + } + + // Dispatch custom event for toolbar refresh after deselection + const gridElement: HTMLDivElement | null | undefined = gridRef?.current?.element; + const selectionEvent: CustomEvent = new CustomEvent('selectionChanged', { + detail: { selectedRowIndexes: [] } + }); + gridElement?.dispatchEvent?.(selectionEvent); + } + }, [gridRef?.current, isRowSelected?.current, addRemoveSelectionClasses]); + + /** + * Deselects specific rows by their indexes. + * + * @param {number[]} indexes - Array of row indexes to deselect + * + * @returns {void} + */ + const clearRowSelection: (indexes?: number[]) => void = useCallback((indexes?: number[]): void => { + if (isRowSelected?.current) { + const data: Object[] = []; + const deSelectedRows: Element[] = []; + const rowIndexes: number[] = []; + const rows: Element[] = Array.from(gridRef?.current.getRows() || []); + const deSelectIndex: number[] = indexes ? indexes : selectedRowIndexes?.current; + for (const rowIndex of deSelectIndex) { + if (rowIndex < 0) { + continue; + } + const selectedIndex: number = selectedRowIndexes?.current.indexOf(rowIndex); + if (selectedIndex < 0) { + continue; + } + const currentRow: Element = rows[parseInt(rowIndex.toString(), 10)] as Element; + const rowObj: IRow = getRowObj(currentRow) as IRow; + + if (rowObj) { + data.push(rowObj.data); + deSelectedRows.push(currentRow); + rowIndexes.push(selectedRowIndexes?.current[parseInt(selectedIndex.toString(), 10)]); + rowObj.isSelected = false; + } + } + if (rowIndexes.length) { + const args: RowSelectingEvent = { + data: data, + rowIndexes: rowIndexes, + isCtrlPressed: isMultiCtrlRequest?.current, + isShiftPressed: isMultiShiftRequest?.current, + row: deSelectedRows, + target: activeTarget?.current, + cancel: false + }; + if (gridRef?.current?.onRowDeselecting) { + gridRef?.current?.onRowDeselecting(args); + if (args.cancel) { return; } + } + const tdElement: HTMLElement[] = [].slice.call((deSelectedRows as Element[]).filter((record: HTMLElement) => record.hasAttribute('aria-selected'))); + for (let j: number = 0; j < tdElement.length; j++) { + tdElement[parseInt(j.toString(), 10)].removeAttribute('aria-selected'); + addRemoveSelectionClasses(tdElement[parseInt(j.toString(), 10)], false); + } + const setIndexes: Set = new Set(rowIndexes); + const setRows: Set = new Set(deSelectedRows); + selectedRowIndexes.current = indexes ? selectedRowIndexes.current.filter((rowIndex: number) => + !setIndexes.has(rowIndex)) : []; + selectedRecords.current = indexes ? selectedRecords.current.filter((record: Element) => !setRows.has(record)) : []; + isRowSelected.current = selectedRowIndexes.current.length > 0; + if (gridRef?.current?.onRowDeselect) { + const deselectedArgs: RowSelectEvent = { + data: data, + rowIndexes: rowIndexes, + row: deSelectedRows, + target: activeTarget?.current + }; + gridRef?.current?.onRowDeselect(deselectedArgs); + } + const gridElement: HTMLDivElement | null | undefined = gridRef?.current?.element; + const selectionEvent: CustomEvent = new CustomEvent('selectionChanged', { + detail: { selectedRowIndexes: selectedRowIndexes?.current } + }); + gridElement?.dispatchEvent?.(selectionEvent); + } + } + }, [gridRef?.current, isRowSelected?.current, selectedRowIndexes?.current, selectedRecords?.current, + addRemoveSelectionClasses, getRowObj]); + + /** + * Gets the index of the selected row + */ + const getSelectedRowIndexes: () => number[] = useCallback((): number[] => { + return selectedRowIndexes?.current; + }, [selectedRowIndexes?.current]); + + /** + * Gets the selected row data + */ + const getSelectedRecords: () => object[] | null = useCallback((): object[] | null => { + let selectedData: Object[] = []; + if (selectedRecords?.current.length) { + selectedData = ([]>gridRef?.current.getRowsObject()).filter((row: IRow) => row.isSelected) + .map((m: IRow) => m.data); + } + return selectedData; + }, [selectedRecords?.current]); + + /** + * Gets a collection of indexes between start and end + * + * @param {number} startIndex - The starting index + * @param {number} [endIndex] - The ending index (optional) + * @returns {number[]} Array of indexes + */ + const getCollectionFromIndexes: (startIndex: number, endIndex?: number) => number[] = + useCallback((startIndex: number, endIndex?: number): number[] => { + const indexes: number[] = []; + // eslint-disable-next-line prefer-const + let { i, max }: { i: number, max: number } = (startIndex <= endIndex) ? + { i: startIndex, max: endIndex } : { i: endIndex, max: startIndex }; + for (; i <= max; i++) { + indexes.push(i); + } + if (startIndex > endIndex) { + indexes.reverse(); + } + return indexes; + }, []); + + const selectedDataUpdate: (selectedData?: Object[], selectedRows?: Element[], rowIndexes?: number[]) => void = + useCallback((selectedData?: Object[], selectedRows?: Element[], rowIndexes?: number[]): void => { + for (let i: number = 0, len: number = rowIndexes.length; i < len; i++) { + const currentRow: Element = gridRef?.current.getRows()[rowIndexes[parseInt(i.toString(), 10)]]; + const rowObj: IRow = getRowObj(currentRow) as IRow; + if (rowObj && rowObj.isDataRow) { + selectedData.push(rowObj.data); + selectedRows.push(currentRow); + } + } + }, []); + + const updateRowProps: (startIndex: number) => void = useCallback((startIndex: number): void => { + prevRowIndex.current = startIndex; + isRowSelected.current = selectedRowIndexes?.current.length && true; + }, [selectedRowIndexes?.current]); + + /** + * Selects a collection of rows by index. + * + * @param {number[]} rowIndexes - Specifies an array of row indexes. + * @returns {void} + */ + const selectRows: (rowIndexes: number[]) => void = useCallback((rowIndexes: number[]): void => { + const selectableRowIndex: number[] = [...rowIndexes]; + const rowIndex: number = gridRef?.current.selectionSettings.mode !== 'Single' ? rowIndexes[0] : rowIndexes[rowIndexes.length - 1]; + const selectedRows: Element[] = []; + const selectedData: Object[] = []; + selectedDataUpdate(selectedData, selectedRows, rowIndexes); + const selectingArgs: RowSelectingEvent = { + cancel: false, + rowIndexes: selectableRowIndex, row: selectedRows, rowIndex: rowIndex, target: activeTarget.current, + previousRow: gridRef?.current.getRows()[parseInt(prevRowIndex?.current?.toString(), 10)], + previousRowIndex: prevRowIndex?.current, isCtrlPressed: isMultiCtrlRequest?.current, + isShiftPressed: isMultiShiftRequest?.current, data: selectedData + }; + if (gridRef?.current.onRowSelecting) { + gridRef?.current.onRowSelecting(selectingArgs); + if (selectingArgs.cancel) { return; } // If canceled, don't proceed with deselection + } + clearSelection(); + if (gridRef?.current.selectionSettings.mode !== 'Single') { + for (const rowIdx of selectableRowIndex) { + updateRowSelection(gridRef?.current.getRowByIndex(rowIdx), rowIdx); + updateRowProps(rowIndex); + } + } + else { + updateRowSelection(gridRef?.current.getRowByIndex(rowIndex), rowIndex); + updateRowProps(rowIndex); + } + const selectedArgs: RowSelectEvent = { + rowIndexes: selectableRowIndex, row: selectedRows, rowIndex: rowIndex, target: activeTarget.current, + previousRow: gridRef?.current.getRows()[parseInt(prevRowIndex?.current?.toString(), 10)], + previousRowIndex: prevRowIndex?.current, data: getSelectedRecords() + }; + if (isRowSelected?.current && gridRef?.current.onRowSelect) { + gridRef?.current.onRowSelect(selectedArgs); + } + + // Dispatch custom event for toolbar refresh after multiple row selection + const gridElement: HTMLDivElement | null | undefined = gridRef?.current?.element; + const selectionEvent: CustomEvent = new CustomEvent('selectionChanged', { + detail: { selectedRowIndexes: selectedRowIndexes?.current } + }); + gridElement?.dispatchEvent?.(selectionEvent); + }, [gridRef, selectedRowIndexes?.current, selectedRecords?.current]); + + + /** + * Selects a range of rows from start and end row indexes. + * + * @param {number} startIndex - Specifies the start row index. + * @param {number} endIndex - Specifies the end row index. + * @returns {void} + */ + const selectRowByRange: (startIndex: number, endIndex?: number) => void = useCallback((startIndex: number, endIndex?: number): void => { + const indexes: number[] = getCollectionFromIndexes(startIndex, endIndex); + selectRows(indexes); + }, [getCollectionFromIndexes, selectRows]); + + /** + * Adds multiple rows to the current selection + * + * @param {number[]} rowIndexes - Array of row indexes to select + * @returns {void} + */ + const addRowsToSelection: (rowIndexes: number[]) => void = useCallback((rowIndexes: number[]): void => { + const indexes: number[] = getSelectedRowIndexes().concat(rowIndexes); + const selectedRow: Element = gridRef?.current.selectionSettings.mode !== 'Single' ? gridRef?.current.getRowByIndex(rowIndexes[0]) : + gridRef?.current.getRowByIndex(rowIndexes[rowIndexes.length - 1]); + const selectedRows: Element[] = []; + const selectedData: Object[] = []; + if (isMultiCtrlRequest?.current) { + selectedDataUpdate(selectedData, selectedRows, rowIndexes); + } + // Process each row index for multi-selection + for (const rowIndex of rowIndexes) { + const rowObj: IRow = getRowObj(rowIndex) as IRow; + const isUnSelected: boolean = selectedRowIndexes?.current.indexOf(rowIndex) > -1; + if (isUnSelected && (gridRef.current?.selectionSettings?.enableToggle || isMultiCtrlRequest?.current)) { + const rowDeselectingArgs: RowSelectingEvent = { + data: rowObj.data, + isCtrlPressed: isMultiCtrlRequest?.current, + isShiftPressed: isMultiShiftRequest?.current, + rowIndex: rowIndex, + row: selectedRow, + target: activeTarget.current, + cancel: false + }; + // Trigger the onRowDeselecting event + if (gridRef?.current.onRowDeselecting) { + gridRef?.current.onRowDeselecting(rowDeselectingArgs); + if (rowDeselectingArgs.cancel) { return; } + } + // Remove selection + selectedRowIndexes?.current.splice(selectedRowIndexes?.current.indexOf(rowIndex), 1); + selectedRecords?.current.splice(selectedRecords?.current.indexOf(selectedRow), 1); + selectedRow.removeAttribute('aria-selected'); + addRemoveSelectionClasses(selectedRow, false); + // Trigger the onRowDeselect event + if (gridRef?.current.onRowDeselect) { + const rowDeselectedArgs: RowSelectEvent = { + data: rowObj.data, + rowIndex: rowIndex, + row: selectedRow, + target: activeTarget.current + }; + gridRef?.current.onRowDeselect(rowDeselectedArgs); + } + } else if (!isUnSelected) { + // Create arguments for the selecting event + const rowSelectArgs: RowSelectingEvent = { + data: selectedData.length ? selectedData : rowObj.data, + rowIndex: rowIndex, + isCtrlPressed: isMultiCtrlRequest?.current, + isShiftPressed: isMultiShiftRequest?.current, + row: selectedRows.length ? selectedRows : selectedRow, + target: activeTarget.current, + previousRow: gridRef?.current.getRows()[parseInt(prevRowIndex?.current?.toString(), 10)], + previousRowIndex: prevRowIndex?.current, + rowIndexes: indexes, + cancel: false + }; + // Trigger the onRowSelecting event + if (gridRef?.current.onRowSelecting) { + gridRef?.current.onRowSelecting(rowSelectArgs); + if (rowSelectArgs.cancel) { return; } + } + if (gridRef?.current.selectionSettings.mode === 'Single') { + clearSelection(); + } + updateRowSelection(selectedRow, rowIndex); + // Trigger the onRowSelect event + if (gridRef?.current.onRowSelect) { + const selectedArgs: RowSelectEvent = { + data: rowSelectArgs.data, + previousRow: rowSelectArgs.previousRow, + previousRowIndex: rowSelectArgs.previousRowIndex, + row: rowSelectArgs.row, + rowIndex: rowSelectArgs.rowIndex, + rowIndexes: rowSelectArgs.rowIndexes, + target: rowSelectArgs.target + }; + gridRef?.current.onRowSelect(selectedArgs); + } + updateRowProps(rowIndex); + } + } + }, [gridRef?.current, selectedRowIndexes?.current, selectedRecords?.current, prevRowIndex?.current, + updateRowSelection, addRemoveSelectionClasses]); + + /** + * Selects a row by the given index. + * + * @param {number} rowIndex - Defines the row index. + * @param {boolean} isToggle - If set to true, then it toggles the selection. + * @returns {void} + */ + const selectRow: (rowIndex: number, isToggle?: boolean) => void = useCallback((rowIndex: number, isToggle?: boolean): void => { + if (!gridRef?.current || rowIndex < 0 || !gridRef?.current?.selectionSettings.enabled) { return; } + const selectedRow: Element = gridRef?.current.getRowByIndex(rowIndex); + const rowData: Object = gridRef?.current.currentViewData?.[parseInt(rowIndex.toString(), 10)]; + const selectData: Object = (getRowObj(rowIndex) as IRow).data; + if (gridRef?.current.selectionSettings.type !== 'Row' || !selectedRow || !rowData) { + return; + } + if (!isToggle || !selectedRowIndexes?.current.length) { + isToggle = false; + } + else { + if (gridRef?.current?.selectionSettings?.mode === 'Single' || (selectedRowIndexes?.current.length === 1 && gridRef?.current?.selectionSettings?.mode === 'Multiple')) { + selectedRowIndexes?.current.forEach((index: number) => { + isToggle = index === rowIndex ? true : false; + }); + } else { + isToggle = false; + } + } + if (!isToggle) { + const args: RowSelectingEvent = { + data: selectData, + rowIndex: rowIndex, + isCtrlPressed: isMultiCtrlRequest?.current, + isShiftPressed: isMultiShiftRequest?.current, + row: selectedRow, + previousRow: gridRef?.current.getRowByIndex(prevRowIndex?.current), + previousRowIndex: prevRowIndex?.current, + target: activeTarget.current, + cancel: false + }; + if (gridRef?.current.onRowSelecting) { + gridRef?.current.onRowSelecting(args); + if (args.cancel) { return; } + } + if (selectedRowIndexes?.current.length) { + clearSelection(); + } + updateRowSelection(selectedRow, rowIndex); + if (gridRef?.current.onRowSelect) { + const args: RowSelectEvent = { data: selectData, rowIndex: rowIndex, row: selectedRow }; + gridRef?.current.onRowSelect(args); + } + + // Dispatch custom event for toolbar refresh after single row selection + const gridElement: HTMLDivElement | null | undefined = gridRef?.current?.element; + const selectionEvent: CustomEvent = new CustomEvent('selectionChanged', { + detail: { selectedRowIndexes: selectedRowIndexes?.current } + }); + gridElement?.dispatchEvent?.(selectionEvent); + } else { + const isRowSelected: boolean = selectedRow.hasAttribute('aria-selected'); + if (isRowSelected) { + clearSelection(); + } else { + updateRowSelection(selectedRow, rowIndex); + } + } + updateRowProps(rowIndex); + }, [gridRef, updateRowSelection, addRemoveSelectionClasses, updateRowProps, selectedRowIndexes?.current]); + + const rowCellSelectionHandler: (rowIndex: number) => void = useCallback((rowIndex: number): void => { + if ((!isMultiCtrlRequest?.current && !isMultiShiftRequest?.current) || gridRef?.current.selectionSettings.mode === 'Single') { + selectRow(rowIndex, gridRef?.current?.selectionSettings?.enableToggle); + } else if (isMultiShiftRequest?.current) { + if (!closest(activeTarget.current, '.sf-rowcell').classList.contains('sf-chkbox')) { + selectRowByRange(isNullOrUndefined(prevRowIndex?.current) ? rowIndex : prevRowIndex?.current, rowIndex); + } else { + addRowsToSelection([rowIndex]); + } + } else { + addRowsToSelection([rowIndex]); + } + }, [gridRef, selectRow, addRowsToSelection, selectRowByRange]); + + /** + * Handle grid-level click event + * + * @returns {void} + */ + const handleGridClick: (event: React.MouseEvent) => void = useCallback((event: React.MouseEvent): void => { + activeTarget.current = event.target as Element; + isMultiShiftRequest.current = event.shiftKey; + isMultiCtrlRequest.current = event.ctrlKey; + const target: Element = !activeTarget.current?.classList.contains('sf-rowcell') ? + activeTarget.current?.closest('.sf-rowcell') : activeTarget.current; + if (gridRef?.current?.selectionSettings.enabled && target && target.parentElement.classList.contains('sf-row')) { + const rowIndex: number = parseInt(target.parentElement.getAttribute('aria-rowindex'), 10) - 1; + rowCellSelectionHandler(rowIndex); + } + isMultiCtrlRequest.current = false; + isMultiShiftRequest.current = false; + }, [gridRef]); + + const shiftDownUpKey: (rowIndex?: number) => void = (rowIndex?: number): void => { + selectRowByRange(prevRowIndex.current, rowIndex); + }; + + const ctrlPlusA: () => void = (): void => { + if (gridRef?.current?.selectionSettings?.mode === 'Multiple' && gridRef?.current.selectionSettings.type === 'Row') { + const rowObj: IRow[] = gridRef?.current?.getRowsObject(); + selectRowByRange(rowObj[0].index, rowObj[rowObj.length - 1].index); + } + }; + + const onCellFocus: (e: CellFocusEvent) => void = (e: CellFocusEvent): void => { + const isHeader: boolean = (e.container as {isHeader?: boolean}).isHeader; + const clear: boolean = isHeader && e.isJump; + const headerAction: boolean = isHeader && e.byKey; + if (!e.byKey || clear || !gridRef?.current?.selectionSettings.enabled) { + if (clear) { + clearSelection(); + } + return; + } + const action: string = gridRef.current.focusModule.getNavigationDirection(e.keyArgs as KeyboardEvent); + if (headerAction || ((action === 'shiftEnter' || action === 'enter') && e.rowIndex === prevRowIndex.current)) { + return; + } + switch (action) { + case 'space': + selectRow(e.rowIndex, true); + break; + case 'shiftDown': + case 'shiftUp': + shiftDownUpKey(e.rowIndex); + break; + case 'escape': + clearSelection(); + break; + case 'ctrlPlusA': + ctrlPlusA(); + break; + } + }; + + return { + clearSelection, + clearRowSelection, + selectRow, + getSelectedRowIndexes, + getSelectedRecords, + handleGridClick, + selectRows, + selectRowByRange, + addRowsToSelection, + onCellFocus, + get selectedRowIndexes(): number[] { return selectedRowIndexes.current; }, + get selectedRecords(): Object[] { return selectedRecords.current; }, + get activeTarget(): Element | null { return activeTarget.current; } + }; +}; diff --git a/components/grids/src/grid/hooks/useSort.ts b/components/grids/src/grid/hooks/useSort.ts new file mode 100644 index 0000000..38f86e5 --- /dev/null +++ b/components/grids/src/grid/hooks/useSort.ts @@ -0,0 +1,234 @@ +import { useCallback, RefObject, useEffect, useState } from 'react'; +import { SortDirection } from '../types'; +import { SortSettings, SortDescriptorModel, SortEvent, SortAPI } from '../types/sort.interfaces'; +import { ColumnProps } from '../types/column.interfaces'; +import { closest, isNullOrUndefined} from '@syncfusion/react-base'; +import { getActualPropFromColl } from '../utils'; +import { GridRef } from '../types/grid.interfaces'; +import { SortProperties } from '../types/interfaces'; + +/** + * Custom hook to manage sort state and configuration + * + * @private + * @param {RefObject} gridRef - Reference to the grid component + * @param {SortSettings} sortSetting - Reference to the sort settings + * @param {Function} setGridAction - Function to update grid actions + * @returns {SortProperties} An object containing various sort-related state and API + */ +export const useSort: (gridRef?: RefObject, sortSetting?: SortSettings, + setGridAction?: (action: Object) => void) => SortAPI = (gridRef?: RefObject, sortSetting?: SortSettings, + setGridAction?: (action: Object) => void) => { + + const [sortSettings, setSortSettings] = useState(sortSetting); + const getSortProperties: SortProperties = { + currentTarget: null, + isMultiSort: false, + sortSettings: { columns: [] }, + contentRefresh: true, + sortedColumns: [] + }; + + /** + * update sortSettings properties sortModule + */ + useEffect(() => { + setSortSettings(sortSetting); + }, [sortSetting]); + + /** + * Initialize sort Column when row or column count changes + */ + useEffect(() => { + if (gridRef.current.getColumns() && sortSettings?.columns?.length) { + getSortProperties.contentRefresh = false; + getSortProperties.isMultiSort = gridRef.current.sortSettings?.columns.length > 1; + for (const col of gridRef.current.sortSettings?.columns.slice()) { + sortByColumn(col.field, col.direction, getSortProperties.isMultiSort); + } + getSortProperties.isMultiSort = false; + getSortProperties.contentRefresh = true; + } + }, []); + + /** + * Handle grid-level click event + */ + const handleGridClick: (event: React.MouseEvent) => void = useCallback((event: React.MouseEvent): void => { + if (!gridRef.current?.sortSettings?.enabled) { return; } + const target: Element = closest(event.target as Element, '.sf-headercell'); + if (target && !(event.target as Element).classList.contains('sf-grptogglebtn')) { + const colObj: ColumnProps = gridRef.current.columns.find((col: ColumnProps) => col.uid === target.querySelector('.sf-headercelldiv').getAttribute('data-mappinguid')); + if (colObj.type !== 'checkbox') { + initiateSort(target, event, colObj); + } + } + }, [gridRef, sortSetting]); + + /** + * Handle grid-level key-press event + */ + const keyUpHandler: (e: React.KeyboardEvent) => void = useCallback((e: React.KeyboardEvent): void => { + const ele: Element = e.target as Element; + if (((e.keyCode === 13 && e.ctrlKey) || (e.keyCode === 13 && e.shiftKey) || e.keyCode === 13) + && closest(ele as Element, '.sf-headercell')) { + const target: Element = ele; + if (isNullOrUndefined(target) || !target.classList.contains('sf-headercell') + || !target.querySelector('.sf-headercelldiv')) { return; } + const colObj: ColumnProps = gridRef.current.columns.find((col: ColumnProps) => col.uid === target.querySelector('.sf-headercelldiv').getAttribute('data-mappinguid')); + initiateSort(target, e, colObj); + } + }, [gridRef]); + + /** + * Sorts a column with the given options. + * + * @param {string} field - Defines the column field to be sorted. + * + * @private + * @returns {void} + */ + const removeSortColumn: (field: string) => void = async(field: string): Promise => { + const cols: SortDescriptorModel[] = getSortProperties.sortSettings?.columns; + if (cols.length === 0 && getSortProperties.sortedColumns.indexOf(field) < 0) { + return; } + const args: SortEvent = { cancel: false, requestType: 'clearSorting', target: getSortProperties.currentTarget }; + args.type = 'sorting'; + const confirmResult: boolean = await gridRef.current?.editModule?.checkUnsavedChanges?.(); + if (!isNullOrUndefined(confirmResult) && !confirmResult) { + return; + } + gridRef.current.onSortStart?.(args); + if (args.cancel) { + return; + } + for (let i: number = 0, len: number = cols.length; i < len; i++) { + if (cols[parseInt(i.toString(), 10)].field === field) { + cols.splice(i, 1); + getSortProperties.sortSettings.columns = cols; + setSortSettings((prev: SortSettings) => + ({ ...prev, columns: cols, allowUnsort: gridRef.current.sortSettings?.allowUnsort })); + args.type = 'actionComplete'; + setGridAction(args); + break; + } + } + }; + + const initiateSort: (target: Element, e: React.MouseEvent | React.KeyboardEvent, column: ColumnProps) => void = ( + target: Element, e: React.MouseEvent | React.KeyboardEvent, column: ColumnProps): void => { + if (column.allowSort === false) { return; } + const field: string = column.field; + getSortProperties.currentTarget = e.target as Element; + const direction: SortDirection | string = !target.getElementsByClassName('sf-ascending').length ? 'Ascending' : + 'Descending'; + getSortProperties.isMultiSort = e.ctrlKey; + if (!(gridRef.current?.sortSettings?.mode === 'multiple')) { + getSortProperties.isMultiSort = false; + } + if (e.shiftKey || (gridRef.current.sortSettings?.allowUnsort && target.getElementsByClassName('sf-descending').length)) { + removeSortColumn(field); + } else { + sortByColumn(field, direction, getSortProperties.isMultiSort as boolean); + } + }; + + + /** + * Sorts a column with the given options. + * + * @param {string} field - Defines the column name to be sorted. + * @param {SortDirection | string} direction - Defines the direction of sorting field. + * @param {boolean} isMultiSort - Specifies whether the previous sorted columns are to be maintained. + * + * @returns {void} + */ + const sortByColumn: (field: string, direction: SortDirection | string, isMultiSort: boolean) => void = async( + field: string, direction: SortDirection | string, isMultiSort: boolean): Promise => { + const column: ColumnProps = gridRef.current.columns.find((col: ColumnProps) => col.field === field ); + if (column.allowSort === false || gridRef.current?.sortSettings?.enabled === false) { return; } + const sortedColumn: SortDescriptorModel = { field: field, direction: direction }; + let index: number; + if (gridRef.current.sortSettings?.columns?.length) { + getSortProperties.sortSettings.columns = gridRef.current.sortSettings?.columns; + } + const args: SortEvent = { + cancel: false, field: field, direction: direction, requestType: 'sorting', + target: getSortProperties.currentTarget }; + if (getSortProperties.contentRefresh) { + args.type = 'sorting'; + const confirmResult: boolean = await gridRef.current?.editModule?.checkUnsavedChanges?.(); + if (!isNullOrUndefined(confirmResult) && !confirmResult) { + return; + } + gridRef.current.onSortStart?.(args); + if (args.cancel) { + return; + } + } + updateSortedCols(field, isMultiSort); + if (!isMultiSort) { + getSortProperties.sortSettings.columns = [sortedColumn]; + column.sortDirection = direction; + if (getSortProperties.contentRefresh) { + setSortSettings((prev: SortSettings) => + ({ ...prev, + columns: getSortProperties.sortSettings?.columns, + allowUnsort: gridRef.current.sortSettings?.allowUnsort + })); + args.type = 'actionComplete'; + setGridAction(args); + } + } else { + index = getSortedColsIndexByField(field); + if (index > -1) { + getSortProperties.sortSettings?.columns?.splice(index, 1); + } + column.sortDirection = direction; + getSortProperties.sortSettings?.columns.push(sortedColumn); + if (getSortProperties.contentRefresh) { + setSortSettings((prev: SortSettings) => + ({ ...prev, + columns: getSortProperties.sortSettings?.columns, + allowUnsort: gridRef.current.sortSettings?.allowUnsort + })); + args.type = 'actionComplete'; + setGridAction(args); + } + } + }; + + const updateSortedCols: (columnName: string, isMultiSort: boolean) => void = (columnName: string, isMultiSort: boolean): void => { + if (!isMultiSort) { + getSortProperties.sortedColumns.splice(0, getSortProperties.sortedColumns.length); + } + if (getSortProperties.sortedColumns.indexOf(columnName) < 0) { + getSortProperties.sortedColumns.push(columnName); + } + }; + + const getSortedColsIndexByField: (field: string, sortedColumns?: SortDescriptorModel[]) => number = useCallback( + (field: string, sortedColumns?: SortDescriptorModel[]): number => { + const cols: SortDescriptorModel[] = sortedColumns ? sortedColumns : gridRef.current.sortSettings?.columns; + for (let i: number = 0, len: number = cols.length; i < len; i++) { + if (cols[parseInt(i.toString(), 10)].field === field) { + return i; + } + } + return -1; + }, []); + + /** + * Clears all the sorted columns of the Grid. + * + * @returns {void} + */ + const clearSort: () => void = useCallback((): void => { + const cols: SortDescriptorModel[] = getActualPropFromColl(gridRef.current.sortSettings?.columns); + for (let i: number = 0, len: number = cols.length; i < len; i++) { + removeSortColumn(cols[parseInt(i.toString(), 10)].field); + } + }, []); + + return { removeSortColumn, sortByColumn, clearSort, handleGridClick, keyUpHandler, sortSettings, setSortSettings }; +}; diff --git a/components/grids/src/grid/hooks/useToolbar.ts b/components/grids/src/grid/hooks/useToolbar.ts new file mode 100644 index 0000000..18e483d --- /dev/null +++ b/components/grids/src/grid/hooks/useToolbar.ts @@ -0,0 +1,288 @@ +import { useState, useCallback, useRef, useEffect } from 'react'; +import { + IToolbar +} from '@syncfusion/react-navigations'; +import { SelectionModel } from '../types/selection.interfaces'; +import { editModule } from '../types/edit.interfaces'; +import * as React from 'react'; +import { ToolbarConfig, ToolbarAPI, ToolbarClickEvent } from '../types/toolbar.interfaces'; + +/** + * Custom hook to manage toolbar operations for the grid + * + * @private + * @param {ToolbarConfig} config - Toolbar configuration + * @param {editModule} editModule - Edit module reference (passed directly to avoid context issues) + * @param {SelectionModel} selectionModule - Selection module reference (passed directly to avoid context issues) + * @param {Object[]} currentViewData - Current view data (passed directly to avoid context issues) + * @param {boolean} allowSearching - Enable or disable the searching UI + * @returns {ToolbarAPI} Toolbar API methods and state + */ +export const useToolbar: (config: ToolbarConfig, editModule?: editModule, selectionModule?: SelectionModel, + currentViewData?: Object[], allowSearching?: boolean) => ToolbarAPI = ( + config: ToolbarConfig, + editModule?: editModule, + selectionModule?: SelectionModel, + currentViewData?: Object[], + allowSearching?: boolean +): ToolbarAPI => { + // Always call hooks in the same order - no conditional hooks + const [isRendered, setIsRendered] = useState(false); + // Track disabled state using React state instead of DOM manipulation + const [disabledItemsState, setDisabledItemsState] = useState>(new Set()); + const toolbarRef: React.RefObject = useRef(null); + const editModuleRef: React.RefObject = useRef(editModule); + const selectionModuleRef: React.RefObject = useRef(selectionModule); + const currentViewDataRef: React.RefObject = useRef(currentViewData); + + const { + gridId, + onToolbarItemClick + } = config; + + // Update refs without causing re-renders + editModuleRef.current = editModule; + selectionModuleRef.current = selectionModule; + currentViewDataRef.current = currentViewData; + + /** + * Gets the toolbar element + * + * @returns {HTMLElement | null} The toolbar element + */ + const getToolbar: () => HTMLElement | null = useCallback((): HTMLElement | null => { + return toolbarRef.current?.element; + }, []); + + /** + * React-compliant enableItems function that updates state instead of DOM + */ + const enableItems: (items: string[], isEnable: boolean) => void = useCallback((items: string[], isEnable: boolean): void => { + setDisabledItemsState((prevDisabled: Set) => { + const newDisabled: Set = new Set(prevDisabled); + items.forEach((itemId: string) => { + if (isEnable) { + newDisabled.delete(itemId); + } else { + newDisabled.add(itemId); + } + }); + return newDisabled; + }); + }, []); + + const refreshToolbarItems: () => void = useCallback((): void => { + const editMod: editModule = editModuleRef.current; + const selectionMod: SelectionModel = selectionModuleRef.current; + const viewData: Object[] = currentViewDataRef.current; + + if (!editMod) { + return; + } + + const enableItemsList: string[] = []; + const disableItemsList: string[] = []; + const { editSettings } = editMod; + + const selectedRowIndexes: number[] = selectionMod?.selectedRowIndexes; + const hasData: boolean = (viewData && viewData.length > 0); + const hasSelection: boolean = selectedRowIndexes.length > 0; + + const gridElement: HTMLElement | null = toolbarRef.current?.element?.closest('.sf-grid'); + const addRow: boolean = editSettings?.showAddNewRow && !gridElement?.querySelector('.sf-editedrow'); + + if (editSettings?.allowAdd) { + enableItemsList.push(`${gridId}_add`); + } else { + disableItemsList.push(`${gridId}_add`); + } + + if (editSettings?.allowEdit && hasData) { + enableItemsList.push(`${gridId}_edit`); + } else { + disableItemsList.push(`${gridId}_edit`); + } + + if (editSettings.allowDelete && hasData) { + enableItemsList.push(`${gridId}_delete`); + } else { + disableItemsList.push(`${gridId}_delete`); + } + + if (allowSearching) { + enableItemsList.push(`${gridId}_search`); + } else { + disableItemsList.push(`${gridId}_search`); + } + + if ((editMod.isEdit || editSettings.showAddNewRow) && (editSettings.allowAdd || editSettings.allowEdit)) { + if (addRow) { + const itemsToEnable: string[] = [`${gridId}_update`, `${gridId}_cancel`, `${gridId}_edit`, `${gridId}_delete`, `${gridId}_search`]; + const itemsToDisable: string[] = [`${gridId}_add`]; + + itemsToEnable.forEach((item: string) => { + if (!enableItemsList.includes(item)) { + enableItemsList.push(item); + } + }); + itemsToDisable.forEach((item: string) => { + const index: number = enableItemsList.indexOf(item); + if (index > -1) { + enableItemsList.splice(index, 1); + } + if (!disableItemsList.includes(item)) { + disableItemsList.push(item); + } + }); + } else { + // Normal edit mode or showAddNewRow with edited row + // When editing an existing row with showAddNewRow enabled, the add new row should be disabled + const itemsToEnable: string[] = [`${gridId}_update`, `${gridId}_cancel`, `${gridId}_search`]; + const itemsToDisable: string[] = [`${gridId}_add`, `${gridId}_edit`, `${gridId}_delete`]; + + itemsToEnable.forEach((item: string) => { + if (!enableItemsList.includes(item)) { + enableItemsList.push(item); + } + }); + itemsToDisable.forEach((item: string) => { + const index: number = enableItemsList.indexOf(item); + if (index > -1) { + enableItemsList.splice(index, 1); + } + if (!disableItemsList.includes(item)) { + disableItemsList.push(item); + } + }); + } + } else { + // Not in edit mode - disable Update/Cancel + disableItemsList.push(`${gridId}_update`, `${gridId}_cancel`); + } + + // Apply selection-based logic for Edit/Delete buttons + // This ensures Edit/Delete are only enabled when rows are selected + if (!hasSelection) { + // Remove Edit/Delete from enable list if no selection + const editIndex: number = enableItemsList.indexOf(`${gridId}_edit`); + const deleteIndex: number = enableItemsList.indexOf(`${gridId}_delete`); + + if (editIndex > -1) { + enableItemsList.splice(editIndex, 1); + if (!disableItemsList.includes(`${gridId}_edit`)) { + disableItemsList.push(`${gridId}_edit`); + } + } + if (deleteIndex > -1) { + enableItemsList.splice(deleteIndex, 1); + if (!disableItemsList.includes(`${gridId}_delete`)) { + disableItemsList.push(`${gridId}_delete`); + } + } + } + + // When editing an existing row with showAddNewRow enabled, the add new row inputs should be disabled + if (editSettings.showAddNewRow && editMod.isEdit && !addRow) { + // The add new row should remain visible but with disabled inputs + disableShowAddNewRowInputs(false); + } else if (editSettings.showAddNewRow && (!editMod.isEdit || addRow)) { + // The add new row inputs should be re-enabled + disableShowAddNewRowInputs(true); + } + + // Apply enable/disable states + enableItems(enableItemsList, true); + enableItems(disableItemsList, false); + }, [gridId, enableItems]); + + const disableShowAddNewRowInputs: (enable: boolean) => void = useCallback((enable: boolean): void => { + const gridElement: HTMLElement | null = toolbarRef.current?.element?.closest('.sf-grid'); + const addRow: HTMLElement | null = gridElement?.querySelector('.sf-addedrow'); + if (!addRow) { + return; + } + + // Handle input elements in the add new row + const inputs: NodeListOf = addRow.querySelectorAll('.sf-input'); + inputs.forEach((input: Element) => { + const inputElement: HTMLInputElement = input as HTMLInputElement; + + // Enable/disable the input + if (enable) { + inputElement.classList.remove('sf-disabled'); + inputElement.removeAttribute('disabled'); + } else { + inputElement.classList.add('sf-disabled'); + inputElement.setAttribute('disabled', 'disabled'); + } + }); + }, []); + + const handleToolbarClick: (args: ToolbarClickEvent) => void = useCallback((args: ToolbarClickEvent): void => { + const editMod: editModule = editModuleRef.current; + + if (!args.item || !editMod) { + return; + } + + const itemId: string = args.item.id; + + const extendedArgs: ToolbarClickEvent = { + ...args, + cancel: false + }; + + // Trigger custom event handler first + onToolbarItemClick?.(extendedArgs); + if (extendedArgs.cancel) { + return; + } + + switch (itemId) { + case `${gridId}_add`: + editMod?.addRecord(); + break; + + case `${gridId}_edit`: + editMod?.editRow(); + break; + + case `${gridId}_update`: + editMod?.saveChanges(); + break; + + case `${gridId}_cancel`: + editMod?.cancelChanges(); + break; + + case `${gridId}_delete`: + editMod?.deleteRecord(); + break; + + case `${gridId}_search`: + break; + } + + // Only refresh after actual actions, not on every click + // Use a small delay to ensure the action has completed + setTimeout(() => refreshToolbarItems(), 50); + }, [gridId, onToolbarItemClick, refreshToolbarItems]); + + // Only set rendered state once, no other effects + useEffect(() => { + setIsRendered(true); + }, []); + + return { + getToolbar, + enableItems, + refreshToolbarItems, + handleToolbarClick, + isRendered, + activeItems: new Set(), // Always return empty set to prevent re-renders + disabledItems: disabledItemsState, // Expose the disabled items state + toolbarRef + }; +}; + +export default useToolbar; diff --git a/components/grids/src/grid/index.ts b/components/grids/src/grid/index.ts new file mode 100644 index 0000000..a143da1 --- /dev/null +++ b/components/grids/src/grid/index.ts @@ -0,0 +1,8 @@ +export * from './hooks'; +export * from './contexts'; +export * from './components'; +export * from './types'; +export * from './views'; +export * from './services'; +export * from './utils'; +export * from './models'; diff --git a/components/grids/src/grid/models/index.ts b/components/grids/src/grid/models/index.ts new file mode 100644 index 0000000..0da5d8b --- /dev/null +++ b/components/grids/src/grid/models/index.ts @@ -0,0 +1 @@ +export * from './useData'; diff --git a/components/grids/src/grid/models/useData.ts b/components/grids/src/grid/models/useData.ts new file mode 100644 index 0000000..e782261 --- /dev/null +++ b/components/grids/src/grid/models/useData.ts @@ -0,0 +1,380 @@ +import { RefObject, useCallback, useMemo } from 'react'; +import { + Action, DataChangeRequestEvent, DataRequestEvent, + MutableGridBase, PendingState +} from '../types'; +import { SortDescriptorModel } from '../types/sort.interfaces'; +import { GridActionEvent, IGrid } from '../types/grid.interfaces'; +import { FilterPredicates } from '../types/filter.interfaces'; +import { ColumnProps } from '../types/column.interfaces'; +import { AggregateColumnProps, AggregateRowProps } from '../types/aggregate.interfaces'; +import { SearchSettings } from '../types/search.interfaces'; +import { MutableGridSetter, UseDataResult } from '../types/interfaces'; +import { extend, isNullOrUndefined} from '@syncfusion/react-base'; +import { AdaptorOptions, DataManager, DataUtil, Predicate, Query, DataResult, Deferred, UrlAdaptor } from '@syncfusion/react-data'; +import { getCaseValue, getDatePredicate } from '../utils'; + +/** + * Custom hook to manage data operations for the grid + * + * @param {Object} [gridInstance] Grid instance reference + * @param {Object} [gridAction] Grid action object (for dispatching actions) + * @param {RefObject} [dataState] - Data state object properties + * @returns {UseDataResult} Object containing APIs for data operations + * @private + */ +export const useData: (gridInstance?: Partial & Partial, gridAction?: Object, + dataState?: RefObject) => UseDataResult = ( + gridInstance?: Partial & Partial, gridAction?: Object, dataState?: RefObject +): UseDataResult => { + const grid: Partial & Partial & Partial = gridInstance; + const dataManager: DataManager | DataResult = useMemo(() => { + const gridDataSource: DataManager | DataResult = grid.dataSource as DataManager | DataResult; + return gridDataSource; + }, [grid?.dataSource]); + + /** + * Check if the data source is remote + */ + const isRemote: () => boolean = useCallback((): boolean => { + return dataManager instanceof DataManager && dataManager.dataSource && + typeof dataManager.dataSource === 'object' && + 'offline' in dataManager.dataSource && + 'url' in dataManager.dataSource && + dataManager.dataSource.offline !== true && + dataManager.dataSource.url !== undefined && + dataManager.dataSource.url !== ''; + }, [dataManager]); + + /** + * The function is used to generate updated Query from Grid model. + * + * @returns {Query} returns the Query + * @private + */ + const generateQuery: () => Query = useCallback((): Query => { + const query: Query = grid?.query?.clone(); + sortQuery(query); + filterQuery(query); + searchQuery(query); + aggregateQuery(query); + pageQuery(query); + return query; + }, [grid]); + + const sortQuery: (query: Query) => Query = useCallback((query: Query): Query => { + if ((grid?.sortSettings?.enabled && grid.sortSettings?.columns?.length)) { + const columns: SortDescriptorModel[] = grid.sortSettings?.columns; + for (let i: number = columns.length - 1; i > -1; i--) { + const col: ColumnProps = grid.columns.find((c: ColumnProps) => c.field === columns[parseInt(i.toString(), 10)].field); + col.sortDirection = columns[parseInt(i.toString(), 10)].direction; + let fn: Function | string = columns[parseInt(i.toString(), 10)].direction; + if (col.onSortComparer) { + fn = !isRemote() ? (col.onSortComparer as Function).bind(col) : columns[parseInt(i.toString(), 10)].direction; + } + if (col.onSortComparer) { + query.sortByForeignKey(col.field, fn, undefined, columns[parseInt(i.toString(), 10)].direction.toLowerCase()); + } else { + query.sortBy(col.field, fn); + } + } + } + return query; + }, [grid?.sortSettings, grid?.sortSettings?.enabled]); + + const grabColumnByFieldFromAllCols: (field: string) => ColumnProps = (field: string): ColumnProps => { + let column: ColumnProps; + const gCols: ColumnProps[] = grid.columns; + for (let i: number = 0; i < gCols?.length; i++) { + if (field === gCols[parseInt(i.toString(), 10)].field) { + column = gCols[parseInt(i.toString(), 10)]; + break; + } + } + return column; + }; + + const filterQuery: (query: Query) => Query = useCallback((query: Query): Query => { + const predicateList: Predicate[] = []; + if (grid.filterSettings?.enabled && grid.filterSettings?.columns?.length) { + const gObj: Partial & Partial = grid; + const columns: FilterPredicates[] = grid.filterSettings?.columns; + const colType: Object = {}; + for (const col of gObj.columns as ColumnProps[]) { + colType[col.field] = grid?.filterSettings?.type; + } + const defaultFltrCols: FilterPredicates[] = []; + for (const col of columns) { + const gridColumn: ColumnProps = gObj.columns.find((c: ColumnProps) => c.field === col.field); + if (isNullOrUndefined(col.type) && gridColumn && (gridColumn.type === 'dateonly' || gridColumn.type === 'datetime' || gridColumn.type === 'date')) { + col.type = (gObj.columns.find((c: ColumnProps) => c.field === col.field)).type; + } + defaultFltrCols.push(col); + } + for (let i: number = 0, len: number = defaultFltrCols.length; i < len; i++) { + const columnFromField: ColumnProps = grabColumnByFieldFromAllCols(defaultFltrCols[parseInt(i.toString(), 10)].field); + defaultFltrCols[parseInt(i.toString(), 10)].uid = defaultFltrCols[parseInt(i.toString(), 10)].uid || + columnFromField?.uid; + } + const excelPredicate: Predicate = getPredicate(defaultFltrCols); + for (const prop of Object.keys(excelPredicate)) { + predicateList.push(excelPredicate[`${prop}`]); + } + query.where(Predicate.and(predicateList)); + } + return query; + }, [grid.filterSettings, grid.filterSettings.enabled]); + + const searchQuery: (query: Query) => Query = useCallback((query: Query): Query => { + const predicateList: Predicate[] = []; + if (grid.searchSettings?.enabled && !isNullOrUndefined(grid.searchSettings?.value) && grid.searchSettings?.value.length) { + const sSettings: SearchSettings = grid.searchSettings; + const fields: string[] = (!isNullOrUndefined(sSettings.fields) && sSettings.fields.length) ? sSettings.fields + : getSearchColumnFieldNames(); + const dataManager: DataManager = grid?.dataSource instanceof DataManager ? grid.dataSource : + new DataManager(grid?.dataSource as DataManager); + const adaptor: AdaptorOptions = dataManager.adaptor; + const adaptorWithMethod: { getModuleName?: Function } = adaptor as { getModuleName?: Function }; + if (adaptorWithMethod.getModuleName && + adaptorWithMethod.getModuleName() === 'ODataV4Adaptor') { + for (let i: number = 0; i < fields.length; i++) { + predicateList.push(new Predicate( + fields[parseInt(i.toString(), 10)], sSettings.operator, grid.searchSettings?.value, + sSettings.caseSensitive, sSettings.ignoreAccent + )); + } + const predList: Predicate = Predicate.or(predicateList); + predList.key = grid.searchSettings?.value; + query.where(predList); + } else { + query.search(grid.searchSettings?.value, fields, sSettings.operator, sSettings.caseSensitive, sSettings.ignoreAccent); + } + } + return query; + }, [grid.searchSettings, grid.searchSettings?.enabled]); + + const aggregateQuery: (query: Query) => Query = useCallback((query: Query): Query => { + const rows: AggregateRowProps[] = grid?.aggregates; + for (let i: number = 0; rows && i < rows.length; i++) { + const row: AggregateRowProps = rows[parseInt(i.toString(), 10)]; + for (let j: number = 0; j < row.columns.length; j++) { + const cols: AggregateColumnProps = row.columns[parseInt(j.toString(), 10)]; + const types: string[] = cols.type instanceof Array ? cols.type : [cols.type]; + for (let k: number = 0; k < types.length; k++) { + query.aggregate(types[parseInt(k.toString(), 10)].toLowerCase(), cols.field); + } + } + } + return query; + }, [grid?.aggregates]); + + const pageQuery: (query: Query) => Query = useCallback((query: Query): Query => { + const fName: string = 'fn'; + if (grid.pageSettings?.enabled) { + const currentPageVal: number = Math.max(1, grid?.currentPage); + if (grid.pageSettings.pageCount <= 0) { + grid.pageSettings.pageCount = 8; + } + if (grid.pageSettings.pageSize <= 0) { + grid.pageSettings.pageSize = 12; + } + if (query.queries.length) { + for (let i: number = 0; i < query.queries.length; i++) { + if (query.queries[parseInt(i.toString(), 10)][`${fName}`] === 'onPage') { + query.queries.splice(i, 1); + } + } + } + query.page(currentPageVal, grid.pageSettings.pageSize); + } + return query; + }, [grid.pageSettings, grid?.currentPage, grid.pageSettings?.enabled]); + + const getSearchColumnFieldNames: () => string[] = (): string[] => { + const colFieldNames: string[] = []; + const columns: ColumnProps[] = grid.columns; + for (const col of columns) { + if (col.allowSearch && !isNullOrUndefined(col.field)) { + colFieldNames.push(col.field); + } + } + return colFieldNames; + }; + + const getPredicate: (columns: FilterPredicates[], isExecuteLocal?: boolean) => Predicate = (columns: FilterPredicates[], + isExecuteLocal?: boolean): Predicate => { + + const cols: FilterPredicates[] = DataUtil.distinct(columns, 'field', true); + let collection: Object[] = []; + const pred: Predicate = {} as Predicate; + for (let i: number = 0; i < cols.length; i++) { + collection = new DataManager(columns as JSON[]).executeLocal( + new Query().where('field', 'equal', cols[parseInt(i.toString(), 10)].field)); + pred[cols[parseInt(i.toString(), 10)].field] = generatePredicate(collection, isExecuteLocal); + } + return pred; + }; + + const generatePredicate: (cols: FilterPredicates[], isExecuteLocal?: boolean) => Predicate = ( + cols: FilterPredicates[], isExecuteLocal?: boolean): Predicate => { + const len: number = cols.length; + let predicate: Predicate; + const operate: string = 'or'; + const first: FilterPredicates = cols[0]; + first.ignoreAccent = !isNullOrUndefined(first.ignoreAccent) ? first.ignoreAccent : false; + if (first.type === 'date' || first.type === 'datetime' || first.type === 'dateonly') { + predicate = getDatePredicate(first, first.type, isExecuteLocal); + } else { + predicate = first.ejpredicate ? first.ejpredicate as Predicate : + new Predicate( + first.field, first.operator, first.value, !getCaseValue(first), + first.ignoreAccent) as Predicate; + } + for (let p: number = 1; p < len; p++) { + if (len > 2 && p > 1 && ((cols[p as number].predicate === 'or' && cols[p as number - 1].predicate === 'or') + || (cols[p as number].predicate === 'and' && cols[p as number - 1].predicate === 'and'))) { + if (cols[p as number].type === 'date' || cols[p as number].type === 'datetime' || cols[p as number].type === 'dateonly') { + predicate.predicates.push( + getDatePredicate(cols[parseInt(p.toString(), 10)], cols[p as number].type, isExecuteLocal)); + } else { + predicate.predicates.push(new Predicate( + cols[p as number].field, cols[parseInt(p.toString(), 10)].operator, + cols[parseInt(p.toString(), 10)].value, !getCaseValue(cols[parseInt(p.toString(), 10)]), + cols[parseInt(p.toString(), 10)].ignoreAccent)); + } + } else { + if (cols[p as number].type === 'date' || cols[p as number].type === 'datetime' || cols[p as number].type === 'dateonly') { + if (cols[parseInt(p.toString(), 10)].predicate === 'and' && cols[parseInt(p.toString(), 10)].operator === 'equal') { + predicate = (predicate[`${operate}`] as Function)( + getDatePredicate(cols[parseInt(p.toString(), 10)], cols[parseInt(p.toString(), 10)].type, isExecuteLocal), + cols[parseInt(p.toString(), 10)].type, cols[parseInt(p.toString(), 10)].ignoreAccent); + } else { + predicate = (predicate[((cols[parseInt(p.toString(), 10)] as Predicate).predicate) as string] as Function)( + getDatePredicate(cols[parseInt(p.toString(), 10)], cols[parseInt(p.toString(), 10)].type, isExecuteLocal), + cols[parseInt(p.toString(), 10)].type, cols[parseInt(p.toString(), 10)].ignoreAccent); + } + } else { + predicate = cols[parseInt(p.toString(), 10)].ejpredicate ? + (predicate[(cols[parseInt(p.toString(), 10)] as Predicate) + .predicate as string] as Function)(cols[parseInt(p.toString(), 10)].ejpredicate) : + (predicate[(cols[parseInt(p.toString(), 10)].predicate) as string] as Function)( + cols[parseInt(p.toString(), 10)].field, cols[parseInt(p.toString(), 10)].operator, + cols[parseInt(p.toString(), 10)].value, !getCaseValue(cols[parseInt(p.toString(), 10)]), + cols[parseInt(p.toString(), 10)].ignoreAccent); + } + } + } + return predicate; + }; + + const getData: (args?: { requestType?: string; data?: Object; index?: number }, query?: Query) => Promise = useCallback(async ( + args: { requestType?: string; data?: Object; index?: number } = { requestType: '' }, + query?: Query + ): Promise => { + const currentQuery: Query = query || generateQuery().requiresCount(); + const primaryKeys: string[] = grid.getPrimaryKeyFieldNames(); + const key: string = primaryKeys.length > 0 ? primaryKeys[0] : 'id'; + + if (dataManager && 'result' in dataManager) { + const def: Deferred = eventPromise(args, currentQuery, key); + return def.promise; + } else { + switch (args.requestType) { + case 'delete': { + if (Array.isArray(args.data)) { + // Multiple deletions + const changes: { addedRecords: Object[]; deletedRecords: Object[]; changedRecords: Object[] } = { + addedRecords: [], + deletedRecords: args.data, + changedRecords: [] + }; + return (dataManager as DataManager) + .saveChanges(changes, key, currentQuery.fromTable, currentQuery.requiresCount()) as Promise; + } else { + // Single deletion + return (dataManager as DataManager) + .remove(key, args.data, currentQuery.fromTable, currentQuery) as Promise; + } + } + + case 'save': { + const index: number = isNullOrUndefined(args.index) ? 0 : args.index; + return (dataManager as DataManager).insert(args.data, currentQuery.fromTable, currentQuery, index) as Promise; + } + + case 'update': { + return (dataManager as DataManager).update(key, args.data, currentQuery.fromTable, currentQuery) as Promise; + } + + default: { + // Default query execution + if ((dataManager as DataManager).ready) { + return (dataManager as DataManager).ready.then(() => { + return (dataManager as DataManager).executeQuery(currentQuery); + }); + } else { + return (dataManager as DataManager).executeQuery(currentQuery); + } + } + } + } + }, [dataManager, generateQuery, grid.getPrimaryKeyFieldNames]); + + const getStateEventArgument: (query: Query) => DataRequestEvent = (query: Query): DataRequestEvent => { + const adaptr: UrlAdaptor = new UrlAdaptor(); + const dm: DataManager = new DataManager({ url: '', adaptor: new UrlAdaptor }); + const state: { data?: string, pvtData?: Object[] } = adaptr.processQuery(dm, query); + const data: Object = JSON.parse(state.data); + return extend(data, state.pvtData); + }; + + const eventPromise: (args: { requestType?: string; data?: Object; index?: number }, query: Query, key: string) => Deferred = + useCallback((args: { requestType?: string; data?: Object; index?: number }, query: Query, key: string): Deferred => { + const def: Deferred = new Deferred(); + const requestType: Action = (gridAction as GridActionEvent).requestType; + if (requestType !== undefined || args.requestType !== undefined) { + const state: DataRequestEvent = getStateEventArgument(query); + state.name = 'onDataRequest'; + if (args.requestType === 'save' || args.requestType === 'delete') { + const argsRef: { + requestType?: string; data?: Object; rowData?: Object; index?: number; + cancel?: boolean; previousData?: Object; type?: string; rows?: Element[]; + } = { ...args }; + delete argsRef.cancel; + delete argsRef.previousData; + delete argsRef.type; + delete argsRef.rows; + delete argsRef.index; + delete argsRef.requestType; + if (argsRef.rowData) { + argsRef.data = argsRef.rowData; + delete argsRef.rowData; + } + state.action = <{}>argsRef; + const deff: Deferred = new Deferred(); + const editArgs: DataChangeRequestEvent = argsRef; + editArgs.key = key; + editArgs.promise = deff.promise; + editArgs.state = state; + editArgs.saveChanges = deff.resolve; + editArgs.cancelChanges = deff.reject; + grid.onDataChangeRequest?.(editArgs); + deff.promise.then(() => { + dataState.current = { isPending: true, resolver: def.resolve, isEdit: true }; + grid.onDataRequest?.(state); + }).catch(() => void 0); + } else { + state.action = gridAction; + dataState.current = { isPending: true, resolver: def.resolve }; + grid.onDataRequest?.(state); + } + } else { + setTimeout(() => { + def.resolve(dataManager); + }, 0); + } + return def; + }, [gridAction, grid, dataManager]); + + return { generateQuery, dataManager, isRemote, getData, dataState }; +}; diff --git a/components/grids/src/grid/services/index.ts b/components/grids/src/grid/services/index.ts new file mode 100644 index 0000000..ebdb397 --- /dev/null +++ b/components/grids/src/grid/services/index.ts @@ -0,0 +1,2 @@ +export * from './service-locator'; +export * from './value-formatter'; diff --git a/components/grids/src/grid/services/service-locator.ts b/components/grids/src/grid/services/service-locator.ts new file mode 100644 index 0000000..c0310c3 --- /dev/null +++ b/components/grids/src/grid/services/service-locator.ts @@ -0,0 +1,42 @@ +import { isNullOrUndefined } from '@syncfusion/react-base'; +import { ServiceLocator } from '../types/interfaces'; +/** + * Creates a new ServiceLocator instance + * + * @returns {ServiceLocator} A ServiceLocator instance + * @private + */ +export const createServiceLocator: () => ServiceLocator = (): ServiceLocator => { + const servicesMap: { [x: string]: Object } = {}; + + const serviceLocator: ServiceLocator = { + /** + * @returns {Object} The services map + */ + get services(): { readonly [x: string]: Object } { + return servicesMap; + }, + + register: (name: string, type: T): void => { + if (isNullOrUndefined(servicesMap[name as string])) { + servicesMap[name as string] = type; + } + }, + + unregisterAll: (): void => { + Object.keys(servicesMap).forEach((key: string) => { + delete servicesMap[key as string]; + }); + }, + + getService: (name: string): T => { + if (isNullOrUndefined(servicesMap[name as string])) { + throw new Error(`The service ${name} is not registered`); + } + return servicesMap[name as string] as T; + } + }; + + return serviceLocator; +}; + diff --git a/components/grids/src/grid/services/value-formatter.ts b/components/grids/src/grid/services/value-formatter.ts new file mode 100644 index 0000000..5f629a2 --- /dev/null +++ b/components/grids/src/grid/services/value-formatter.ts @@ -0,0 +1,71 @@ +import { getDateFormat, getDateParser, getNumberFormat, getNumberParser, isNullOrUndefined } from '@syncfusion/react-base'; +import { setCulture, NumberFormatOptions, DateFormatOptions } from '@syncfusion/react-base'; +import { useMemo } from 'react'; +import { IValueFormatter } from '../types'; +/** + * Custom hook that provides value formatting capabilities for various types of data + * + * @param {string} cultureName - The culture name to use for formatting + * @returns {IValueFormatter} An IValueFormatter instance + * @private + */ +export const useValueFormatter: (cultureName?: string) => IValueFormatter = (cultureName?: string): IValueFormatter => { + const formatter: IValueFormatter = useMemo(() => ({ + getFormatFunction: (format: NumberFormatOptions | DateFormatOptions): Function => { + try { + format.locale = cultureName; + if (!isNullOrUndefined(format) && + ((format as DateFormatOptions).type === 'dateTime' || + (format as DateFormatOptions).type === 'datetime' || + (format as DateFormatOptions).type === 'date' || + (format as DateFormatOptions).type === 'time')) { + return getDateFormat(format as DateFormatOptions); + } else { + return getNumberFormat(format as NumberFormatOptions); + } + } catch (error) { + console.error('Error creating format function:', error); + return () => ''; + } + }, + getParserFunction: (format: NumberFormatOptions | DateFormatOptions): Function => { + try { + format.locale = cultureName; + if ((format as DateFormatOptions).type) { + return getDateParser(format as DateFormatOptions); + } else { + return getNumberParser(format as NumberFormatOptions); + } + } catch (error) { + console.error('Error creating parser function:', error); + return () => ''; + } + }, + fromView: (value: string, format: Function, type?: string): string | number | Date => { + try { + if ((type === 'date' || type === 'datetime' || type === 'number') && + (!isNullOrUndefined(format)) && + (!isNullOrUndefined(value))) { + return format(value); + } else { + return value; + } + } catch (error) { + console.error('Error converting from view:', error); + return value; + } + }, + toView: (value: number | Date, format: Function): string | Object => { + try { + return format(value); + } catch (error) { + console.error('Error converting to view:', error); + return value?.toString(); + } + }, + setCulture: (cultureName: string): void => { + setCulture(cultureName); + } + }), [cultureName]); + return formatter; +}; diff --git a/components/grids/src/grid/types/aggregate.interfaces.ts b/components/grids/src/grid/types/aggregate.interfaces.ts new file mode 100644 index 0000000..7080256 --- /dev/null +++ b/components/grids/src/grid/types/aggregate.interfaces.ts @@ -0,0 +1,268 @@ +import { ReactNode, ReactElement } from 'react'; +import { AggregateType } from './enum'; +import { DateFormatOptions, NumberFormatOptions } from '@syncfusion/react-base'; +import { ColumnProps } from './column.interfaces'; + +/** + * Defines the configuration properties for aggregate columns in grid component. + * Specifies how data calculations are performed and displayed in summary rows. + * Controls aggregation behavior including calculation types, display formatting, and custom functions. + */ +export interface AggregateColumnProps { + /** + * Defines the `field` name from the data source for performing aggregate calculations. + * Specifies which column `field` contains the data to be processed for summary operations. + * Must correspond to an existing `field` in the grid's data source collection. + * + * @default - + * @example + * ```tsx + * // Define field for price aggregation + * + * ``` + */ + field?: string; + + /** + * Defines the column name where calculated aggregate results will be displayed. + * Specifies the target column for showing computed summary values in the grid. + * Uses the `field` name as default when this property is not explicitly defined. + * + * @default - + * @example + * ```tsx + * // Display results in summary column + * + * ``` + */ + columnName?: string; + + /** + * Defines the aggregate calculation type to be applied on the column data. + * Specifies one or multiple calculation methods for comprehensive data analysis. + * Supports built-in types including `Sum`, `Average`, `Count`, `Min`, `Max`, and `Custom` calculations. + * + * @default - + * @example + * ```tsx + * // Apply multiple aggregation types + * + * ``` + */ + type?: AggregateType | AggregateType[] | string | string[]; + + /** + * Defines the template for rendering aggregate values in grid footer cells. + * Specifies custom content and formatting for displaying calculated results. + * Accepts string templates, React elements, or functions for dynamic content generation. + * + * @default - + * @example + * ```tsx + * // Custom footer template with formatting + * Total: ${props.Sum}} + * /> + * ``` + */ + footerTemplate?: string | ReactElement | ((props?: Object) => ReactElement | string); + + /** + * Defines the format string applied to calculated aggregate values before display. + * Specifies number or date formatting rules for presenting results in readable format. + * Supports standard format strings and detailed `NumberFormatOptions` or `DateFormatOptions`. + * + * @default - + * @example + * ```tsx + * // Apply currency formatting + * + * ``` + */ + format?: string | NumberFormatOptions | DateFormatOptions; + + /** + * Defines the custom function for calculating aggregate values when using custom aggregation. + * Specifies the calculation logic to be executed when the `type` property is set to `Custom`. + * Enables implementation of specialized calculations beyond the standard built-in aggregate types. + * + * @default - + * @example + * ```tsx + * // Define custom calculation function + * calculateWeightedAverage(data)} + * /> + * ``` + */ + customAggregate?: string | ((data: Object[] | Object, column: AggregateColumnProps) => Object); + /** + * Defines the CSS class name for styling aggregate cells based on data context. + * Specifies custom styling rules that are applied to individual aggregate cells during rendering. + * Enables conditional styling based on column details, row data, and aggregate values. + * + * @param props - Contains column configuration, complete aggregate row data, and row index information. + * @returns A CSS class name string to apply to the aggregate cell. + * + * @default - + * @example + * ```tsx + * const GridComponent = () => { + * const handleAggregateCellClass = (args: AggregateCellClassEvent) => { + * return args.column.field === 'Total' ? 'total-cell' : ''; + * }; + * + * return ( + * + * + * + * + * + * + * + * ); + * }; + * ``` + */ + aggregateCellClass?: string | ((props?: AggregateCellClassEvent) => string); +} + +/** + * Defines the event interface for applying custom CSS classes to aggregate cells. + * Provides comprehensive context information including column details, row data, and positioning details. + * Enables conditional styling of aggregate cells based on calculated values and column properties. + */ +export interface AggregateCellClassEvent { + /** + * Defines the column configuration object containing properties. + * Specifies information about the target aggregate column including field mapping and display settings. + * Contains essential details for identifying which column is being processed for styling operations. + * + * @defaut - + */ + column: ColumnProps; + + /** + * Defines the complete data object representing the entire aggregate row being processed. + * Contains all calculated aggregate values and summary information for the current row. + * Provides access to computed results for implementing conditional styling logic. + * + * @defaut - + */ + rowData: Object; + + /** + * Defines the numeric index indicating the position of the aggregate row within the grid structure. + * Specifies whether the row represents a footer summary, group summary, or other aggregate row type. + * Enables position-specific styling rules and visual hierarchy implementation. + * + * @defaut - + */ + rowIndex: number; +} + +/** + * Defines the properties interface for aggregate row components in grid component. + * Specifies configuration for rows containing calculated summary values and aggregate information. + * Controls the collection of aggregate columns and child elements within summary rows. + */ +export interface AggregateRowProps { + /** + * Defines the array of aggregate column configurations for performing calculations on grid data. + * Specifies which columns will have aggregate operations applied such as sum, average, count, or custom calculations. + * Contains the complete set of column definitions that determine how summary values are computed and displayed. + * + * @default [] + */ + columns?: AggregateColumnProps[]; + + /** + * Defines the child elements to be rendered within the aggregate row structure. + * Specifies React components or nodes for custom rendering of aggregate row content. + * Enables advanced customization of how aggregate information is presented within the row. + * + * @defaut - + * @private + */ + children?: ReactNode; +} + +/** + * Defines the event interface for aggregate cell rendering operations in grid components. + * Provides context information during the rendering process of individual aggregate cells. + * Contains cell element, associated data, and column configuration for customization purposes. + * + * @private + */ +export interface AggregateCellRenderEvent { + /** + * Defines the aggregate row data object containing all calculated summary values. + * Specifies the complete set of aggregated information associated with the current cell. + * Provides access to computed results for implementing custom rendering logic. + * + * @default {} + */ + rowData: Object; + /** + * Defines the DOM element representing the aggregate cell being rendered. + * Specifies the actual HTML element that will display the aggregate value. + * Enables direct manipulation of the cell element for advanced customization scenarios. + * + * @default null + */ + cell: Element; + /** + * Defines the aggregate column configuration object associated with the current cell. + * Specifies the column settings including field mapping, calculation type, and display properties. + * Contains metadata necessary for understanding how the cell value was calculated and should be presented. + * + * @default {} + */ + column: AggregateColumnProps; +} +/** + * Defines the event interface for aggregate row rendering operations in grid components. + * Provides comprehensive context during the rendering process of entire aggregate rows. + * Contains row element, associated data, and dimensional information for layout customization. + * + * @private + */ +export interface AggregateRowRenderEvent { + /** + * Defines the complete row data object containing all aggregate calculations for the current row. + * Specifies the full set of summary values and calculated results associated with the row. + * Provides access to all computed aggregate information for implementing custom row rendering logic. + * + * @default {} + */ + rowData: Object; + /** + * Defines the DOM element representing the aggregate row being rendered in the grid. + * Specifies the actual HTML element that will contain all aggregate cells within the row. + * Enables direct manipulation of the row element for advanced styling and layout customization. + * + * @default null + */ + row: Element; + /** + * Defines the height dimension of the aggregate row in pixels. + * Specifies the vertical space allocated for displaying the aggregate row content. + * Controls the visual spacing and layout of aggregate information within the grid structure. + * + * @default - + */ + rowHeight: number; +} + +/** + * Defines the function signature for custom aggregate calculation implementations. + * Specifies the contract for functions that perform specialized summary operations on grid data. + * Enables implementation of custom aggregation logic beyond standard built-in calculation types. + * + * @private + */ +export type CustomSummaryType = (data: Object[] | Object, column: AggregateColumnProps) => Object; diff --git a/components/grids/src/grid/types/column.interfaces.ts b/components/grids/src/grid/types/column.interfaces.ts new file mode 100644 index 0000000..7b46a4f --- /dev/null +++ b/components/grids/src/grid/types/column.interfaces.ts @@ -0,0 +1,1009 @@ +import { TextAlign, ClipMode, ColumnType } from '../types/enum'; +import { CSSProperties, ReactElement, ReactNode, TdHTMLAttributes, ThHTMLAttributes, RefObject, JSX } from 'react'; +import { DateFormatOptions, NumberFormatOptions } from '@syncfusion/react-base'; +import { ValueType, IRow, ICell } from '../types/interfaces'; +import { FilterType, FilterBarType } from './index'; +import { ColumnEditConfig, EditTemplateProps } from '../types/edit.interfaces'; +import { FormValueType } from '@syncfusion/react-inputs'; +import { NumericTextBoxProps, TextBoxProps } from '@syncfusion/react-inputs'; +import { DatePickerProps } from '@syncfusion/react-calendars'; +import { DropDownListProps } from '@syncfusion/react-dropdowns'; + +/** + * Defines the properties for configuring a column in the grid, including layout, behavior, and data binding options. + * Specifies comprehensive column settings that control appearance, functionality, and user interaction capabilities. + * Enables customization of sorting, filtering, editing, and display characteristics for individual grid columns. + */ +export interface ColumnProps { + /** + * Defines the field name that maps the column to a specific data source property for data binding operations. + * Enables sorting and filtering functionality based on the specified field name within the dataset. + * Strongly recommended to avoid JavaScript reserved words and special characters in the field name for optimal compatibility. + * + * @default '' + */ + field?: string; + + /** + * Defines the unique identifier for the column used internally by the grid for referencing and tracking operations. + * Specifies a distinct value that enables the grid to manage column state and perform internal operations efficiently. + * Automatically generated by the grid system when not explicitly provided during column configuration. + * + * @private + * @default '' + */ + uid?: string; + + /** + * Specifies the column's position in the grid's column collection, controlling display order. + * + * @private + * @default - + */ + index?: number; + + /** + * Sets the text displayed in the column header. Defaults to the `field` name if not set. + * Provides a user-friendly label for the column. + * + * @default - + */ + headerText?: string; + + /** + * Defines the column width in pixels (e.g., 100) or percentage (e.g., '20%'). + * Controls the column's size in the grid layout. + * + * @default '' + */ + width?: string | number; + + /** + * Aligns text in header and content cells (e.g., `Left`, `Right`, `Center`). + * Enhances visual consistency across the column. + * + * @default TextAlign.Left | 'Left' + */ + textAlign?: TextAlign | string; + + /** + * Aligns text specifically in the header cell (e.g., `Left`, `Right`, `Center`). + * Overrides `textAlign` for header-specific styling. + * + * @default - + */ + headerTextAlign?: TextAlign | string; + + /** + * Defines the cell content's overflow mode. The available modes are + * * `Clip` - Truncates the cell content when it overflows its area. + * * `Ellipsis` - Displays ellipsis when the cell content overflows its area. + * * `EllipsisWithTooltip` - Applies an ellipsis to overflowing cell content and displays a tooltip on hover for enhanced readability. + * + * @default ClipMode.Ellipsis | 'Ellipsis' + */ + clipMode?: ClipMode | string; + + /** + * If false, encodes HTML in header and content cells, preventing raw HTML rendering. + * When true, allows raw HTML but requires caution for security. + * + * @default true + */ + disableHtmlEncode?: boolean; + + /** + * Specifies the column's data type (e.g., `string`, `number`, `date`). + * Influences sorting, filtering, and formatting behavior. + * + * @default null + */ + type?: ColumnType | string; + + /** + * Defines the format for cell values (e.g., `C2` for currency or custom `NumberFormatOptions`/`DateFormatOptions`). + * Applied before rendering without altering the original data. + * + * @default null + */ + format?: string | NumberFormatOptions | DateFormatOptions; + + /** + * If false, hides the column. Defaults to true for visibility. + * Useful for conditional display based on user roles or state. + * + * @default true + */ + visible?: boolean; + + /** + * Renders custom content in data cells using a template string, function, or HTML element ID. + * Enables complex or dynamic cell layouts. + * + * @default null + */ + template?: string | ReactElement | ((props?: ColumnTemplateProps) => ReactElement | string); + + /** + * Renders custom content in the header cell using a template string, function, or HTML element ID. + * Supports advanced header customization like icons or buttons. + * + * @default null + */ + headerTemplate?: string | ReactElement | ((props?: ColumnHeaderTemplateProps) => ReactElement | string); + + /** + * If false, disables sorting for the column, preventing header click sorting. + * Defaults to true, enabling sorting. + * + * @default true + */ + allowSort?: boolean; + + /** + * If false, disables filtering for the column, hiding filter bar or menu. + * Defaults to true, enabling filtering. + * + * @default true + */ + allowFilter?: boolean; + + /** + * If false, disables editing for the column's cells in edit mode. + * Defaults to true, allowing editing. + * + * @default true + */ + allowEdit?: boolean; + + /** + * Applies custom CSS styles or attributes (e.g., `class`, `style`) to the column's cells. + * Enables tailored styling or behavior customization. + * + * @private + * @default - + */ + customAttributes?: CustomAttributes; + + /** + * If true, displays boolean values as checkboxes instead of text. + * + * @default false + */ + displayAsCheckBox?: boolean; + + /** + * Fires when a data cell begins to render or refresh in the grid. + * Allows transformation of cell values for display purposes only. + * Does not affect the underlying data source. + * + * @event onValueAccessor + * @example + * ```tsx + * const GridComponent = () => { + * const handleValueAccessor = (args: ValueAccessorEvent) => { + * return `#${args.rowData[args.field]}`; + * }; + * + * return ( + * + * + * + * + * + * ); + * }; + * ``` + */ + onValueAccessor?: (props?: ValueAccessorEvent) => ValueType; + + /** + * Fires when a header cell begins to render or refresh in the grid. + * Allows transformation of header text for display purposes only. + * Does not affect the underlying data source. + * + * @event onHeaderValueAccessor + * @example + * ```tsx + * const GridComponent = () => { + * const handleHeaderValueAccessor = (args: HeaderValueAccessorEvent) => { + * return `Column: ${args.headerText}`; + * }; + * + * return ( + * + * + * + * + * + * ); + * }; + * ``` + */ + onHeaderValueAccessor?: (props?: HeaderValueAccessorEvent) => ValueType; + + + /** + * Defines nested columns for stacked or hierarchical headers. + * Supports multi-level column structures for complex grids. + * + * @private + * @default null + */ + columns?: ColumnProps[]; + + /** + * If true, marks the column as a primary key for unique record identification. + * Critical for editing, adding and deleting operations. + * + * @default false + */ + isPrimaryKey?: boolean; + + /** + * Defines the filter options to customize filtering for the particular column. + * + * @default {} + */ + filter?: ColumnFilterConfig; + + /** + * Validation rules for editing (e.g., `required`, `minLength`). + * Ensures valid input during create or update operations. + * + * @default null + */ + validationRules?: ColumnValidationConfig; + + /** + * Default value for the column when adding new records. + * Ensures consistent initial values for new entries. + * + * @default null + */ + defaultValue?: string | number | Date | boolean | null; + + /** + * Custom edit cell configuration for advanced editing, such as custom input types or validation logic. + * Enhances editing flexibility. + * + * @default {} + */ + edit?: ColumnEditConfig; + + /** + * If true, marks the column as an identity column for auto-incrementing values. + * Typically used for primary keys. + * + * @private + * @default false + */ + isIdentity?: boolean; + + /** + * Media query to hide the column based on screen size or device (e.g., `(max-width: 600px)`). + * Supports responsive grid layouts. + * + * @private + * @default '' + */ + hideAtMedia?: string; + + /** + * Fires when a sorting operation begins on a each grid column. + * Allows overriding the default sorting behavior with custom logic by comparing reference and comparer values. + * Advanced custom sorting can be implemented using complete row data and the current sort direction context. + * + * @event onSortComparer + * + * @param referenceValue - The value from the reference row to compare. Typically the current row being sorted. + * @param comparerValue - The value from the comparer row to compare against. Used to determine sort order. + * @param referenceRowData - Optional. Complete data object of the reference row. Useful for advanced sorting logic. + * @param comparerRowData - Optional. Complete data object of the comparer row. Useful for advanced sorting logic. + * @param sortDirection - Optional. Current sort direction: 'Ascending', 'Descending', or ''. Helps determine sort logic flow. + * + * @returns A number or string indicating the sort order. Return a negative number if referenceValue should come before comparerValue, positive if after, or zero if equal. + * + * @example + * ```tsx + * const GridComponent = () => { + * const handleSortComparer = ( + * referenceValue: ValueType, + * comparerValue: ValueType, + * referenceRowData?: Object, + * comparerRowData?: Object, + * sortDirection?: string + * ): number => { + * // Custom sort logic based on values and optional row data + * return referenceValue > comparerValue ? 1 : -1; + * }; + * + * return ( + * + * + * + * + * + * ); + * }; + * ``` + */ + onSortComparer?: (referenceValue: ValueType, comparerValue: ValueType, referenceRowData?: Object, + comparerRowData?: Object, sortDirection?: string) => number | string; + + + /** + * Template for the column's edit UI, as a string, function, or HTML element ID. + * Customizes the editor during editing. + * + * @default null + */ + editTemplate?: string | ReactElement | ((args: EditTemplateProps) => React.ReactElement); + + /** + * Template for the column's filter UI, as a string, function, or HTML element ID. + * Customizes the filter interface. + * + * @default null + */ + filterTemplate?: string | ReactElement | Function; + + /** + * If true, enables searching for the column in the grid's search bar. + * + * @default true + */ + allowSearch?: boolean; + + /** + * Configures the 'aria-label' behavior for cells rendered using column templates. + * Improves accessibility by providing screen readers with meaningful labels when templates are used in grid columns. + * + * @default {} + */ + templateSettings?: TemplateConfig; + + /** + * Gets the formatter function for the column. + * + * @returns {Function} The formatter function + * @private + */ + getFormatter?: Function; + + /** + * Gets the parser function for the column. + * + * @returns {Function} The parser function + * @private + */ + getParser?: Function; + + /** + * Sets the format function for the column. + * + * @private + */ + formatFn?: Function; + + /** + * Sets the parser function for the column. + * + * @private + */ + parseFn?: Function; + + /** + * Formats a value for display in the column cell. + * + * @param {string | Object | null} value - The value to format + * @returns {string} The formatted value + * @private + */ + formatValue?: (value: string | Object | null) => string; + + /** + * Specifies child elements to be rendered within the column, typically React components or nodes for custom rendering. + * + * @private + */ + children?: ReactNode; + + /** + * Specifies the current sorting direction applied to the column. + * + * @private + * @default '' + */ + sortDirection?: string; + + /** + * Fires for each content cell during data binding or subsequent content cell refresh cycles. + * Enables dynamic assignment of CSS class names to content cells based on row and column context, + * allowing customization of cell appearance. + * + * @param props - Contains column configuration, complete row data, and row index. + * @returns A CSS class name to apply to the cell. + * + * @default - + * @example + * ```tsx + * const GridComponent = () => { + * const handleCellClass = (args: DataCellClassEvent) => { + * return args.rowIndex % 2 === 0 ? 'even-row' : 'odd-row'; + * }; + * + * return ( + * + * + * + * + * + * ); + * }; + * ``` + */ + dataCellClass?: string | ((props?: DataCellClassEvent) => string); + + /** + * Fires for each header cell during initial rendering or subsequent header cell refresh cycles. + * Enables dynamic assignment of CSS class names to header cells based on column configuration and header row index. + * + * @param props - Contains column configuration and header row index. + * @returns A CSS class name to apply to the header cell. + * + * @default - + * @example + * ```tsx + * const GridComponent = () => { + * const handleHeaderCellClass = (args: HeaderCellClassEvent) => { + * return args.column.field === 'OrderID' ? 'highlight-header' : ''; + * }; + * + * return ( + * + * + * + * + * + * ); + * }; + * ``` + */ + headerCellClass?: string | ((props?: HeaderCellClassEvent) => string); +} + +/** + * Defines event arguments for applying custom CSS classes to data cells in the Syncfusion React Grid. + * Provides metadata to dynamically style cells based on column configuration, row data, or row position. + * Used to customize the visual appearance of individual cells during rendering. + */ +export interface DataCellClassEvent { + /** + * Represents the column configuration associated with the data cell. + * Includes metadata such as field, type, format, and other column-specific properties. + * Enables conditional styling logic to the data cell based on column attributes. + * + * @default - + */ + column: ColumnProps; + + /** + * Contains the complete data object for the row containing the cell. + * Enables conditional styling logic based on any field value within the row. + * Useful for dynamic styling to the data cell based on business rules or row data context. + * + * @default - + */ + rowData: Object; + + /** + * Specifies the zero-based index of the row in the grid. + * Identifies the row’s position, enabling row-specific styling such as alternating row colors. + * Used for applying CSS classes to the data cell based on row position or sequence. + * + * @default - + */ + rowIndex: number; +} + +/** + * Defines event arguments for applying custom CSS classes to header cells in the Syncfusion React Grid. + * Provides metadata to dynamically style header cells based on column configuration or row position. + * Used to customize the visual appearance of column headers during rendering. + */ +export interface HeaderCellClassEvent { + /** + * Represents the column configuration associated with the header cell. + * Includes metadata such as field, type, format, and other column-specific properties. + * Enables conditional styling logic to the header cell based on column properties. + * + * @default - + */ + column: ColumnProps; + + /** + * Specifies the zero-based index of the header row in the grid. + * Identifies the header row’s position, useful for multi-level headers or conditional styling. + * Used to apply additional CSS classes to the header cell based on header row context. + * + * @default - + */ + rowIndex: number; +} + +/** + * Represents the contextual properties passed to column template functions in the Syncfusion React Grid. + * Enables dynamic rendering of custom cell content by providing access to row data, column metadata, and row index. + * Commonly used in template-based columns to implement flexible and user-defined cell rendering logic. + */ +export interface ColumnTemplateProps { + /** + * The complete data object for the row associated with the current cell. + * Grants access to all fields in the row, allowing template logic to render or manipulate cell content based on row-level data. + * + * @default {} + */ + rowData: Object; + + /** + * The column configuration object containing metadata such as field name, data type, and formatting options. + * Facilitates context-aware rendering by enabling templates to adapt based on column-specific attributes. + * + * @default {} + */ + column: ColumnProps; + + /** + * The zero-based index of the row within the grid. + * Useful for implementing conditional rendering or styling logic based on the row’s position. + * + * @default - + */ + rowIndex: number; +} + +/** + * Represents the contextual properties passed to column header template functions in the Syncfusion React Grid. + * Enables dynamic rendering of custom header content by providing access to column metadata and index. + * Commonly used in template-based headers to implement flexible and user-defined header rendering logic. + */ +export interface ColumnHeaderTemplateProps { + /** + * The column configuration object containing metadata such as field name, data type, and formatting options. + * Facilitates context-aware header rendering by enabling templates to adapt based on column-specific attributes. + * + * @default {} + */ + column: ColumnProps; + + /** + * The zero-based index of the column within the grid. + * Useful for implementing conditional rendering or styling logic based on the column’s position. + * + * @default - + */ + columnIndex: number; +} +/** + * @private + */ +export type CustomAttributes = (TdHTMLAttributes | ThHTMLAttributes) & { + index?: number; + [key: string]: string | number | boolean | undefined | readonly string[] | CSSProperties; +}; + + +/** + * Defines event arguments for customizing header cell rendering in the Syncfusion React Grid. + * Enables dynamic transformation of header text based on column metadata or application requirements. + * Used to modify or localize header content during rendering. + */ +export interface HeaderValueAccessorEvent { + /** + * Specifies the text displayed in the header cell of the grid. + * Allows modification for purposes such as localization, formatting, or custom display logic. + * Reflects the visible header content for the associated column. + * + * @default - + */ + headerText: string; + + /** + * Contains the column configuration object with metadata such as field name, type, or formatting. + * Provides context for dynamic header rendering, enabling logic based on column properties. + * Used to customize header text or behavior for specific columns. + * + * @default - + */ + column: ColumnProps; +} + +/** + * Defines event arguments for customizing data cell rendering in the Syncfusion React Grid. It enables modification of displayed content + * based on the column field and row data, strictly for presentation purposes. These changes do not affect the original data source, and + * operations such as filtering, sorting, searching, CRUD actions, etc., are based on the actual source values. + */ +export interface ValueAccessorEvent { + /** + * Specifies the field name of the column being rendered. Identifies the corresponding data key in the row object. Typically accessed + * directly when transforming or displaying cell values, without referencing the full column configuration. + * @default - + */ + field: string; + + /** + * Contains the complete data object for the row being rendered, providing access to all field values and enabling calculations + * across fields, merging or transforming values, applying conditional formatting, and customizing cell display logic based on row data. + * + * @default - + */ + rowData: Object; + + /** + * Defines the column configuration object, including metadata such as field, type, headerText, and other column-specific properties. + * Provides context for dynamic cell rendering and supports logic based on the column’s definition. Commonly used when customizing cell content according to column settings. + * + * @default - + */ + column: ColumnProps; +} + +/** + * Defines configuration options for template-based columns in the Syncfusion React Grid. + * + * This interface is used to control accessibility attributes applied to grid cells rendered using templates. + * It helps improve usability for assistive technologies and ensures compliance with accessibility standards. + */ +export interface TemplateConfig { + /** + * Specifies the value of the aria-label attribute applied to cells in template-based columns. + * + * When this property is set, the grid assigns the provided string as an accessibility label + * to each template cell in the column. This enhances screen reader support and improves accessibility + * for users relying on assistive technologies. If left empty, the aria-label attribute is not applied. + * + * @default '' + */ + ariaLabel?: string; +} + +/** + * Defines validation rules for column editing in the Syncfusion React Grid to ensure data integrity. + * Specifies constraints and checks for input values during cell editing operations. + * Used to enforce data quality and consistency in editable grid columns. + */ +export interface ColumnValidationConfig { + /** + * Indicates whether the field is mandatory during editing. + * When true, requires a non-empty value to pass validation. when false, allows empty inputs. + * Ensures critical fields contain valid data before submission. + * + * @default false + */ + required?: boolean; + + /** + * Specifies the minimum length for string values in the field. + * Enforces a lower bound on the number of characters allowed during editing. + * Used to ensure string inputs meet minimum length requirements. + * + * @default null + */ + minLength?: number; + + /** + * Specifies the maximum length for string values in the field. + * Enforces an upper bound on the number of characters allowed during editing. + * Used to restrict string inputs to a maximum length. + * + * @default null + */ + maxLength?: number; + + /** + * Specifies the minimum value for numeric inputs in the field. + * Enforces a lower bound for numeric data during editing. + * Used to ensure numeric inputs meet minimum value requirements. + * + * @default null + */ + min?: number; + + /** + * Specifies the maximum value for numeric inputs in the field. + * Enforces an upper bound for numeric data during editing. + * Used to restrict numeric inputs to a maximum value. + * + * @default null + */ + max?: number; + + /** + * Defines a range [min, max] for numeric values in the field. + * Enforces both lower and upper bounds for numeric inputs in a single rule. + * Used to ensure numeric inputs fall within a specific range. + * + * @default null + */ + range?: [number, number]; + + /** + * Defines a range [min, max] for the length of string values in the field. + * Enforces both lower and upper bounds for string length during editing. + * Used to ensure string inputs fall within a specific length range. + * + * @default null + */ + rangeLength?: [number, number]; + + /** + * Specifies a regular expression pattern for custom validation of the field’s value. + * Enforces pattern matching for string inputs, such as specific formats or character sets. + * Used to implement complex validation rules via regex. + * + * @default null + */ + regex?: RegExp | string; + + /** + * Defines a custom validation function for the field’s value. + * Executes user-defined logic to validate input, returning an error message or null if valid. + * Used for bespoke validation scenarios not covered by standard rules. + * + * @param {FormValueType} value - The value to validate + * @returns {string | null} Error message or null if valid + * @default null + */ + customValidator?: (value: FormValueType) => string | null; + + /** + * Indicates whether the field’s value must be a valid number. + * When true, enforces numeric validation, rejecting non-numeric inputs. + * Used for fields that require numeric data, such as quantities or prices. + * + * @default false + */ + number?: boolean; + + /** + * Indicates whether the field’s value must be a valid date. + * When true, enforces date format validation, rejecting invalid date inputs. + * Used for fields that require date values, such as deadlines or birthdate. + * + * @default false + */ + date?: boolean; + + /** + * Indicates whether the field’s value must be a valid email address. + * When true, enforces email format validation, rejecting invalid email inputs. + * Used for fields that require email addresses. + * + * @default false + */ + email?: boolean; + + /** + * Indicates whether the field’s value must be a valid URL. + * When true, enforces URL format validation, rejecting invalid URL inputs. + * Used for fields that require web addresses or links. + * + * @default false + */ + url?: boolean; + + /** + * Indicates whether the field’s value must contain only digits. + * When true, enforces validation for numeric-only strings, rejecting non-digit characters. + * Used for fields like postal codes or phone numbers. + * + * @default false + */ + digits?: boolean; + + /** + * Indicates whether the field’s value must be a valid credit card number. + * When true, enforces credit card format validation, rejecting invalid card numbers. + * Used for fields that require payment card information. + * + * @default false + */ + creditCard?: boolean; + + /** + * Indicates whether the field’s value must be a valid telephone number. + * When true, enforces phone number format validation, rejecting invalid phone inputs. + * Used for fields that require contact numbers. + * + * @default false + */ + tel?: boolean; + + /** + * Specifies the field name of another field whose value must match this field’s value. + * Enforces equality between two fields, such as password confirmation. + * Used for validation scenarios requiring matching inputs. + * + * @default '' + */ + equalTo?: string; +} + +/** + * Configures filtering behavior for columns in the Syncfusion React Grid. + * Defines settings for the filter UI, operator, and component parameters. + * Used to customize how data is filtered within specific columns. + */ +export interface ColumnFilterConfig { + /** + * Specifies the type of filter UI applied to the column, such as 'FilterBar' or other supported filter types. + * Determines the user interface and interaction style for filtering data in the column. + * Affects the filtering experience and available controls. + * + * @default FilterBar + * @private + */ + type?: FilterType; + + /** + * Specifies the filter type for the column, such as `stringFilter` or `numericFilter`. + * Defines the filtering logic applied to the column’s data, aligning with its data type. + * Ensures appropriate filtering behavior for strings, numbers, or other data. + * + * @default 'stringFilter' + */ + filterBarType?: string | FilterBarType; + + /** + * Defines the operator used for filtering requests, such as `contains` or `equal`. + * Specifies how the filter value is matched against column data during filtering. + * Used to customize the filtering logic for specific columns. + * + * @default '' + */ + operator?: string; + + /** + * Specifies configuration parameters for the filter component, such as text box, numeric input, or dropdown properties. + * Allows customization of the filter UI components, like placeholders or value ranges. + * Enhances the filtering experience with tailored input controls. + * + * @default {} + */ + params?: Partial; +} + +/** + * Extended column interface with additional properties for internal grid operations. + * + * @private + */ +export interface IColumnBase extends ColumnProps { + /** + * Row data for the column. + * + * @default null + */ + row?: IRow; + + /** + * Cell data for the column. + * + * @default null + */ + cell?: ICell; + + /** + * Format function for the column. + * + * @default null + */ + formatFn?: Function; + + /** + * Parser function for the column. + * + * @default null + */ + parseFn?: Function; + + /** + * CSS class for header alignment. + * + * @default '' + */ + alignHeaderClass?: string; + + /** + * CSS class for cell alignment. + * + * @default '' + */ + alignClass?: string; + + /** + * Formatted value for display. + * + * @default null + */ + formattedValue?: string | ReactNode; + + /** + * Unique identifier for the column. + * + * @default '' + */ + uid?: string; +} + +/** + * Interface for preparing column configurations in the grid. + * + * @private + */ +export interface PrepareColumns { + /** + * Array of column configurations. + * + * @default [] + */ + columns: ColumnProps[]; + /** + * Depth level for nested columns. + * + * @default 1 + */ + depth: number; + /** + * Child elements for the columns. + * + * @default null + */ + children: ReactNode; + /** + * Array of column group elements. + * + * @default [] + */ + colGroup: JSX.Element[]; + /** + * Flag indicating if columns have changed. + * + * @default false + */ + isColumnChanged?: boolean; + /** + * Flag indicating if UI column properties have changed. + * + * @default false + */ + isUIColumnpropertiesChanged?: boolean; + /** + * Array of UI column configurations. + * + * @default [] + */ + uiColumns?: ColumnProps[]; +} + +/** + * Interface for column reference to DOM elements. + * + * @private + */ +export interface ColumnRef { + /** + * Reference to the cell DOM element. + * + * @default null + */ + readonly cellRef: RefObject; +} diff --git a/components/grids/src/grid/types/edit.interfaces.ts b/components/grids/src/grid/types/edit.interfaces.ts new file mode 100644 index 0000000..13cd020 --- /dev/null +++ b/components/grids/src/grid/types/edit.interfaces.ts @@ -0,0 +1,1005 @@ +import { RefObject, ComponentType } from 'react'; +import { ColumnProps } from '../types/column.interfaces'; +import { useEdit } from '../hooks'; +import { IFormValidator } from '@syncfusion/react-inputs'; +import { ITextBox, TextBoxProps, FormState } from '@syncfusion/react-inputs'; +import { INumericTextBox, NumericTextBoxProps } from '@syncfusion/react-inputs'; +import { ICheckbox, CheckboxProps } from '@syncfusion/react-buttons'; +import { IDatePicker, DatePickerProps } from '@syncfusion/react-calendars'; +import { IDropDownList, DropDownListProps } from '@syncfusion/react-dropdowns'; +import { EditType } from '../types/enum'; +import { ValueType } from './'; + +/** + * Edit mode enumeration for Grid edit modes. + * + * @private + */ +export type EditMode = 'Normal'; + +/** + * Represents the configuration options for enabling and customizing editing behavior in a grid component. + * + * This interface provides control over record-level operations such as adding, editing, and deleting, + * as well as customization of edit modes, confirmation dialogs, and integration of custom templates. + */ +export interface EditSettings { + /** + * Determines whether new records can be added to the grid. + * + * When this property is set to true, new rows can be inserted either through programmatic methods + * or by using toolbar 'Add' action. To enable this functionality, at least one column must be defined + * as a primary key using the isPrimaryKey property in the column configuration. + * + * @default false + */ + allowAdd?: boolean; + + /** + * Determines whether existing records in the grid can be edited. + * + * When this property is set to true, users can modify cell values in existing rows either through + * programmatic updates or via toolbar 'Edit' interaction. Editing requires that at least one column + * is marked as a primary key using the isPrimaryKey property. + * + * @default false + */ + allowEdit?: boolean; + + /** + * Determines whether records can be deleted from the grid. + * + * When this property is enabled, rows can be removed either programmatically or through toolbar 'Delete' action. + * Deletion operations require that at least one column is configured as a primary key. + * + * @default false + */ + allowDelete?: boolean; + + /** + * Specifies the editing mode used within the grid. + * + * The editing mode defines how users interact with editable cells. + * + * Supported mode include: + * - Normal: Allows inline editing directly within grid cells. + * + * @default 'Normal' + * @private + */ + mode?: EditMode; + + /** + * Indicates whether double-clicking a row should activate edit mode in the Grid. + * + * When set to true, users can initiate editing by double-clicking a row. + * If set to false, double-click interactions will not trigger edit mode. + * This property is useful for controlling interaction behavior in editable grids. + * + * @default true + */ + editOnDoubleClick?: boolean; + + /** + * Controls whether a confirmation dialog is displayed when saving or discarding changes. + * + * When enabled, the grid prompts users to confirm actions such as saving edits, cancelling changes, + * or navigating away from an edited row. If disabled, these actions proceed without confirmation. + * + * @default true + */ + confirmOnEdit?: boolean; + + /** + * Controls whether a confirmation dialog is displayed before deleting a record. + * + * When enabled, users are prompted to confirm the deletion action to prevent accidental data loss. + * If disabled, records are deleted immediately without confirmation. + * + * @default false + */ + confirmOnDelete?: boolean; + + /** + * Determines whether the grid should automatically display a persistent "Add New Row" form when initialized. + * + * When this property is enabled, the grid enters an edit state by default, allowing to add new records + * without interacting with an explicit add toolbar button. This feature requires that allowAdd is set to true. + * + * @default false + * @private + */ + showAddNewRow?: boolean; + + /** + * Specifies a custom React component to be used as the edit template for grid rows. + * + * This component replaces the default editing interface and receives the current row data as props. + * It can be used to implement advanced form layouts, validation logic, or third-party integrations. + * + * @default null + */ + template?: React.ComponentType | Date>; + + /** + * Specifies the position at which a new row is inserted into the grid. + * + * Supported values include: + * - Top: Inserts the new row at the beginning of the grid. + * - Bottom: Inserts the new row at the end of the grid. + * + * @default 'Top' + */ + newRowPosition?: 'Top' | 'Bottom'; +} + +/** + * Edit state interface for managing edit operations + * + * @private + */ +export interface EditState { + isEdit: boolean; + editRowIndex: number; + editCellField: string | null; + editData: Object; + originalData: Record | Object; + validationErrors: { [field: string]: string }; + showAddNewRowData: Record; // Data for the persistent add new row + isShowAddNewRowActive: boolean; // Whether the add new row is currently active + isShowAddNewRowDisabled: boolean; // Whether the add new row inputs should be disabled (but still visible) +} + +/** + * Interface for the useEdit hook return type + * + * @private + */ +export interface UseEditResult { + isEdit: boolean; + editSettings: EditSettings; + editRowIndex: number; + editData: Object; + validationErrors: { [field: string]: string }; + originalData: Record | Object; + showAddNewRowData: Record; + isShowAddNewRowActive: boolean; + isShowAddNewRowDisabled: boolean; + editRow: (rowElement?: HTMLTableRowElement) => Promise; + saveChanges: () => Promise; + cancelChanges: () => Promise; + addRecord: (data?: Object | null, index?: number) => void; + deleteRecord: (fieldName?: string, data?: ValueType | null) => Promise; + updateRow: (index: number, data: Object) => void; + validateEditForm: () => boolean; + validateField: (field: string) => boolean; + updateEditData: (field: string, value: ValueType | null) => void; + getCurrentEditData: () => ValueType | null; + handleGridClick: (event: React.MouseEvent) => void; + handleGridDoubleClick: (event: React.MouseEvent, rowElement?: HTMLTableRowElement) => void; + checkUnsavedChanges: () => Promise; + // Dialog state and methods for confirmation dialogs + isDialogOpen: boolean; + dialogConfig: ConfirmDialogConfig; + onDialogConfirm: (() => void) | null; + onDialogCancel: (() => void) | null; + nextPrevEditRowInfo: RefObject; + focusLastField: RefObject; + escEnterIndex: RefObject; +} + +/** + * Type definition for edit strategy module that represents the return type of the useEdit hook. + * + * @private + */ +export type editModule = ReturnType; + +/** + * Props interface for custom edit template components used in grid. + * + * This interface defines the structure of data and callbacks passed to a custom + * React component used for editing grid rows. + */ +export interface EditTemplateProps { + /** + * The initial value for the field being edited. + * + * This value is typically used to populate the input control within the template. + * + * @default - + */ + defaultValue: ValueType; + + /** + * Configuration object for the current column being edited. + * + * Includes metadata such as field name, data type, validation rules, and display settings. + * + * @default - + */ + column: ColumnProps; + + /** + * Complete data object for the row currently being edited. + * + * Useful for accessing other field values or performing conditional logic within the template. + * + * @default - + */ + rowData: Object | Record; + + /** + * Validation error message associated with the current field. + * + * If present, this message should be displayed to the user to indicate input issues. + * + * @private + * @default - + */ + error: string; + + /** + * Specifies the current editing context in which the template is being rendered. + * + * This property indicates whether the template is used for adding a new record or editing an existing one. + * It helps differentiate between create and update operations, allowing conditional rendering or logic + * based on the editing mode. + * + * Supported values: + * - Add: The template is used to create a new record. + * - Edit: The template is used to modify an existing record. + * + * @default - + */ + action: 'Add' | 'Edit'; + + /** + * Callback function triggered when the field value changes. + * + * @param value - The updated value from the input control. + * @returns void + */ + onChange(value: ValueType): void; +} + +/** + * Represents the event triggered when a form is initialized for editing or adding a row. + * + * This interface provides access to the form validator and the associated row data, + * allowing configuration of validation logic and dynamic form behavior during rendering. + */ +export interface FormRenderEvent { + /** + * Holds a reference to the form validator instance used for validating input fields. + * + * Enables access to validation methods and state, allowing programmatic control + * over form validation during initialization or interaction. + * + * @default null + */ + formRef?: RefObject; + + /** + * Specifies the index of the row for which the form is rendered. + * + * Identifies the row's position in the grid's data source, supporting both + * edit and insert operations within the grid component. + * + * @default - + */ + rowIndex?: number; + + /** + * Contains the complete data object for the row being edited or added. + * + * Provides access to field values required for conditional rendering, + * dynamic validation, or logic execution during form setup. + * + * @default - + */ + rowData?: Object; +} + + +/** + * Event structure triggered during a row edit operation. + * + * This interface allows access to the row data and index, and supports cancellation + * of the edit operation based on custom conditions. + */ +export interface RowEditEvent { + /** + * Data object containing the current values of the row being edited. + * + * @default - + */ + rowData?: Object; + + /** + * Index of the row being edited in the grid's data source. + * + * @default - + */ + rowIndex?: number; + + /** + * Flag indicating whether to cancel the edit operation. + * + * Set to true to prevent the row from entering edit mode. + * + * @default false + */ + cancel?: boolean; +} + +/** + * Describes the structure of the event triggered during a row add operation in the grid. + * + * Provides access to the new row's data and index, and supports conditional cancellation + * of the insertion process based on custom logic or validation requirements. + */ +export interface RowAddEvent { + /** + * The initial data object for the row being added. + * + * Represents the field values intended for insertion, allowing transformation, + * validation, or enrichment prior to committing the row to the grid. + * + * @default - + */ + rowData?: Object; + + /** + * The zero-based index at which the new row will be inserted in the grid's data source. + * + * Indicates the target position for the new entry, which can be used to determine + * placement logic or enforce ordering constraints. + * + * @default - + */ + rowIndex?: number; + + /** + * Indicates whether the add operation should be canceled. + * + * When set to true, the row insertion is prevented, allowing conditional control + * over grid updates based on business rules or validation outcomes. + * + * @default false + */ + cancel?: boolean; +} + +/** + * Event triggered at the start of a delete operation. + * + * It provides access to the records targeted for deletion and + * supports cancellation of the operation based on custom logic. + */ +export interface DeleteEvent { + /** + * Array of record objects that are about to be deleted from the grid. + * + * Each object represents a row in the grid's data source. + * + * @default - + */ + data?: Object[]; + + /** + * Specifies whether to cancel the delete operation. + * If true, the deletion is aborted and no changes are made. + * + * @default false + */ + cancel?: boolean; + + /** + * Specifies the type of action being performed, such as add, edit, or delete. + * + * @default - + */ + action?: string; +} + +/** + * Event triggered at the start of a save operation. + * + * It provides access to the current and previous row data, + * and supports cancellation of the save based on validation or business rules. + */ +export interface SaveEvent { + /** + * Data object containing the latest values for the row being saved. + * + * Includes all fields that were edited or added. + * + * @default - + */ + editedRowData?: Object; + + /** + * Index of the row being saved within the grid's data source. + * + * Useful for identifying the row position during update operations. + * + * @default - + */ + rowIndex?: number; + + /** + * Original data of the row before any modifications were made. + * + * Useful for comparing changes or reverting edits. + * + * @default - + */ + rowData?: Object; + + /** + * Flag indicating whether to cancel the save operation. + * + * Set to true to prevent the row from being saved. + * + * @default false + */ + cancel?: boolean; + + /** + * Specifies the type of action being performed, such as add, edit, or delete. + * + * @default - + */ + action?: string; +} + +/** + * Event triggered when an edit or add form is cancelled. + * + * It provides access to the form state and row context, + * allowing cleanup or rollback logic to be executed. + */ +export interface CancelFormEvent { + /** + * Data object containing the current form state for the row. + * + * Includes field values at the time of cancellation. + * + * @default - + */ + rowData?: Object; + + /** + * Index of the row being edited or added in the grid's data source. + * + * @default - + */ + rowIndex?: number; + + /** + * Reference to the form component used for editing or adding the row. + * + * Can be used to reset validation or perform cleanup actions. + * + * @default - + */ + formRef?: RefObject; +} + +/** + * Union type for all possible component ref types that EditCell can handle + * + * @private + */ +export type EditCellInputRef = HTMLInputElement | HTMLSelectElement | ITextBox | INumericTextBox | ICheckbox | IDatePicker | IDropDownList; + +/** + * Union type for edit component parameters that combines all possible props from different edit input components. + * + * @private + */ +export type EditParams = Partial; + +/** + * Configuration interface for column-level edit settings in the grid. + * + * Defines the type of input component to render during editing and allows + * passing component-specific configuration parameters. + */ +export interface ColumnEditConfig { + /** + * Specifies the type of edit component to be rendered for the column. + * + * Determines the input control used during editing, such as a textbox, dropdown, + * date picker, or checkbox. Custom string identifiers may also be used for + * custom edit components. + * + * @default EditType.TextBox | 'stringEdit' + */ + type?: EditType | string; + + /** + * Configuration parameters passed to the edit component. + * + * These parameters vary depending on the selected edit type and may include + * properties such as placeholder text, formatting options, + * and dropdown data sources. + * + * Supports partial props from multiple component types including: + * TextBoxProps, `NumericTextBoxProps`, `CheckboxProps`, `DatePickerProps`, and `DropDownListProps`. + * + * @default - + */ + params?: Partial; +} + +/** + * Props interface for EditCell component + * + * @private + */ +export interface EditCellProps { + /** + * Column configuration object containing properties and settings for the current column being edited. + * + * @default - + */ + column: ColumnProps; + + /** + * The current value of the cell being edited. + * + * @defaut - + */ + value: string | number | boolean | Object | undefined; + + /** + * The complete data object for the current row, providing context for the edit operation. + * + * @default - + */ + rowData: Object; + + /** + * Validation error message to display for the current cell, if any validation fails. + * + * @default - + */ + error?: string; + + /** + * Specifies whether the input should automatically receive focus when the edit cell is rendered. + * + * @default false + */ + autoFocus?: boolean; + + /** + * Callback function triggered when the cell value changes during editing. + * + * @param value - The new value from the edit component + * @returns {void} + */ + onChange(value: unknown): void; + + /** + * Callback function triggered when the input loses focus. + * + * @param value - The current value when focus is lost + * @returns {void} + */ + onBlur(value: string | number | boolean | Object | undefined): void; + + /** + * Callback function triggered when the input gains focus. + * + * @returns {void} + */ + onFocus(): void; + + /** + * Internal property indicating whether this is an add operation rather than an edit operation. + * + * @default false + */ + isAdd?: boolean; + + /** + * Specifies whether the input should be disabled. Used for showAddNewRow feature when editing another row. + * + * @default false + */ + disabled?: boolean; + + /** + * Form validation state object from the FormValidator component. + * + * @default null + */ + formState?: FormState; +} + +/** + * Ref interface for EditCell component + * + * @private + */ +export interface EditCellRef { + /** + * Sets focus on the input element of the edit cell. + * + * @returns {void} + */ + focus(): void; + + /** + * Retrieves the current value from the edit cell input. + * + * @returns {ValueType | null} The current value of the edit cell + */ + getValue(): ValueType | null; + + /** + * Sets a new value for the edit cell input. + * + * @param value - The value to set in the edit cell + * @returns {void} + */ + setValue(value: ValueType | null): void; +} + +/** + * Props interface for EditForm component + * + * @private + */ +export interface EditFormProps { + /** + * Array of column configuration objects that define the structure and properties of the form fields. + * + * @default [] + */ + columns: ColumnProps[]; + + /** + * The current data object containing the values being edited in the form. + * + * @default {} + */ + editData: Object; + + /** + * Object containing validation error messages for each field, keyed by field name. + * + * @default {} + */ + validationErrors: { [field: string]: string }; + + /** + * The index of the row being edited in the grid's data source. + * + * @default - + */ + editRowIndex: number; + + /** + * Unique identifier for the row being edited, used for tracking and validation purposes. + * + * @default - + */ + rowUid: string; + + /** + * Callback function triggered when a field value changes in the edit form. + * + * @param field - The field name that changed + * @param value - The new value for the field + * @returns {void} + */ + onFieldChange?: (field: string, value: string | number | boolean | Record | Date) => void; + + /** + * Callback function triggered when the form should be saved. + * + * @param isForwardTab - Optional parameter indicating if save was triggered by tab navigation + * @returns {void} + */ + onSave?: (isForwardTab?: boolean) => void; + + /** + * Callback function triggered when the form editing should be cancelled. + * + * @returns {void} + */ + onCancel?: () => void; + + /** + * Custom component to use as the edit template instead of the default form fields. + * + * @default null + */ + template?: ComponentType | Date>; + + /** + * Specifies whether the form inputs should be disabled. Used for showAddNewRow feature when editing an existing row. + * + * @default false + */ + disabled?: boolean; +} + +/** + * Ref interface for EditForm component + * + * @private + */ +export interface InlineEditFormRef { + /** + * Sets focus on the first editable field in the form. + * + * @returns {void} + */ + focusFirstField: () => void; + + /** + * Validates all form fields and returns the validation result. + * + * @returns {boolean} True if validation passes, false otherwise + */ + validateForm: () => boolean; + + /** + * Retrieves an array of all edit cell references in the form. + * + * @returns {EditCellRef[]} Array of edit cell references + */ + getEditCells: () => EditCellRef[]; + + /** + * Gets the HTML form element reference. + * + * @returns {HTMLFormElement | null} The form element or null if not available + */ + getFormElement: () => HTMLFormElement | null; + + /** + * Retrieves the current data from all form fields. + * + * @returns {string | number | boolean | Record | Date} The current form data + */ + getCurrentData: () => string | number | boolean | Record | Date; + + /** + * Reference object containing all edit cell refs organized by field name. + * + * @default {} + */ + editCellRefs: React.RefObject<{ [field: string]: EditCellRef }>; + + /** + * Current state of the form validation from FormValidator. + * + * @default null + */ + formState: FormState; + + /** + * Reference to the FormValidator instance for the form. + * + * @default null + */ + formRef: React.RefObject; +} + +/** + * Props interface for InlineEditForm component + * + * @private + */ +export interface InlineEditFormProps extends EditFormProps { + /** + * Stable key used for React memoization to prevent unnecessary re-renders. + * + * @default - + */ + stableKey: string; + + /** + * Specifies whether the form inputs should be disabled. Used for showAddNewRow feature when editing an existing row. + * + * @default false + */ + disabled?: boolean; + + /** + * Indicates whether this form is being used for an add operation rather than an edit operation. + * + * @default false + */ + isAddOperation: boolean; +} + +/** + * Props interface for ConfirmDialog component + * + * @private + */ +export interface ConfirmDialogProps { + /** + * Specifies whether the confirmation dialog is currently open and visible. + * + * @default false + */ + isOpen?: boolean; + + /** + * Configuration object containing the dialog's content, buttons, and styling information. + * + * @default null + */ + config?: ConfirmDialogConfig | null; + + /** + * Callback function triggered when the confirm button is clicked. + * + * @returns {void} + */ + onConfirm?: () => void; + + /** + * Callback function triggered when the cancel button is clicked. + * + * @returns {void} + */ + onCancel?: () => void; +} + +/** + * Props interface for ValidationTooltips component that displays validation errors using React Tooltip. + * This component properly integrates with the FormValidator and displays validation errors. + * + * @private + */ +export interface ValidationTooltipsProps { + /** + * Current validation state from the FormValidator component containing error information. + * + * @default null + */ + formState: FormState | null; + + /** + * Reference object containing all edit cell refs organized by field name for tooltip positioning. + * + * @default null + */ + editCellRefs?: React.RefObject<{ [field: string]: EditCellRef }>; +} + +/** + * Configuration interface for confirmation dialogs used in edit operations + * + * @private + */ +export interface ConfirmDialogConfig { + /** + * The title text displayed at the top of the confirmation dialog. + * + * @default - + */ + title: string; + + /** + * The main message content displayed in the dialog body. + * + * @default - + */ + message: string; + + /** + * Text displayed on the confirm button. If not specified, a default confirm text is used. + * + * @default 'Confirm' + */ + confirmText?: string; + + /** + * Text displayed on the cancel button. If not specified, a default cancel text is used. + * + * @default 'Cancel' + */ + cancelText?: string; + + /** + * The type of dialog which determines the styling, icons, and color scheme used. + * + * @default 'confirm' + */ + type?: 'confirm' | 'delete' | 'warning' | 'info'; +} + +/** + * State interface for managing confirmation dialog state + * + * @private + */ +export interface DialogState { + /** + * Indicates whether the dialog is currently open and visible. + * + * @default false + */ + isOpen: boolean; + + /** + * Configuration object for the dialog content and appearance. + * + * @default null + */ + config: ConfirmDialogConfig | null; + + /** + * Callback function to execute when the confirm button is clicked. + * + * @default null + */ + onConfirm: (() => void) | null; + + /** + * Callback function to execute when the cancel button is clicked. + * + * @default null + */ + onCancel: (() => void) | null; +} + +/** + * Return interface for the useConfirmDialog hook + * + * @private + */ +export interface UseConfirmDialogResult { + /** + * Shows a confirmation dialog for delete operations. + * + * @returns {Promise} Promise resolving to true if confirmed, false if cancelled + */ + confirmOnDelete: () => Promise; + + /** + * Shows a confirmation dialog with custom configuration for edit operations. + * + * @param config - Configuration object for the dialog + * @returns {Promise} Promise resolving to true if confirmed, false if cancelled + */ + confirmOnEdit: (config: ConfirmDialogConfig) => Promise; + + /** + * Indicates whether any confirmation dialog is currently open. + * + * @default false + */ + isDialogOpen: boolean; + + /** + * Current configuration of the open dialog, or null if no dialog is open. + * + * @default null + */ + dialogConfig: ConfirmDialogConfig | null; + + /** + * Handler function for when the user confirms the dialog action. + * + * @returns {void} + */ + onDialogConfirm: () => void; + + /** + * Handler function for when the user cancels the dialog action. + * + * @returns {void} + */ + onDialogCancel: () => void; +} diff --git a/components/grids/src/grid/types/enum.ts b/components/grids/src/grid/types/enum.ts new file mode 100644 index 0000000..4e80f7c --- /dev/null +++ b/components/grids/src/grid/types/enum.ts @@ -0,0 +1,524 @@ +/** + * Defines the horizontal text alignment within grid cells and headers. + * Used to control the visual alignment of text for better readability and layout consistency. + * + * @default TextAlign.Left + * @example + * ```tsx + * + * ``` + */ +export enum TextAlign { + /** + * Aligns text to the left edge of the cell. + * Commonly used for textual and string-based data. + * + * @default 'Left' + */ + Left = 'Left', + + /** + * Aligns text to the right edge of the cell. + * Ideal for numeric or financial data to maintain column alignment. + * + * @default 'Right' + */ + Right = 'Right', + + /** + * Centers the text horizontally within the cell. + * Useful for headers or balanced visual presentation. + * + * @default 'Center' + */ + Center = 'Center', + + /** + * Justifies the text to evenly spread across the cell width. + * Best suited for paragraph-style content or long descriptions. + * + * @default 'Justify' + */ + Justify = 'Justify' +} + +/** + * Defines various types of cells in the grid. + * + * @private + */ +export enum CellType { + /** Defines CellType as Data */ + Data, + /** Defines CellType as Header */ + Header, + /** Defines CellType as Summary */ + Summary, + /** Defines CellType as GroupSummary */ + GroupSummary, + /** Defines CellType as CaptionSummary */ + CaptionSummary, + /** Defines CellType as Filter */ + Filter, + /** Defines CellType as Indent */ + Indent, + /** Defines CellType as GroupCaption */ + GroupCaption, + /** Defines CellType as GroupCaptionEmpty */ + GroupCaptionEmpty, + /** Defines CellType as Expand */ + Expand, + /** Defines CellType as HeaderIndent */ + HeaderIndent, + /** Defines CellType as StackedHeader */ + StackedHeader, + /** Defines CellType as DetailHeader */ + DetailHeader, + /** Defines CellType as DetailExpand */ + DetailExpand, + /** Defines CellType as CommandColumn */ + CommandColumn, + /** Defines CellType as DetailFooterIntent */ + DetailFooterIntent, + /** Defines CellType as RowDrag */ + RowDragIcon, + /** Defines CellType as RowDragHeader */ + RowDragHIcon +} + +/** + * Defines the grid line display modes for the grid layout. + * Controls the visibility of horizontal and vertical lines between cells, enhancing visual structure and readability. + * + * @default GridLine.Default | 'Default' + * @example + * ```tsx + * + * ``` + */ +export enum GridLine { + /** + * Displays both horizontal and vertical grid lines. + * Provides a fully bordered layout for clear separation of cells. + */ + Both = 'Both', + + /** + * No grid lines are displayed. + * Creates a clean, borderless layout for minimalistic design. + */ + None = 'None', + + /** + * Displays only horizontal grid lines. + * Useful for row-based separation while keeping columns visually merged. + */ + Horizontal = 'Horizontal', + + /** + * Displays only vertical grid lines. + * Useful for column-based separation while keeping rows visually merged. + */ + Vertical = 'Vertical', + + /** + * Displays only horizontal grid lines. + * Useful for row-based separation while keeping columns visually merged. + * + * @default 'Default' + */ + Default = 'Default' +} + +/** + * Defines the supported data types for grid columns. + * Used to define how data is interpreted and rendered in each column. + * If not explicitly defined, the type is inferred from the first row's data based on each cell value type. + * + * @default - + * @example + * ```tsx + * + * ``` + */ +export enum ColumnType { + /** + * Represents text or string values. + * Commonly used for names, descriptions, or identifiers. + * + * @default 'string' + */ + String = 'string', + + /** + * Represents numeric values. + * Used for quantities, prices, or any numerical data. + * + * @default 'number' + */ + Number = 'number', + + /** + * Represents boolean values. + * Used for true/false or yes/no type fields. + * + * @default 'boolean' + */ + Boolean = 'boolean', + + /** + * Represents date values. + * Used for timestamps, birthdays, or scheduling data. + * + * @default 'date' + */ + Date = 'date', + + /** + * Represents date and time values. + * Used for precise timestamps, scheduling data with time. + * + * @default 'dateTime' + */ + DateTime = 'dateTime' +} + +/** + * Defines types of Render. + * + * @private + */ +export enum RenderType { + /** Defines RenderType as Header */ + Header, + /** Defines RenderType as Filter */ + Filter, + /** Defines RenderType as Content */ + Content, + /** Defines RenderType as Summary */ + Summary +} + +/** + * Defines the direction of sorting applied to grid columns. + * Used to control the order in which data is displayed. + * + * @default SortDirection.Ascending + * @example + * ```tsx + * + * ``` + */ +export enum SortDirection { + /** + * Sorts data in ascending order (e.g., A–Z, 0–9). + * Commonly used for alphabetical or chronological sorting. + * + * @default 'Ascending' + */ + Ascending = 'Ascending', + + /** + * Sorts data in descending order (e.g., Z–A, 9–0). + * Useful for prioritizing higher values or latest entries. + * + * @default 'Descending' + */ + Descending = 'Descending' +} + +/** + * Defines types of Filter. + * + * @private + * ```props + * * FilterBar :- Specifies the filter type as filter bar. + * ``` + */ +export type FilterType = + 'FilterBar'; + +/** + * Enumerates the filter bar types supported by Grid component for column-level filtering. + * Defines the type of filter UI and logic applied to a column, such as string, numeric, or date-based filtering. + * Used to configure the filtering behavior and user interface for specific columns in the grid. + * + * @default TextBox + * @example + * ```tsx + * + * ``` + */ +export enum FilterBarType { + /** + * Applies a string-based filter using a text input. + * Suitable for filtering textual data. + * + * @default 'stringFilter' + */ + TextBox = 'stringFilter', + + /** + * Applies a numeric filter using a number input. + * Ideal for filtering numeric fields such as price, quantity, etc. + * + * @default 'numericFilter' + */ + NumericTextBox = 'numericFilter', + + /** + * Applies a date-based filter using a date picker. + * Useful for filtering columns with date values. + * + * @default 'datePickerFilter' + */ + DatePicker = 'datePickerFilter' +} + +/** + * Defines the filter bar mode options for grid filtering behavior. + * Determines how and when the filter operation is triggered in the grid. + * + * @default FilterBarMode.OnEnter + * @example + * ```tsx + * + * ``` + */ +export enum FilterBarMode { + /** + * Initiates the filter operation only after the Enter key is pressed. + * Suitable for precise filtering and reducing unnecessary operations. + * + * @default 'OnEnter' + */ + OnEnter = 'OnEnter', + + /** + * Initiates the filter operation automatically after a short delay (default: 500 ms). + * Ideal for responsive filtering as the user types. + * + * @default 'Immediate' + */ + Immediate = 'Immediate' +} + +/** + * Defines the available text wrapping modes for grid cells. + * Used to control how text is displayed within header and content cells, improving readability and layout flexibility. + * + * @default WrapMode.Both + * @example + * ```tsx + * + * ``` + */ +export enum WrapMode { + /** + * Enables wrapping for both header and content cells. + * Ensures full visibility of labels and cell values, especially useful for long or multilingual text. + * + * @default 'Both' + */ + Both = 'Both', + + /** + * Wraps only the header cells. + * Content cells remain single-line for a compact layout while preserving header readability. + * + * @default 'Header' + */ + Header = 'Header', + + /** + * Wraps only the content cells. + * Header cells stay single-line to maintain alignment and layout consistency. + * + * @default 'Content' + */ + Content = 'Content' +} + +/** + * Defines the cell content's overflow handling mode. + * Controls how text is displayed when it exceeds the cell's visible area. + * + * @default ClipMode.Clip + * @example + * ```tsx + * + * ``` + */ +export enum ClipMode { + /** + * Truncates the cell content when it overflows its area. + * No visual indication is provided for clipped content. + * + * @default 'Clip' + */ + Clip = 'Clip', + + /** + * Displays an ellipsis (`...`) when the cell content overflows. + * Provides a visual cue that content is truncated. + */ + Ellipsis = 'Ellipsis', + + /** + * Applies an ellipsis to overflowing cell content and shows a tooltip on hover. + * Enhances readability by allowing users to view the full content. + */ + EllipsisWithTooltip = 'EllipsisWithTooltip' +} + +/** + * Defines Actions of the Grid. + * ```props + * * filtering :- Defines current action as filtering. + * * clearFiltering :- Defines current action as clear filtering. + * * sorting :- Defines current action as sorting. + * * clearSorting :- Defines current action as clear sorting. + * * searching :- Defines current action as searching. + * * paging :- Defines current action as paging. + * ``` + * + * @private + */ +export type Action = + 'filtering' | + 'clearFiltering' | + 'sorting' | + 'clearSorting' | + 'searching' | + 'paging' | + 'delete' | + 'edit' | + 'add' | + 'refresh'; +/** + * Enumerates the types of aggregate calculations supported by the Grid component. + * Defines the available aggregation methods for summarizing data in the grid’s footer sections. + * Used to configure how data is aggregated for display in aggregate rows or columns. + * ```props + * * Sum :- Specifies sum aggregate type. + * * Average :- Specifies average aggregate type. + * * Max :- Specifies maximum aggregate type. + * * Min :- Specifies minimum aggregate type. + * * Count :- Specifies count aggregate type. + * * TrueCount :- Specifies true count aggregate type. + * * FalseCount :- Specifies false count aggregate type. + * * Custom :- Specifies custom aggregate type. + * ``` + */ +export enum AggregateType { + Sum = 'Sum', + Average = 'Average', + Max = 'Max', + Min = 'Min', + Count = 'Count', + TrueCount = 'TrueCount', + FalseCount = 'FalseCount', + Custom = 'Custom' +} + +/** + * Defines the set of actionable items displayed in the grid toolbar. Each item maps to a specific user command. Enables direct data operations, UI control. + * ```props + * * Add :- Creates new row or record. Opens blank form or inserts editable row. + * * Edit :- Enables editing for selected row. Supports selection logic, inline editing. + * * Update :- Saves changes to data source. Triggers validation, lifecycle hooks. + * * Delete :- Removes selected row or record. + * * Cancel :- Discards unsaved changes. Exits edit mode, maintains data integrity. + * * Search :- Displays input for row filtering. Supports keyword match, column-level queries + * ``` + */ +export type ToolbarItems = + 'Add' | + 'Edit' | + 'Update' | + 'Delete' | + 'Cancel' | + 'Search'; + +/** + * Defines the available edit types for grid columns. + * Used to configure the input control rendered during column cell editing. + * + * @default EditType.TextBox + * @example + * ```tsx + * + * ``` + */ +export enum EditType { + /** + * Defines a default standard text input for editing string values. + * Suitable for general-purpose text fields. + * + * @default 'stringEdit' + */ + TextBox = 'stringEdit', + + /** + * Defines a dropdown list for selecting string values. + * Useful for predefined options or lookup fields. + * + * @default 'dropDownEdit' + */ + DropDownList = 'dropDownEdit', + + /** + * Defines a date picker for editing date values. + * Ideal for scheduling, timestamps, or calendar-based inputs. + * + * @default 'datePickerEdit' + */ + DatePicker = 'datePickerEdit', + + /** + * Defines a checkbox for editing boolean values. + * Used for true/false or yes/no type fields. + * + * @default 'booleanEdit' + */ + CheckBox = 'booleanEdit', + + /** + * Defines a numeric input for editing number values. + * Supports validation and formatting for numeric fields. + * + * @default 'numericEdit' + */ + NumericTextBox = 'numericEdit' +} + +/** + * @private + */ +export type EditEndAction = 'Click' | 'Key'; + +/** + * Defines the keyboard navigation keys. + * + * @private + */ +export enum KeyboardKeys { + UP = 'ArrowUp', + DOWN = 'ArrowDown', + LEFT = 'ArrowLeft', + RIGHT = 'ArrowRight', + TAB = 'Tab', + HOME = 'Home', + END = 'End', + ENTER = 'Enter', + SPACE = ' ', + ESCAPE = 'Escape', + ALT_J = 'j', + ALT_W = 'w', + PAGE_UP = 'PageUp', + PAGE_DOWN = 'PageDown', + F2 = 'F2', + DELETE = 'Delete', + CTRL_HOME = 'Home', + CTRL_END = 'End' +} diff --git a/components/grids/src/grid/types/filter.interfaces.ts b/components/grids/src/grid/types/filter.interfaces.ts new file mode 100644 index 0000000..b74d086 --- /dev/null +++ b/components/grids/src/grid/types/filter.interfaces.ts @@ -0,0 +1,740 @@ +import { Dispatch, SetStateAction } from 'react'; +import { FilterType, FilterBarMode } from './index'; +import { ColumnProps } from '../types/column.interfaces'; +import { ICustomOptr } from '../types/interfaces'; +import { GridActionEvent } from '../types/grid.interfaces'; + +/** + * Defines the configuration for filtering functionality in the Grid component. + * Specifies settings for enabling filtering, defining filter conditions, and customizing the filter UI. + * Controls how data is filtered based on user input and predefined criteria. + */ +export interface FilterSettings { + /** + * Determines whether filtering is enabled for the grid’s columns. + * When set to true, allows to apply filters via the filter bar. When false, disables all filtering functionality from the grid. + * Affects all columns unless overridden by individual column settings. + * + * @type {boolean} + * @default false + */ + enabled?: boolean; + + /** + * Specifies an array of `PredicateModel` objects to define initial or active filter conditions for grid columns. + * Each predicate represents a filter rule applied to a specific column, such as `field`, `operator`, and `value`. + * Used to pre-filter data on grid initialization or to retrieve the current filter state. + * + * @type {FilterPredicates[]} + * @default [] + */ + columns?: FilterPredicates[]; + + /** + * Configures the type of filter UI to be used in the grid, such as `FilterBar`. + * Determines the visual interface for applying filters to columns. + * + * @type {FilterType} + * @default 'FilterBar' + * @private + */ + type?: FilterType; + + /** + * Specifies the operational mode of the filter bar, controlling when filtering is triggered. + * Supports `OnEnter` (filtering starts when the Enter key is pressed) or `Immediate` (filtering starts after a time delay). + * Influences the responsiveness and user experience of filtering interactions. + * + * @type {FilterBarMode | string} + * @default 'Immediate' + */ + mode?: FilterBarMode | string; + + /** + * Sets the time delay (in milliseconds) for filtering in `Immediate` mode. + * Controls the wait time before the grid processes filter input, balancing responsiveness and performance. + * Only applicable when the filter mode is set to `Immediate`. + * + * @type {number} + * @default 1500 + */ + immediateModeDelay?: number; + + /** + * Provides a configuration object to override default filter operators in the filter menu. + * Allows customization of operators (e.g., 'equal', 'contains') based on column data types (string, number, date, boolean). + * Used internally to tailor filter behavior for specific use cases. + * + * @type {ICustomOptr} + * @default null + * @private + */ + operators?: ICustomOptr; + + /** + * Enables or disables accent-insensitive filtering for string fields. + * When true, diacritic characters (e.g., accents like é, ñ) are ignored during filtering, treating them as their base characters. + * Improves filter usability for multilingual datasets. + * + * @type {boolean} + * @default false + */ + ignoreAccent?: boolean; + + /** + * Enables or disables case-sensitive filtering for string fields. + * When true, string filtering distinguishes between uppercase and lowercase letters. When false, it is case-insensitive. + * Does not affect filtering for numbers, booleans, or dates. + * + * @type {boolean} + * @default false + */ + caseSensitive?: boolean; +} + +/** + * Represents the event triggered when a filtering operation completes in the Grid component. + * Provides comprehensive details about the completed filter action and its results. + * Used to handle post-filtering logic or updates. + */ +export interface FilterEvent extends GridActionEvent { + /** + * Defines the predicate object for the filter that was just applied. + * Contains the `field`, `operator`, and `value` used in the completed filter operation. + * + * @type {FilterPredicates} + * @default - + */ + currentFilterObject?: FilterPredicates; + + /** + * Lists all predicate objects representing the current filter conditions across columns. + * Provides a complete set of active filters applied to the grid. + * Used to inspect or modify the grid’s filter state post-operation. + * + * @type {FilterPredicates[]} + * @default [] + */ + columns?: FilterPredicates[]; + + /** + * Indicates the type of filter action that was completed (e.g., 'filtering', 'clearFiltering'). + * Describes the operation performed, aiding in post-filter processing. + * Helps differentiate between various filter-related actions. + * + * @type {string} + * @default - + */ + action?: 'filtering' | 'clearFiltering'; + + /** + * Provides the configuration object for the filtered column. + * Contains metadata such as field name, data type, and column-specific settings. + * Enables access to column properties for post-filter customization. + * + * @type {ColumnProps} + * @default - + */ + currentFilterColumn?: ColumnProps; + + /** + * Allows cancellation of the filtering action before it is applied. + * When set to true, prevents the filter from being executed, useful for validation or conditional logic. + * Typically used in event handlers to control filter behavior. + * + * @private + * @type {boolean} + * @default false + */ + cancel?: boolean; +} + +/** + * Configures filter predicates for individual columns in the Grid component. + * Defines the criteria, operators, and behavior for filtering data in a specific column. + * Supports filtering scenarios with logical conditions. + */ +export interface FilterPredicates { + /** + * Identifies the column `field` in the data source to which the filter is applied. + * Used to map the filter condition to the correct column. + * + * @type {string} + * @default - + */ + field?: string; + + /** + * Specifies the comparison operator used for filtering grid column data. + * Determines how the filter value is compared to column data. + * + * Must align with the column’s data type (e.g., string, number, date) to ensure valid filtering behavior. + * Common operators include: + * equal, notEqual – for exact matches. + * greaterThan, lessThan – for numeric or date comparisons. + * contains, startsWith, endsWith – for string-based filtering. + * + * @type {string} + * @default - + */ + operator?: string; + + /** + * Defines the `value` used to filter records in the column. + * Supports single value or arrays for strings, numbers, dates, or booleans, depending on the column type. + * Used to match records against the specified operator. + * + * @type {string | number | Date | boolean | (string | number | Date | boolean)[]} + * @default - + */ + value?: string | number | Date | boolean | (string | number | Date | boolean)[]; + + /** + * Determines whether string filtering is case-sensitive. + * When true, exact case matching is enforced for string fields. When false, filtering is case-insensitive. + * Does not affect non-string fields like numbers or dates. + * + * @type {boolean} + * @default false + */ + caseSensitive?: boolean; + + /** + * Enables accent-insensitive filtering for string fields. + * When true, diacritic characters (e.g., accents like á, é) are treated as their base characters during filtering. + * Enhances filter capabilities for multilingual data. + * + * @type {boolean} + * @default false + */ + ignoreAccent?: boolean; + + /** + * Specifies the logical operator ('and' or 'or') to combine this predicate with others. + * Enables filtering by linking multiple conditions for the same or different columns. + * Used to build advanced filter queries. + * + * @type {string} + * @default - + */ + predicate?: string; + + /** + * Stores the actual filter value applied to the column for internal processing. + * Used by the Grid to manage filter state and apply filter logic. + * Typically handled internally and not set directly by users. + * + * @type {Object} + * @default - + * @private + */ + actualFilterValue?: Object; + + /** + * Stores the actual filter operator applied to the column for internal logic. + * Used by the Grid to validate and process filter operations. + * Managed internally for filter execution and state tracking. + * + * @type {Object} + * @default - + * @private + */ + actualOperator?: Object; + + /** + * Specifies the data type of the column being filtered (e.g., 'string', 'number'). + * Used internally to determine appropriate filter behavior and operator compatibility. + * Set automatically based on the column’s configuration. + * + * @type {string} + * @default - + * @private + */ + type?: string; + + /** + * Stores the predicate object for advanced filter logic and query building. + * Used internally by the Grid to construct complex filter queries. + * Not typically modified directly by users. + * + * @type {Object} + * @default - + * @private + */ + ejpredicate?: Object; + + /** + * Provides a unique identifier for the filter predicate to track its state. + * Used by the grid to manage and reference individual filter conditions. + * Ensures accurate filter application and state persistence. + * + * @type {string} + * @default - + */ + uid?: string; + + /** + * Indicates whether the column is a foreign key for relational data filtering. + * When true, enables filtering based on associated foreign key data rather than local column values. + * Used internally for handling relational data sources. + * + * @type {boolean} + * @default false + * @private + */ + isForeignKey?: boolean; + + /** + * Defines the logical condition ('and' or 'or') for combining this predicate with others. + * Used to construct complex filter queries by linking multiple predicates. + * Complements the predicate property for advanced filtering logic. + * + * @type {string} + * @default - + * @private + */ + condition?: string; +} + +/** + * Manages internal properties for the Grid’s Filter module. + * Handles filter state, operations, and configuration for filtering logic. + * Used internally to control filter behavior and performance. + * @private + */ +export interface FilterProperties { + /** + * Specifies the operator used for filtering the column (e.g., 'equal', 'contains'). + * Determines how the filter value is compared to column data during filtering. + * Set based on the filter configuration or user input. + * + * @type {string} + * @default - + */ + operator: string; + + /** + * Identifies the field name of the column being filtered in the data source. + * Maps to the column’s field property to apply the filter condition. + * Essential for targeting the correct column during filtering. + * + * @type {string} + * @default - + */ + fieldName: string; + + /** + * Enables accent-insensitive filtering for string fields. + * When true, diacritic characters are ignored, treating them as base characters. + * Aligns with the ignoreAccent setting in FilterSettings. + * + * @type {boolean} + * @default false + */ + ignoreAccent: boolean; + + /** + * Enables case-sensitive filtering for string fields. + * When true, distinguishes between uppercase and lowercase letters in string comparisons. + * Aligns with the caseSensitive setting in FilterSettings. + * + * @type {boolean} + * @default false + */ + caseSensitive: boolean; + + /** + * References the DOM element currently targeted for filtering operations. + * Used internally to track the active filter input element (e.g., filter bar input). + * Facilitates event handling and UI interactions during filtering. + * + * @type {Element | null} + * @default null + */ + currentTarget: Element | null; + + /** + * Indicates whether multi-column sorting is enabled during filtering. + * When true, allows sorting to influence filter results in multi-column scenarios. + * Used internally to coordinate sorting and filtering operations. + * + * @type {boolean} + * @default false + */ + isMultiSort: boolean; + + /** + * Contains the configuration object for the column being filtered. + * Includes metadata such as field name, data type, and column-specific settings. + * Used to apply filter logic specific to the column’s properties. + * + * @type {ColumnProps} + * @default - + */ + column: ColumnProps; + + /** + * Specifies the value used to filter the column’s data. + * Supports strings, numbers, dates, or booleans, depending on the column type. + * Used to match records against the specified operator. + * + * @type {string | number | Date | boolean} + * @default - + */ + value: string | number | Date | boolean; + + /** + * Indicates whether filtering is performed via method calls (e.g., filterByColumn). + * When true, signifies programmatic filtering rather than user-driven UI filtering. + * Used internally to differentiate filter invocation methods. + * + * @type {boolean} + * @default false + */ + filterByMethod: boolean; + + /** + * Specifies the logical predicate ('and' or 'or') for combining filter conditions. + * Used to build complex filter queries by linking multiple predicates. + * Aligns with the predicate property in FilterPredicates. + * + * @type {string} + * @default - + */ + predicate: string; + + /** + * Stores a collection of filter predicates organized by field name. + * Maps each column field to an array of predicate objects for filtering. + * Used internally to manage and apply multiple filter conditions. + * + * @type {{ [key: string]: FilterPredicates[] }} + * @default {} + */ + actualPredicate: { [key: string]: FilterPredicates[] }; + + /** + * Contains filter values for internal processing and state management. + * Stores the raw filter values applied to columns during filtering operations. + * Used by the Grid for filter execution and validation. + * + * @type {Object} + * @default {} + */ + values: Object; + + /** + * Stores cell text values used during filtering operations. + * Contains the display text or input values for filter processing. + * Used internally to manage filter bar or menu input data. + * + * @type {Object} + * @default {} + */ + cellText: Object; + + /** + * Indicates whether a refresh of the filter state is required. + * When true, triggers a re-evaluation of filter conditions or UI updates. + * Used to ensure filter consistency after changes. + * + * @type {boolean} + * @default false + */ + refresh: boolean; + + /** + * Indicates whether a refresh of the grid content is needed after filtering. + * When true, forces the grid to re-render with updated filter results. + * Used to maintain UI consistency post-filtering. + * + * @type {boolean} + * @default false + */ + contentRefresh: boolean; + + /** + * Indicates whether the grid is in its initial load state. + * When true, signifies that the grid is loading with initial filter settings. + * Used to handle first-time filter application scenarios. + * + * @type {boolean} + * @default true + */ + initialLoad: boolean; + + /** + * Stores a status message for filter operations (e.g., error or success messages). + * Used internally to communicate filter operation outcomes to the user or system. + * Helps in debugging or providing feedback during filtering. + * + * @type {string} + * @default - + */ + filterStatusMsg: string; + + /** + * Lists string input types to skip during filtering operations. + * Used to exclude specific string-based inputs from filter processing. + * Configured internally to optimize filter behavior. + * + * @type {string[]} + * @default [] + */ + skipStringInput: string[]; + + /** + * Lists number input types to skip during filtering operations. + * Used to exclude specific number-based inputs from filter processing. + * Configured internally to streamline filter logic. + * + * @type {string[]} + * @default [] + */ + skipNumberInput: string[]; + + /** + * Stores the timer value for filtering operations in 'Immediate' mode. + * Represents the delay (in milliseconds) before processing filter input. + * Used to manage the timing of filter execution. + * + * @type {number} + * @default 0 + */ + timer: number; +} + +/** + * Defines string constants for filter operators used in the Grid. + * Provides a set of operator names for various filtering operations. + * Used internally to map operators to filter logic. + * + * @private + */ +export interface IFilterOperator { + /** + * Represents the 'contains' operator for string filtering. + * Filters records where the column value contains the specified string. + * Commonly used for partial text matches. + * + * @type {string} + * @default - + */ + contains: string; + + /** + * Represents the 'ends with' operator for string filtering. + * Filters records where the column value ends with the specified string. + * Useful for suffix-based string filters. + * + * @type {string} + * @default - + */ + endsWith: string; + + /** + * Represents the 'equal' operator for filtering. + * Filters records where the column value exactly matches the specified value. + * Applicable to strings, numbers, dates, and booleans. + * + * @type {string} + * @default - + */ + equal: string; + + /** + * Represents the 'greater than' operator for filtering. + * Filters records where the column value is greater than the specified value. + * Typically used for numeric or date fields. + * + * @type {string} + * @default - + */ + greaterThan: string; + + /** + * Represents the 'greater than or equal' operator for filtering. + * Filters records where the column value is greater than or equal to the specified value. + * Used for numeric or date comparisons. + * + * @type {string} + * @default - + */ + greaterThanOrEqual: string; + + /** + * Represents the 'less than' operator for filtering. + * Filters records where the column value is less than the specified value. + * Commonly used for numeric or date fields. + * + * @type {string} + * @default - + */ + lessThan: string; + + /** + * Represents the 'less than or equal' operator for filtering. + * Filters records where the column value is less than or equal to the specified value. + * Applicable to numeric or date fields. + * + * @type {string} + * @default - + */ + lessThanOrEqual: string; + + /** + * Represents the 'not equal' operator for filtering. + * Filters records where the column value does not match the specified value. + * Supports strings, numbers, dates, and booleans. + * + * @type {string} + * @default - + */ + notEqual: string; + + /** + * Represents the 'starts with' operator for string filtering. + * Filters records where the column value starts with the specified string. + * Useful for prefix-based string filters. + * + * @type {string} + * @default - + */ + startsWith: string; + + /** + * Represents the 'is null' operator for filtering. + * Filters records where the column value is null or undefined. + * Applicable to any column type. + * + * @type {string} + * @default - + */ + isNull: string; + + /** + * Represents the 'not null' operator for filtering. + * Filters records where the column value is not null or undefined. + * Applicable to any column type. + * + * @type {string} + * @default - + */ + isNotNull: string; + + /** + * Represents the 'wildcard' operator for string filtering. + * Filters records using wildcard patterns (e.g., '*' or '?') in the filter value. + * Used for advanced string matching scenarios. + * + * @type {string} + * @default - + */ + wildCard: string; + + /** + * Represents the 'like' operator for string filtering. + * Filters records using pattern-based matching similar to SQL LIKE. + * Supports partial matches with wildcards. + * + * @type {string} + * @default - + */ + like: string; +} + +/** + * Defines the API for managing filtering operations in the Grid. + * Provides methods and properties to control filter behavior, state, and events. + * Used internally to handle filter interactions and updates. + * + * @private + */ +export interface FilterAPI { + /** + * Applies a filter to a specific column with the given criteria. + * Uses the specified field name, operator, and value to filter grid rows, with optional logical predicates. + * Supports case-sensitive and accent-insensitive filtering options. + * + * @param {string} fieldName - The column field to filter. + * @param {string} filterOperator - The operator for filtering (e.g., 'equal', 'contains'). + * @param {string | number | Date | boolean | number[] | string[] | Date[] | boolean[]} filterValue - The value(s) to filter against. + * @param {string} [predicate] - Logical operator ('and'/'or') for combining filters. + * @param {boolean} [caseSensitive] - Enables case-sensitive string filtering. + * @param {boolean} [ignoreAccent] - Enables accent-insensitive string filtering. + * @returns {void} + */ + filterByColumn(fieldName: string, filterOperator: string, + filterValue: string | number | Date | boolean | number[] | string[] | Date[] | boolean[], + predicate?: string, caseSensitive?: boolean, ignoreAccent?: boolean): void; + + /** + * Clears all filter conditions from the grid or specific columns. + * Resets the filter state, removing all or specified column filters, and refreshes the grid. + * Optionally clears filter bar input values. + * + * @param {string[]} [fields] - Array of field names to clear filters from; if omitted, clears all filters. + * @returns {void} + */ + clearFilter(fields?: string[]): void; + + /** + * Removes the filter condition for a specific column by its field name. + * Optionally clears the filter bar input value for the column. + * Used to reset filtering for individual columns without affecting others. + * + * @param {string} [field] - The field name of the column to remove the filter from. + * @param {boolean} [isClearFilterBar] - If true, clears the filter bar input value. + * @returns {void} + */ + removeFilteredColsByField(field?: string, isClearFilterBar?: boolean): void; + + /** + * Handles keyboard input events during filter operations. + * Processes key presses (e.g., Enter) to trigger filtering in the filter bar or menu. + * Used to enhance user interaction with the filter UI. + * + * @param {React.KeyboardEvent} event - The keyboard event object. + * @returns {void} + */ + keyUpHandler: (event: React.KeyboardEvent) => void; + + /** + * Handles mouse down events during filter operations. + * Processes mouse clicks in the filter UI (e.g., filter menu selection) to initiate filtering. + * Used to manage user interactions with filter controls. + * + * @param {React.MouseEvent} event - The mouse event object. + * @returns {void} + */ + mouseDownHandler: (event: React.MouseEvent) => void; + + /** + * Stores the current filter settings configuration for the grid. + * Contains all filter-related properties, such as enabled state, type, and predicates. + * Used to access or update the grid’s filter state. + * + * @type {FilterSettings} + * @default {} + */ + filterSettings: FilterSettings; + + /** + * Provides a function to update the filter settings state. + * Used with React’s useState to programmatically modify filter configurations. + * Enables dynamic updates to filter behavior or UI. + * + * @type {Dispatch>} + * @default - + */ + setFilterSettings: Dispatch>; +} + +/** + * Represents the FilterAPI interface for filter operations in the Grid. + * Defines the contract for the Filter module’s functionality and state management. + * Used internally to encapsulate filtering logic. + * + * @private + */ +export type filterModule = FilterAPI; diff --git a/components/grids/src/grid/types/focus.interfaces.ts b/components/grids/src/grid/types/focus.interfaces.ts new file mode 100644 index 0000000..ba03f7f --- /dev/null +++ b/components/grids/src/grid/types/focus.interfaces.ts @@ -0,0 +1,796 @@ +import { SyntheticEvent, MouseEvent, KeyboardEvent } from 'react'; +import { IRow } from '../types'; +import { ColumnProps } from '../types/column.interfaces'; +import { useFocusStrategy } from '../hooks'; + +/** + * Defines the type for the focus strategy module in the Grid. + * Represents the return type of the useFocusStrategy hook for managing focus navigation. + * Used internally to encapsulate focus-related functionality. + * + * @private + */ +export type FocusStrategyModule = ReturnType; + +/** + * Represents the matrix object for managing focusable cells in the Grid. + * Provides a grid-like structure to track and navigate focusable cells in content, header, or aggregate sections. + * Used internally to facilitate keyboard navigation and cell focus operations. + * + * @private + */ +export interface IFocusMatrix { + /** + * Stores a two-dimensional array representing the focusable state of cells in the grid. + * Each element indicates whether a cell at [rowIndex, columnIndex] is focusable (e.g., 1 for focusable, 0 for not). + * Used to map the grid’s structure for navigation and focus management. + * + * @default [] + */ + matrix: number[][]; + + /** + * Specifies the current position in the matrix as an array of [rowIndex, columnIndex]. + * Tracks the currently focused cell’s coordinates for navigation operations. + * Updated dynamically as focus moves within the grid. + * + * @default [] + */ + current: number[]; + + /** + * Indicates the total number of columns in the focus matrix. + * Reflects the width of the grid’s focusable structure, corresponding to visible columns. + * Used to define boundaries for navigation calculations. + * + * @default 0 + */ + columns: number; + + /** + * Indicates the total number of rows in the focus matrix. + * Reflects the height of the grid’s focusable structure, corresponding to visible rows. + * Used to define boundaries for navigation calculations. + * + * @default 0 + */ + rows: number; + + /** + * Updates the focusable state of a specific cell in the matrix. + * Sets whether the cell at the given row and column indices is focusable, with an optional boolean flag. + * Used to dynamically configure the focusable structure of the grid. + * + * @param {number} rowIndex - The row index of the cell. + * @param {number} columnIndex - The column index of the cell. + * @param {boolean} allow - Optional. Whether the cell should be focusable. + * @returns {void} + */ + set: (rowIndex: number, columnIndex: number, allow?: boolean) => void; + + /** + * Retrieves the coordinates of the next valid focusable cell based on navigation parameters. + * Uses the current position, navigation direction, and optional validation to determine the next cell to focus. + * Supports complex navigation scenarios, such as skipping non-focusable cells. + * + * @param {number} rowIndex - The current row index. + * @param {number} columnIndex - The current column index. + * @param {number[]} navigator - Navigation direction array. + * @param {string} action - Optional. The navigation action. + * @param {Function} validator - Optional. Function to validate cell selection. + * @param {Object} active - Optional. Active element information. + * @returns {number[]} The next valid cell coordinates. + */ + get: (rowIndex: number, columnIndex: number, navigator: number[], action?: string, validator?: Function, + active?: Object) => number[]; + + /** + * Selects a specific cell in the matrix to receive focus. + * Updates the grid’s focus state to highlight the cell at the given row and column indices. + * Used for programmatic focus changes or user-driven navigation. + * + * @param {number} rowIndex - The row index of the cell to select. + * @param {number} columnIndex - The column index of the cell to select. + * @returns {void} + */ + select: (rowIndex: number, columnIndex: number) => void; + + /** + * Generates a focus matrix from the grid’s row data. + * Creates a two-dimensional array of focusable states based on row data and a selector function. + * Supports row templates for customized focus behavior in complex grid layouts. + * + * @param {IRow[]} rowsData - Array of row data. + * @param {Function} selector - Function to select focusable cells. + * @param {boolean} isRowTemplate - Optional. Whether using row template. + * @returns {number[][]} The generated matrix. + */ + generate: (rowsData: IRow[], selector: Function, isRowTemplate?: boolean) => number[][]; + + /** + * Checks whether a cell’s value in the matrix is invalid (0 or undefined). + * Determines if the cell at the given value is non-focusable, aiding navigation logic. + * Used to skip non-focusable cells during focus traversal. + * + * @param {number} value - The cell value to check. + * @returns {boolean} True if the value is invalid. + */ + inValid: (value: number) => boolean; + + /** + * Finds the index of the first valid focusable cell in a vector (row or column). + * Searches the specified vector starting from the given index, using navigation direction and optional action. + * Supports moving focus to the identified cell if specified. + * + * @param {number[]} vector - The vector to search. + * @param {number} index - The starting index. + * @param {number[]} navigator - Navigation direction array. + * @param {boolean} moveTo - Optional. Whether to move to the cell. + * @param {string} action - Optional. The navigation action. + * @returns {number} The index of the first valid cell. + */ + first: (vector: number[], index: number, navigator: number[], moveTo?: boolean, action?: string) => number; + + /** + * Finds the next or previous valid focusable cell index in the matrix. + * Determines the coordinates of the next or previous focusable cell based on the current position and direction. + * Used to support sequential navigation through focusable cells. + * + * @param {number[]} checkCellIndex - Current cell index to check. + * @param {boolean} next - Whether to find the next cell (true) or previous (false). + * @returns {number[]} The coordinates of the found cell. + */ + findCellIndex: (checkCellIndex: number[], next: boolean) => number[]; +} + +/** + * Defines event arguments for cell focus events in the Grid. + * Provides detailed context about the focused cell, including its position, data, and triggering event. + * Used to handle focus-related interactions and updates. + */ +export interface CellFocusEvent { + /** + * Specifies the zero-based row index of the focused cell in the grid. + * Identifies the row position of the cell that has received focus. + * Used for tracking or processing the focused row’s context. + * + * @default - + */ + rowIndex: number; + + /** + * Specifies the zero-based column index of the focused cell in the grid. + * Identifies the column position of the cell that has received focus. + * Used for tracking or processing the focused column’s context. + * + * @default - + */ + columnIndex: number; + + /** + * References the DOM element of the focused cell. + * Provides access to the cell’s HTMLElement for manipulation or inspection. + * Used internally to manage focus state and UI updates. + * + * @private + */ + element?: HTMLElement; + + /** + * Contains the data object associated with the focused cell’s row. + * Provides access to the row’s record data for processing or display. + * Useful for retrieving context about the focused cell’s content. + * + * @default null + */ + rowData?: Object; + + /** + * Contains the column configuration associated with the focused cell. + * Provides metadata such as field name, data type, or formatting for the column. + * Enables access to column-specific properties during focus events. + * + * @default null + */ + column?: ColumnProps; + + /** + * Contains the synthetic event object that triggered the focus event. + * Provides details about the user action (e.g., click or keypress) that caused the focus change. + * Useful for advanced event handling or custom logic. + * + * @default null + */ + event?: SyntheticEvent; + + /** + * References the parent DOM element of the focused cell. + * Typically the row or container element housing the cell, used for context or navigation. + * Used internally to manage focus within the grid’s structure. + * + * @private + */ + parent?: HTMLElement; + + /** + * Contains an array of cell indexes, typically [rowIndex, colIndex], for the focused cell. + * Provides a structured way to access the focused cell’s coordinates. + * Used for navigation or state tracking during focus operations. + * + * @default [] + * @private + */ + indexes?: number[]; + + /** + * Indicates whether the focus event was triggered by keyboard navigation. + * When true, signifies that a keypress (e.g., arrow keys) caused the focus change. + * Used internally to differentiate between keyboard and mouse interactions. + * + * @private + * @default false + */ + byKey?: boolean; + + /** + * Indicates whether the focus event was triggered by a mouse click. + * When true, signifies that a click action caused the focus change. + * Used internally to differentiate between click and keyboard interactions. + * + * @private + * @default false + */ + byClick?: boolean; + + /** + * Contains arguments for the keyboard event that triggered the focus, if applicable. + * Provides details about the keypress, such as key code or modifiers, for advanced handling. + * Used to process keyboard-driven focus navigation. + * + * @default null + * @private + */ + keyArgs?: Object; + + /** + * Indicates whether the focus change involves a jump to a non-adjacent cell. + * When true, signifies a non-sequential focus move, such as to a specific cell or section. + * Used to handle special navigation cases like home/end key actions. + * + * @default false + * @private + */ + isJump?: boolean; + + /** + * Contains information about the container of the focused cell, such as content or header. + * Provides context about the grid section (e.g., content, header, aggregate) where focus resides. + * Used to manage focus within specific grid areas. + * + * @default null + * @private + */ + container?: Object; + + /** + * Determines whether the focus outline is displayed for the focused cell. + * When true, shows a visual focus indicator. When false, suppresses the outline for styling. + * Enhances the user experience by controlling focus visibility. + * + * @default true + * @private + */ + outline?: boolean; + + /** + * Contains information about matrix swapping during focus navigation. + * Provides details about transitions between different matrix types (e.g., content to header). + * Used internally to manage focus across grid sections. + * + * @default null + * @private + */ + swapInfo?: Object; + + /** + * Determines whether the focus action should be prevented. + * When set to true, cancels the focus change, allowing validation or conditional logic. + * Used in event handlers to control focus behavior. + * + * @private + */ + cancel?: boolean; +} + +/** + * Enumerates the matrix types used for focus navigation in the Grid. + * Defines the grid sections (content, header, or aggregate) for focus management. + * Used internally to differentiate focus contexts during navigation. + * + * @private + */ +export type Matrix = 'content' | 'header' | 'aggregate'; + +/** + * Defines information for swapping focus between matrices during navigation in the Grid. + * Specifies details about transitions between grid sections, such as content or header. + * Used internally to manage focus movement across different matrix types. + * + * @private + */ +export interface SwapInfo { + /** + * Indicates whether a matrix swap is required during focus navigation. + * When true, triggers a transition to a different matrix type (e.g., from content to header). + * Used to manage focus movement between grid sections. + * + * @default false + */ + swap?: boolean; + + /** + * Indicates whether focus should move to the header matrix. + * When true, directs navigation to the header section of the grid. + * Used to control transitions to column headers during focus navigation. + * + * @default false + */ + toHeader?: boolean; + + /** + * Specifies the target matrix type for focus navigation. + * Defines the grid section (content, header, or aggregate) to move focus to. + * Guides the focus system to the appropriate matrix during swaps. + * + * @default 'content' + */ + toMatrix?: Matrix; +} + +/** + * Defines the return value of the useFocusStrategy hook in the Grid. + * Provides methods and properties for managing focus navigation, cell selection, and grid focus state. + * Used internally to control focus behavior and interactions within the grid. + * + * @private + */ +export interface FocusStrategyResult { + /** + * Retrieves information about the currently focused cell in the grid. + * Returns details such as row and column indexes, element references, and focus context. + * Used to access the current focus state for processing or UI updates. + * + * @returns {FocusedCellInfo} The focused cell information. + */ + getFocusedCell: () => FocusedCellInfo; + + /** + * Indicates whether the grid currently has focus. + * When true, signifies that the grid or one of its elements is actively focused; when false, indicates no focus. + * Used to track the grid’s focus state for navigation or interaction handling. + * + * @default false + */ + isGridFocused: boolean; + + /** + * Indicates whether the current focus was triggered by a mouse click event. + * When true, signifies that a click caused the focus change; when false, indicates another trigger like keyboard input. + * Used to differentiate between click-based and other focus mechanisms. + * + * @default false + */ + focusByClick: boolean; + + /** + * Sets the focus state of the grid. + * Updates whether the grid or its elements should be focused, controlling the active focus state. + * Used to programmatically manage focus for accessibility or user interactions. + * + * @param {boolean} isFocused - Whether the grid should be focused. + * @returns {void} + */ + setGridFocus: (isFocused: boolean) => void; + + /** + * Retrieves the focus matrix for the grid’s content area. + * Returns the matrix representing focusable cells in the content section, used for navigation. + * Enables focus management within the grid’s data rows. + * + * @returns {IFocusMatrix} The content focus matrix. + */ + getContentMatrix: () => IFocusMatrix; + + /** + * Retrieves the focus matrix for the grid’s header area. + * Returns the matrix representing focusable cells in the header section, used for column header navigation. + * Enables focus management within the grid’s header elements. + * + * @returns {IFocusMatrix} The header focus matrix. + */ + getHeaderMatrix: () => IFocusMatrix; + + /** + * Retrieves the focus matrix for the grid’s aggregate area. + * Returns the matrix representing focusable cells in the aggregate (e.g., footer) section, used for summary row navigation. + * Enables focus management within the grid’s aggregate elements. + * + * @returns {IFocusMatrix} The aggregate focus matrix. + */ + getAggregateMatrix: () => IFocusMatrix; + + /** + * Retrieves the currently active focus matrix for navigation. + * Returns the matrix (content, header, or aggregate) currently used for focus operations. + * Used to determine the active focus context within the grid. + * + * @returns {IFocusMatrix} The active focus matrix. + */ + getActiveMatrix: () => IFocusMatrix; + + /** + * Sets the active matrix type for focus navigation. + * Updates the focus context to the specified matrix type (content, header, or aggregate). + * Used to switch focus navigation between different grid sections. + * + * @param {Matrix} matrixType - The matrix type to set as active. + * @returns {void} + */ + setActiveMatrix: (matrixType: Matrix) => void; + + /** + * Sets focus to the grid or a specific cell, optionally triggered by a keyboard event. + * Activates focus on the grid or a targeted cell, updating the UI and focus state. + * Used for programmatic focus changes or user-driven navigation. + * + * @param {KeyboardEvent} e - Optional. The keyboard event that triggered focus. + * @returns {void} + */ + focus: (e?: KeyboardEvent) => void; + + /** + * Removes focus from the grid. + * Clears the current focus state, removing focus from any cell or grid element. + * Used to reset focus for UI updates or navigation transitions. + * + * @returns {void} + */ + removeFocus: () => void; + + /** + * Removes the tab index from all focusable elements in the grid. + * Disables keyboard navigation by clearing tab index attributes, preventing focus via Tab key. + * Used to control accessibility behavior or focus management. + * + * @returns {void} + */ + removeFocusTabIndex: () => void; + + /** + * Applies focus to a specific cell based on provided cell information. + * Updates the focus state to highlight the specified cell, optionally triggered by a keyboard event. + * Used for precise focus control in response to user or programmatic actions. + * + * @param {FocusedCellInfo} info - The cell information to focus. + * @param {KeyboardEvent} e - Optional. The keyboard event that triggered focus. + * @returns {void} + */ + addFocus: (info: FocusedCellInfo, e?: KeyboardEvent) => void; + + /** + * Retrieves detailed information about the current focus state. + * Returns the FocusedCellInfo object with details like row and column indexes, element references, and context. + * Used to access the current focus for processing or UI updates. + * + * @returns {FocusedCellInfo} The current focus information. + */ + getFocusInfo: () => FocusedCellInfo; + + /** + * Sets the tab index for the first focusable element in the grid. + * Ensures the first focusable cell or element is accessible via keyboard navigation. + * Enhances accessibility by defining the initial focus point for Tab key navigation. + * + * @returns {void} + */ + setFirstFocusableTabIndex: () => void; + + /** + * Sets focus to the content area of the grid. + * Moves focus to the content section, typically the first focusable cell in the data rows. + * Used to initiate focus navigation within the grid’s main data area. + * + * @returns {void} + */ + focusContent: () => void; + + /** + * Applies a focus outline to the currently focused cell. + * Adds a visual indicator (e.g., CSS class) to highlight the focused cell in the UI. + * Enhances user experience by visually marking the active cell. + * + * @returns {void} + */ + addOutline: () => void; + + /** + * Removes the focus outline or indicator from the currently focused cell. + * Clears any visual markers (e.g., CSS class) used to highlight the focused cell. + * Used to reset the UI or manage focus visibility. + * + * @returns {void} + */ + clearIndicator: () => void; + + /** + * Processes keyboard events to handle focus navigation within the grid. + * Interprets keypresses (e.g., arrow keys, Tab) to move focus between cells or sections. + * Enhances accessibility by enabling keyboard-driven navigation. + * + * @param {KeyboardEvent} event - The keyboard event. + * @returns {void} + */ + handleKeyDown: (event: KeyboardEvent) => void; + + /** + * Processes mouse click events to manage focus within the grid. + * Updates the focus state based on user clicks on cells or other focusable elements. + * Used to handle click-based focus changes and UI updates. + * + * @param {MouseEvent} event - The mouse event. + * @returns {void} + */ + handleGridClick: (event: MouseEvent) => void; + + /** + * Navigates focus to a specific cell identified by row and column indexes. + * Moves focus to the specified cell, optionally within a given matrix type (content, header, or aggregate). + * Used for precise programmatic navigation to a target cell. + * + * @param {number} rowIndex - The row index to navigate to. + * @param {number} colIndex - The column index to navigate to. + * @param {Matrix} matrixType - Optional. The matrix type containing the cell. + * @returns {void} + */ + navigateToCell: (rowIndex: number, colIndex: number, matrixType?: Matrix) => void; + + /** + * Navigates focus to the next cell in the specified direction. + * Moves focus based on directions like 'up', 'down', 'left', 'right', 'nextCell', or 'prevCell'. + * Used for sequential navigation through focusable cells in the grid. + * + * @param {'up' | 'down' | 'left' | 'right' | 'nextCell' | 'prevCell'} direction - The navigation direction. + * @returns {void} + */ + navigateToNextCell: (direction: 'up' | 'down' | 'left' | 'right' | 'nextCell' | 'prevCell') => void; + + /** + * Navigates focus to the first focusable cell in the grid. + * Moves focus to the initial focusable cell, typically in the content or header matrix. + * Used for quick navigation to the starting point of focusable elements. + * + * @returns {void} + */ + navigateToFirstCell: () => void; + + /** + * Navigates focus to the last focusable cell in the grid. + * Moves focus to the final focusable cell, typically in the content or aggregate matrix. + * Used for quick navigation to the end of focusable elements. + * + * @returns {void} + */ + navigateToLastCell: () => void; + + /** + * Determines whether a keyboard event corresponds to a navigation key. + * Checks if the event involves keys like arrow keys, Tab, or Home/End for focus navigation. + * Used to filter relevant keypresses for navigation handling. + * + * @param {KeyboardEvent} event - The keyboard event to check. + * @returns {boolean} True if it's a navigation key. + */ + isNavigationKey: (event: KeyboardEvent) => boolean; + + /** + * Determines the navigation direction from a keyboard event. + * Maps keypresses (e.g., arrow keys) to direction strings like 'up', 'down', 'left', or 'right'. + * Used to interpret user intent for focus navigation. + * + * @param {KeyboardEvent} event - The keyboard event. + * @returns {string} The navigation direction string. + */ + getNavigationDirection: (event: KeyboardEvent) => string; + + /** + * Retrieves the indexes of the previously focused cell. + * Returns an object with row and cell indexes of the prior focused cell, if applicable. + * Used to track focus history for navigation or state management. + * + * @returns {{ rowIndex?: number, cellIndex?: number }} The previous indexes. + */ + getPrevIndexes: () => { rowIndex?: number, cellIndex?: number }; + + /** + * Stores the index of the first focusable cell in the content area as [rowIndex, colIndex]. + * Identifies the starting point for focus navigation in the grid’s data rows. + * Used to initialize focus or navigate to the first content cell. + * + * @default [] + */ + firstFocusableContentCellIndex: number[]; + + /** + * Stores the index of the first focusable cell in the header area as [rowIndex, colIndex]. + * Identifies the starting point for focus navigation in the grid’s column headers. + * Used to initialize focus or navigate to the first header cell. + * + * @default [] + */ + firstFocusableHeaderCellIndex: number[]; + + /** + * Stores the index of the last focusable cell in the content area as [rowIndex, colIndex]. + * Identifies the ending point for focus navigation in the grid’s data rows. + * Used to navigate to the final content cell. + * + * @default [] + */ + lastFocusableContentCellIndex: number[]; + + /** + * Stores the index of the last focusable cell in the header area as [rowIndex, colIndex]. + * Identifies the ending point for focus navigation in the grid’s column headers. + * Used to navigate to the final header cell. + * + * @default [] + */ + lastFocusableHeaderCellIndex: number[]; + + /** + * Stores the index of the first focusable cell in the aggregate area as [rowIndex, colIndex]. + * Identifies the starting point for focus navigation in the grid’s summary or footer rows. + * Used to initialize focus or navigate to the first aggregate cell. + * + * @default [] + */ + firstFocusableAggregateCellIndex: number[]; + + /** + * Stores the index of the last focusable cell in the aggregate area as [rowIndex, colIndex]. + * Identifies the ending point for focus navigation in the grid’s summary or footer rows. + * Used to navigate to the final aggregate cell. + * + * @default [] + */ + lastFocusableAggregateCellIndex: number[]; +} + +/** + * Defines callback functions for focus-related events in the Grid. + * Specifies handlers for cell focus, click, and pre-focus events to customize focus behavior. + * Used internally to manage focus-related interactions. + * + * @private + */ +export interface FocusStrategyCallbacks { + /** + * Invoked when a cell receives focus in the grid. + * Provides event arguments to process or respond to the focus change, such as updating UI or state. + * Used to handle post-focus logic for user or programmatic interactions. + * + * @param {CellFocusEvent} args - The cell focus event arguments. + * @event cellFocused + */ + onCellFocus?: (args: CellFocusEvent) => void; + + /** + * Invoked when a cell is clicked in the grid. + * Provides event arguments to process click-based focus changes or trigger related actions. + * Used to handle user interactions that involve clicking cells. + * + * @param {CellFocusEvent} args - The cell focus event arguments. + * @event cellClick + */ + onCellClick?: (args: CellFocusEvent) => void; + + /** + * Invoked before a cell receives focus, allowing cancellation of the focus action. + * Provides event arguments to validate or modify the focus change before it occurs. + * Used to control focus behavior with conditional logic. + * + * @param {CellFocusEvent} args - The cell focus event arguments. + * @event beforeCellFocus + */ + beforeCellFocus?: (args: CellFocusEvent) => void; +} + +/** + * Defines information about a focused cell in the Grid. + * Provides detailed context about the cell’s position, DOM element, and focus properties. + * Used internally to manage focus state and navigation. + * + * @private + */ +export interface FocusedCellInfo { + /** + * Specifies the zero-based row index of the focused cell. + * Identifies the row position of the cell within the grid’s data or structure. + * Used to track the focused cell’s location for navigation or processing. + * + * @default - + */ + rowIndex: number; + + /** + * Specifies the zero-based column index of the focused cell. + * Identifies the column position of the cell within the grid’s data or structure. + * Used to track the focused cell’s location for navigation or processing. + * + * @default - + */ + colIndex: number; + + /** + * Indicates whether the focused cell is located in the header section of the grid. + * When true, signifies the cell is part of the column headers; when false, indicates content or aggregate. + * Used to determine the focus context within the grid’s structure. + * + * @default false + */ + isHeader: boolean; + + /** + * Indicates whether the focused cell is located in the aggregate section of the grid. + * When true, signifies the cell is part of the summary or footer rows; when false, indicates content or header. + * Used to determine the focus context within summary sections. + * + * @default false + */ + isAggregate?: boolean; + + /** + * References the DOM element of the focused cell. + * Provides access to the cell’s HTMLElement for manipulation, styling, or focus management. + * Used to update the UI or interact with the focused cell. + * + * @default null + */ + element?: HTMLElement; + + /** + * References the specific element within the cell that should receive focus. + * Identifies a sub-element (e.g., input or button) within the cell for precise focus targeting. + * Used to handle complex cells with focusable components. + * + * @default null + */ + elementToFocus?: HTMLElement; + + /** + * Specifies a unique identifier for the focused cell. + * Used to track the cell across operations or for state management within the grid. + * Ensures accurate reference to the focused cell in dynamic scenarios. + * + * @default - + */ + uid?: string; + + /** + * Determines whether the focus action for the cell should be skipped. + * When true, prevents the cell from receiving focus, allowing navigation to bypass it. + * Used to control focus behavior in specific scenarios. + * + * @default false + */ + skipAction?: boolean; + + /** + * Determines whether to apply a focus outline (e.g., sf-focused CSS class) to the cell. + * When true, displays a visual indicator for the focused cell; when false, suppresses the outline. + * Enhances user experience by controlling focus visibility. + * + * @default true + */ + outline?: boolean; +} diff --git a/components/grids/src/grid/types/grid.interfaces.ts b/components/grids/src/grid/types/grid.interfaces.ts new file mode 100644 index 0000000..48a9cbd --- /dev/null +++ b/components/grids/src/grid/types/grid.interfaces.ts @@ -0,0 +1,2173 @@ +import { HTMLAttributes, ReactElement, ReactNode } from 'react'; +import { DataManager, DataResult, Query, ReturnType as DataReturnType } from '@syncfusion/react-data'; +import { IL10n } from '@syncfusion/react-base'; +import { HeaderCellRenderEvent, CellRenderEvent, RowRenderEvent, ServiceLocator } from '../types/interfaces'; +import { GridLine, SortDirection, WrapMode, Action, ClipMode, ValueType } from './index'; +import { FilterSettings, FilterEvent } from '../types/filter.interfaces'; +import { ColumnProps } from '../types/column.interfaces'; +import { CellFocusEvent } from '../types/focus.interfaces'; +import { EditSettings, FormRenderEvent, RowEditEvent, DeleteEvent, + RowAddEvent, SaveEvent, CancelFormEvent } from '../types/edit.interfaces'; +import { AggregateCellRenderEvent, AggregateRowRenderEvent, AggregateRowProps } from '../types/aggregate.interfaces'; +import { ToolbarClickEvent, ToolbarItemConfig } from '../types/toolbar.interfaces'; +import { RenderRef, MutableGridBase, DataChangeRequestEvent, DataRequestEvent, IRowBase, IValueFormatter } from '../types/interfaces'; +import { RowSelectEvent, RowSelectingEvent, SelectionSettings } from '../types/selection.interfaces'; +import { PageEvent, PageSettings } from '../types/page.interfaces'; +import { SortEvent, SortSettings } from '../types/sort.interfaces'; +import { SearchEvent, SearchSettings } from '../types/search.interfaces'; + +/** + * Represents information about a specific row and cell in the grid. + * Used for identifying row/cell context during events and operations. + */ +export interface RowInfo { + /** + * Represents the element of a cell within a grid row. + * + * This element provides access to the cell's DOM properties and methods. + * + * @default null + */ + cell?: Element; + + /** + * Specifies the zero-based index of the cell within its parent row. + * + * Used for identifying the cell's position in the row. + * + * @default null + */ + columnIndex?: number; + + /** + * Represents the element of the row within the grid. + * + * This element provides access to the row's DOM properties and methods. + * + * @default null + */ + row?: Element; + + /** + * Specifies the zero-based index of the row within the grid. + * + * Used for identifying the row's position in the grid. + * + * @default null + */ + rowIndex?: number; + + /** + * Contains the data object associated with the row. + * + * This object holds the row's data, which can be used for rendering or processing. + * + * @default null + */ + rowData?: Object; + + /** + * Provides configuration for the column associated with the cell. + * + * Such as column name or formatting rules. + * + * @default null + */ + column?: ColumnProps; +} + +/** + * Defines the structure of the error event triggered during grid operations. + * + * Provides structured error information for handling failures that occur during grid actions, + * supporting consistent reporting and resolution workflows in component logic. + */ +export interface GridErrorEvent { + /** + * Represents the error instance containing details about the failure. + * + * Includes the error message and metadata required for centralized error handling, + * logging, and operational diagnostics within grid-based components. + * + * @default null + */ + error: Error; +} + +/** + * Interface for Grid component reference containing all imperative methods and properties. + * Provides access to grid instance methods and current state information. + * + * @private + */ +export interface GridRef extends Omit, IGrid, MutableGridBase { + /** + * Reference to the grid's root DOM element. + * + * @default null + */ + element?: HTMLDivElement | null; + + /** + * Current view data available in the grid. + * + * @default [] + */ + currentViewData?: Object[]; + + /** + * Defines the selected row indexes. + * + * @default [] + */ + selectedRowIndexes?: number[]; + + /** + * Whether the grid is currently in edit mode. + * + * @default false + */ + isEdit?: boolean; + + /** + * Index of the row being edited. + * + * @default - + */ + editRowIndex?: number; + + /** + * Data of the row being edited. + * + * @default null + */ + editData?: Record; +} + +/** + * @private + */ +export interface GridProps extends Omit, 'children' | 'onError'> { + /** + * Specifies a unique identifier for the grid component. + * Provides a distinct ID for the grid instance, enabling targeted interactions, styling, or accessibility features. + * Used to differentiate multiple grid instances within the same application or DOM. + * + * @example + * ```tsx + * + * ``` + */ + id?: string; + + /** + * Supplies the data to be displayed in the grid. + * + * The data source can be provided as: + * * An array of JavaScript objects + * * A `DataManager` instance for local/remote data operations + * * A `DataResult` object with processed data + * + * The grid will automatically bind to this data and render rows based on the provided records. + * + * @default [] + * + * @example + * ```tsx + * import React from 'react'; + * import { Grid } from '@company/react-grid'; + * + * const GridExample: React.FC = () => { + * // Local data array + * const employees = [ + * { id: 1, name: 'John Doe', role: 'Developer', salary: 75000 }, + * { id: 2, name: 'Jane Smith', role: 'Designer', salary: 65000 }, + * ]; + * + * return ( + * + * ); + * }; + * ``` + */ + dataSource?: Object[] | DataManager | DataResult; + + /** + * Defines the columns to be displayed in the grid. + * + * An array of ColumnProps objects that specify how each column in the grid should be configured. + * This includes properties like `field`, `headerText`, `width`, `format`, and more. + * The order of columns in the array determines their display order in the grid. + * + * @default [] + * + * @example + * ```tsx + * + * ``` + */ + columns?: ColumnProps[]; + + /** + * Sets the height of the grid component. + * + * Controls the vertical size of the grid. Can be specified as: + * * A number (interpreted as pixels). + * * A string with CSS units (e.g., '500px', '100%'). + * * `auto` to adjust to content. + * + * When a fixed height is set, scrollbars appear automatically when content exceeds the height. + * + * @default 'auto' + * + * @example + * ```tsx + * + * ``` + */ + height?: number | string; + + /** + * Sets the width of the grid component. + * + * Controls the horizontal size of the grid. Can be specified as: + * * A number (interpreted as pixels). + * * A string with CSS units (e.g., '800px', '100%'). + * * `auto` to adjust to parent container. + * + * When a fixed width is set, horizontal scrollbars appear automatically when content exceeds the width. + * + * @default 'auto' + * + * @example + * ```tsx + * + * ``` + */ + width?: number | string; + + /** + * Configures the visibility of grid lines between cells. + * + * Determines which grid lines are displayed in the grid. Available options are: + * * `Default`: Shows horizontal lines only. + * * `None`: Displays no grid lines. + * * `Both`: Shows both horizontal and vertical grid lines. + * * `Horizontal`: Shows horizontal lines only. + * * `Vertical`: Shows vertical lines only. + * + * @default 'Default' + * + * @example + * ```tsx + * + * ``` + */ + gridLines?: GridLine | string; + + /** + * Controls whether hover effect is applied to grid rows. + * + * By default, rows are visually highlighted on pointer hover. + * When set to false, rows retain a static appearance regardless of pointer hover movement. + * + * @default true + * + * @example + * ```tsx + * + * ``` + */ + enableHover?: boolean; + + /** + * Controls whether keyboard navigation is enabled for the Grid. + * + * By default, navigation and interaction with grid elements can be performed using keyboard shortcuts and arrow keys. + * When set to false, the grid's default focus navigation behavior is disable + * + * @default true + * + * @example + * ```tsx + * + * ``` + */ + allowKeyboard?: boolean; + + /** + * Defines the cell content's overflow mode. The available modes are + * * `Clip` - Truncates the cell content when it overflows its area. + * * `Ellipsis` - Displays ellipsis when the cell content overflows its area. + * * `EllipsisWithTooltip` - Applies an ellipsis to overflowing cell content and displays a tooltip on hover for enhanced readability. + * + * @default ClipMode.Ellipsis | 'Ellipsis' + */ + clipMode?: ClipMode | string; + + /** + * Determines whether the sf-altrow CSS class is added to alternate rows in the Grid. + * + * When set to true, the grid adds the sf-altrow class to alternate row elements. + * This supports alternating row styles, which can improve readability in data-dense layouts. + * The grid does not apply any default styling for this class. Styling must be defined externally. + * + * When set to false, the grid does not add the sf-altrow class to any row. + * + * @default true + * + * @example + * ```tsx + * + * + * // External CSS + * .sf-altrow { + * background-color: #f5f5f5; + * } + * ``` + */ + enableAltRow?: boolean; + + /** + * Enables right-to-left (RTL) direction for the grid. + * + * When set to true, the grid's layout changes to support right-to-left languages like Arabic. + * This includes reversing the direction of UI elements, text alignment, and scrollbars. + * + * @private + * @default false + * @example + * ```tsx + * + * ``` + */ + enableRtl?: boolean; + + /** + * Configures the grid's selection settings, determines whether single or multiple selections are allowed. + * Used to customize the selection experience for user interactions. + * + * @default { mode: 'Single', enableToggle: true } + * + * @example + * ```tsx + * + * ``` + */ + selectionSettings?: SelectionSettings; + + /** + * Specifies the sorting configuration for the grid, includes options to enable/disable sorting and controlling how data is ordered. + * Used to customize sorting behavior for data presentation and user interactions. + * + * @default { columns: [] } + * + * @example + * ```tsx + * + * ``` + */ + sortSettings?: SortSettings; + + /** + * Specifies the filtering configuration for the grid, controlling the filter UI and behavior. + * Includes options to enable/disable filtering, set the filter UI type, define custom operators, and configure case or accent sensitivity. + * Used to tailor the filtering experience to match application requirements and data types. + * + * @default - + * + * @example + * ```tsx + * + * ``` + */ + filterSettings?: FilterSettings; + + /** + * Specifies the search configuration for the grid, controlling how data is searched. + * Defines settings for enabling the search bar, specifying searchable fields, initial search terms, operators, and case/accent sensitivity. + * Used to customize the search experience for filtering grid data. + * + * @default { ignoreCase: true, fields: [] } + * + * @example + * ```tsx + * + * ``` + */ + searchSettings?: SearchSettings; + + /** + * Specifies the pagination configuration for the grid, controlling how data is divided and navigated. + * Includes options to enable/disable pagination, set the number of records per page, define the number of navigation links, and select the initial page. + * Used to tailor the pagination UI and behavior for efficient data handling. + * + * @default { currentPage: 1, pageSize: 12, pageCount: 8 } + * + * @example + * ```tsx + * + * ``` + */ + pageSettings?: PageSettings; + + /** + * Controls HTML sanitization for grid content. + * + * When set to true, the grid will sanitize any suspected untrusted HTML content before rendering it. + * This helps prevent cross-site scripting (XSS) attacks by removing or neutralizing potentially malicious scripts and HTML. + * + * @default false + * + * @example + * ```tsx + * + * ``` + */ + enableHtmlSanitizer?: boolean; + + /** + * Makes the grid header remain visible during scrolling. + * + * When enabled, column headers will "stick" to the top of the viewport and remain visible even when the user scrolls down through the grid data. + * This improves usability by keeping column headers in view at all times. + * + * @default false + * + * @example + * ```tsx + * + * ``` + */ + enableStickyHeader?: boolean; + + /** + * Specifies the text wrapping configuration for the grid, controlling how text is displayed. + * Defines the wrap mode to determine which grid sections (header, content, or both) apply text wrapping. + * Used to customize text display for readability and layout optimization. + * + * @default { wrapMode: 'Both' } + * + * @example + * ```tsx + * + * ``` + */ + textWrapSettings?: TextWrapSettings; + + /** + * Sets a fixed height for all rows in the grid. + * + * This property sets a uniform height for all rows in the grid. When set to a number, all rows will have exactly that height in pixels. + * When null (default), row height is determined automatically based on content and styles. + * + * @default null + * + * @example + * ```tsx + * + * ``` + */ + rowHeight?: number; + + /** + * Child components for the grid. + * + * Allows rendering of child elements within the grid component structure. + * + * @default null + * @private + */ + children?: ReactElement | ReactElement[] | ReactNode; + + /** + * Service for value formatting + * + * @private + */ + valueFormatterService?: IValueFormatter; + + /** + * Service locator for dependency injection + * + * @private + */ + serviceLocator?: ServiceLocator; + + /** + * Localization object + * + * @private + */ + localeObj?: IL10n; + + /** + * Sets the localization language for the grid. + * + * Determines the language used for all text in the grid interface, including built-in messages, button labels, and other UI text. + * The grid must have the corresponding locale definitions loaded to use a specific locale. + * + * @private + * @default 'en-US' + */ + locale?: string; + + /** + * Defines a query to execute against the data source. + * + * Allows you to apply a predefined `Query` object to the data source, which can include filtering, sorting, paging, and other data operations. + * This is especially useful when working with remote data sources or when you need complex data operations. + * + * @default null + * + * @example + * ```tsx + * import { Query } from '@company/data'; + * + * const GridExample: React.FC = () => { + * // Create a query to filter and sort data + * const query = new Query() + * .where('salary', 'greaterThan', 50000) + * .sortBy('name', 'ascending'); + * + * return ( + * + * ); + * }; + * ``` + */ + query?: Query; + + /** + * Template for displaying content when the grid has no records. + * + * Customizes what is displayed when the grid has no data to show. This can be provided as a string, React element, or a function that returns content. + * It provides better user experience by explaining why the grid is empty or suggesting actions to take. + * + * @default null + * + * @example + * ```tsx + * const GridExample: React.FC = () => { + * // Custom template as a React element + * const emptyTemplate = ( + *
+ * No data + *

No employees found

+ *

Try adjusting your search or filters, or add a new employee.

+ * + *
+ * ); + * + * return ( + * + * ); + * }; + * ``` + */ + emptyRecordTemplate?: string | ReactElement | Function; + + /** + * Specifies a custom template for rendering rows in the grid. + * + * Allows complete customization of row rendering by providing a template that replaces the default row structure. + * This can be a string template, React element, or function that returns the row content. + * + * @default null + * + * @example + * ```tsx + * const CustomRowTemplate = (props: any) => { + * return ( + * + * + *
+ *

{props.name}

+ *

Role: {props.role} | Salary: {props.salary}

+ *
+ * + * + * ); + * }; + * + * + * ``` + */ + rowTemplate?: string | ReactElement | Function; + + /** + * Configures summary rows with aggregate functions. + * + * The aggregates property allows you to add summary rows to the grid, such as totals, averages, or counts. + * Each aggregate row can contain multiple aggregations that apply functions like sum, average, min, max, or count to specific columns. + * + * @default null + * + * @example + * ```tsx + * + * ``` + */ + aggregates?: AggregateRowProps[]; + + /** + * Configures the editing behavior of the Grid. + * + * The editSettings property enables and controls editing functionality. + * It defines which editing operations are permitted, such as adding, editing, and deleting rows, + * and specifies the editing mode to be used. + * + * @default null + * + * @example + * ```tsx + * + * ``` + */ + editSettings?: EditSettings; + + /** + * Configures the grid toolbar with predefined or custom items. + * + * The toolbar property allows you to add a toolbar to the grid with both predefined actions (add, edit, delete, update, cancel, search) + * and custom items. Custom items can include text, template content, and click handlers. + * + * @default null + * + * @example + * ```tsx + * + * ``` + */ + toolbar?: (string | ToolbarItemConfig)[]; + + /** + * Specifies a CSS class to apply to each grid row during data binding or refresh. + * + * Accepts either a static class name or a callback function that returns a class name + * based on the row's type, index, or associated data. This enables dynamic styling + * of rows such as headers, aggregates, or data rows. + * + * @param props - Optional event payload containing row type, row index, and complete row data. + * @returns A CSS class name to apply to the row. + * + * @default - + * + * @example + * const GridComponent = () => { + * const handleRowClass = (args?: RowClassEvent): string => { + * return args?.rowType === 'aggregate' ? 'summary-row' : ''; + * }; + * + * return ( + * + * ); + * }; + */ + rowClass?: string | ((props?: RowClassEvent) => string); + + /** + * Fires at the start of grid initialization before data processing. + * Useful for initial configurations or showing loading indicators. + * + * @event onGridRenderStart + * @example + * ```tsx + * const GridComponent = () => { + * const handleGridRender = () => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onGridRenderStart?: () => void; + + /** + * Fires after the grid is fully initialized and rendered in the DOM. + * Ideal for DOM-related operations or interacting with the grid. + * + * @private + * @event onGridInit + */ + onGridInit?: () => void; + + /** + * Fires after data is received but before binding to the grid. + * Allows data modification or filtering before rendering. + * + * @private + * @event onDataLoadStart + */ + onDataLoadStart?: (args: DataLoadStartEvent | DataReturnType) => void; + + /** + * Fires after data is successfully bound to the grid. + * Suitable for actions requiring fully loaded data. + * + * @event onDataLoad + * @example + * ```tsx + * const GridComponent = () => { + * const handleDataLoaded = () => { + * // handle your action here + * }; + * + * return ( + *
+ *
Loading...
+ * + *
+ * ); + * }; + * ``` + */ + onDataLoad?: () => void; + + /** + * Fired when the grid is fully loaded and ready for user interaction. + * Suitable for actions requiring only on grid initially fully loaded data. + * + * @event onGridRenderComplete + * @example + * ```tsx + * const GridComponent = () => { + * const handleGridReady = () => { + * // handle your action here + * }; + * + * return ( + *
+ *
Loading...
+ * + *
+ * ); + * }; + * ``` + */ + onGridRenderComplete?: () => void; + + /** + * Fires for each header cell during grid rendering. + * Enables customization of header cell appearance or content. + * + * @private + * @event onHeaderCellRender + */ + onHeaderCellRender?: (args: HeaderCellRenderEvent) => void; + + /** + * Fires for each aggregate cell during grid rendering. + * Allows customization of aggregate cell appearance or content. + * + * @private + * @event onAggregateCellRender + */ + onAggregateCellRender?: (args: AggregateCellRenderEvent) => void; + + /** + * Fires for each data cell during grid rendering. + * Enables customization of data cell appearance or content. + * + * @private + * @event onCellRender + */ + onCellRender?: (args: CellRenderEvent) => void; + + /** + * Fires for each row when bound with data. + * Allows customization of row appearance or behavior. + * + * @private + * @event onRowRender + */ + onRowRender?: (args: RowRenderEvent) => void; + + /** + * Fires for each aggregate row when bound with data. + * Enables customization of aggregate row appearance or behavior. + * + * @private + * @event onAggregateRowRender + */ + onAggregateRowRender?: (args: AggregateRowRenderEvent) => void; + + /** + * Fires when grid operations like sorting or filtering fail. + * Provides error details for handling and user feedback. + * + * @event onError + * @example + * ```tsx + * const GridComponent = () => { + * const handleActionFailure = (error: GridErrorEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onError?: (args: GridErrorEvent) => void; + + /** + * Fires when grid refresh. + * + * @private + */ + onRefreshStart?: (args: Object) => void; + + /** + * Fired when the grid data is refreshed or updated. + * + * @event onRefresh + * @example + * ```tsx + * const GridComponent = () => { + * const handleGridRefresh = () => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onRefresh?: () => void; + + /** + * Fires when grid data state changes due to sorting or paging. + * Monitors and responds to changes in grid state. + * + * @event onDataRequest + * @example + * ```tsx + * const GridComponent = () => { + * const [currentState, setCurrentState] = useState({}); + * const handleDataStateRequest = (args: DataRequestEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onDataRequest?: (args: DataRequestEvent) => void; + + /** + * Fires when the grid's data source is changed. + * Monitors and responds to updates in the grid's data source. + * + * @event onDataChangeRequest + * @example + * ```tsx + * const GridComponent = () => { + * const [currentData, setCurrentData] = useState([]); + * const handleDataChangeRequest = (args: DataChangeRequestEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onDataChangeRequest?: (args: DataChangeRequestEvent) => void; + + /** + * Fires when the grid component is destroyed. + * + * @private + * @event onGridDestroy + */ + onGridDestroy?: () => void; + + /** + * Fires when a filtering operation begins on the grid. + * Allows customization or cancellation of filter behavior. + * + * @private + * @event onFilterStart + */ + onFilterStart?: (args: FilterEvent) => void; + + /** + * Fires after a filtering operation completes on the grid. + * Provides filter state details for post-filter actions. + * + * @event onFilter + * @example + * ```tsx + * const GridComponent = () => { + * const handleFilterEnd = (args: FilterEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onFilter?: (args: FilterEvent) => void; + + /** + * Fires when a sorting operation begins on the grid. + * Allows customization or cancellation of sort behavior. + * + * @private + * @event onSortStart + */ + onSortStart?: (args: SortEvent) => void; + + /** + * Fires after a sorting operation completes on the grid. + * Provides sort state details for post-sort actions. + * + * @event onSort + * @example + * ```tsx + * const GridComponent = () => { + * const handleSortEnd = (args: SortEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onSort?: (args: SortEvent) => void; + + /** + * Fires when a searching operation begins on the grid. + * Allows customization or addition of search conditions. + * + * @private + * @event onSearchStart + */ + onSearchStart?: (args: SearchEvent) => void; + + /** + * Fires after a searching operation completes on the grid. + * Provides search result details for post-search actions. + * + * @event onSearch + * @example + * ```tsx + * const GridComponent = () => { + * const handleSearchEnd = (args: SearchEvent) => { + * // handle your action here + * }; + * + * return ( + *
+ * + *
+ * ); + * }; + * ``` + */ + onSearch?: (args: SearchEvent) => void; + + /** + * Fires when a grid row is clicked. + * Provides details about the clicked row for custom actions. + * + * @event onRowDoubleClick + * @example + * ```tsx + * const GridComponent = () => { + * const handleRowDoubleClick = (args: RecordDoubleClickEvent) => { + * // handle your action here + * }; + * + * return ( + *
+ * + *
+ * ); + * }; + * ``` + */ + onRowDoubleClick?: (args: RecordDoubleClickEvent) => void; + + /** + * Fires when a toolbar item is clicked. + * Enables custom actions for toolbar buttons. + * + * @event onToolbarItemClick + * @example + * ```tsx + * const GridComponent = () => { + * const handleToolbarClick = (args: ClickEventArgs) => { + * // handle your action here + * }; + * + * return ( + * + * + * + * ); + * }; + * ``` + */ + onToolbarItemClick?: (args: ToolbarClickEvent) => void; + + /** + * Fires when a grid cell gains focus. + * Provides details about the focused cell. + * + * @event onCellFocus + * @example + * ```tsx + * const GridComponent = () => { + * const handleCellFocused = (args: CellFocusEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onCellFocus?: (args: CellFocusEvent) => void; + + /** + * Fires when a grid cell is clicked. + * Provides details about the clicked cell. + * + * @event onCellClick + * @example + * ```tsx + * const GridComponent = () => { + * const handleCellClick = (args: CellFocusEvent) => { + * // handle your action here + * }; + * + * return ( + *
+ * + *
+ * ); + * }; + * ``` + */ + onCellClick?: (args: CellFocusEvent) => void; + + /** + * Fires before a grid cell gains focus. + * Allows validation or modification of focus behavior. + * + * @private + * @event onCellFocusStart + */ + onCellFocusStart?: (args: CellFocusEvent) => void; + + /** + * Fires before a row is selected. + * Allows validation or cancellation of row selection. + * + * @private + * @event onRowSelecting + */ + onRowSelecting?: (args: RowSelectingEvent) => void; + + /** + * Fires after a row is successfully selected. + * Provides details about the selected row. + * + * @event onRowSelect + * @example + * ```tsx + * const GridComponent = () => { + * const handleRowSelected = (args: RowSelectEvent) => { + * // handle your action here + * }; + * + * return ( + *
+ * + *
+ * ); + * }; + * ``` + */ + onRowSelect?: (args: RowSelectEvent) => void; + + /** + * Fires before a row is deselected. + * Allows validation or cancellation of row deselection. + * + * @private + * @event onRowDeselecting + */ + onRowDeselecting?: (args: RowSelectingEvent) => void; + + /** + * Fires after a row is successfully deselected. + * Provides details about the deselected row. + * + * @event onRowDeselect + * @example + * ```tsx + * const GridComponent = () => { + * const handleRowDeselected = (args: RowSelectEvent) => { + * // handle your action here + * }; + * + * return ( + *
+ * + *
+ * ); + * }; + * ``` + */ + onRowDeselect?: (args: RowSelectEvent) => void; + + /** + * Event triggered before the paging operation start. + * + * @private + * @event onPageChangeStart + */ + onPageChangeStart?: (args: PageEvent) => void; + + /** + * Event triggered after a paging operation is completed on the grid. + * + * @event onPageChange + * @example + * ```tsx + * const GridComponent = () => { + * const handlePageChangeEnd = (args: PageEvent) => { + * // handle your action here + * }; + * + * return ( + *
+ * + *
+ * ); + * }; + * ``` + */ + onPageChange?: (args: PageEvent) => void; + + /** + * Fires when editing begins on a grid record. + * Allows validation or field modification before editing. + * + * @event onRowEditStart + * @example + * ```tsx + * const GridComponent = () => { + * const handleRowEdit = (args: EditEventArgs) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onRowEditStart?: (args: RowEditEvent) => void; + /** + * Fires when the process of adding a new row starts. + * + * @event onRowAddStart + * @example + * ```tsx + * const GridComponent = () => { + * const handleRowAdd = (args: RowAddEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onRowAddStart?: (args: RowAddEvent) => void; + /** + * Fires when the edit or add form is fully loaded and ready for user input. + * + * @event onFormRender + * @example + * ```tsx + * const GridComponent = () => { + * const handleFormReady = (args: FormRenderEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onFormRender?: (args: FormRenderEvent) => void; + /** + * Fires when a create, update, or delete operation is started. + * + * @event onDataChangeStart + * @example + * ```tsx + * const GridComponent = () => { + * const handleDataChangeStart = (args: SaveEvent | DeleteEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onDataChangeStart?: (args: SaveEvent | DeleteEvent) => void; + /** + * Fires when a create, update, or delete operation is completed. + * + * @event onDataChangeComplete + * @example + * ```tsx + * const GridComponent = () => { + * const handleDataChangeComplete = (args: SaveEvent | DeleteEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onDataChangeComplete?: (args: SaveEvent | DeleteEvent) => void; + /** + * Fires when a CRUD operation is canceled. + * + * @event onDataChangeCancel + * @example + * ```tsx + * const GridComponent = () => { + * const handleDataChangeCancel = (args: CancelFormEvent) => { + * // handle your action here + * }; + * + * return ( + * + * ); + * }; + * ``` + */ + onDataChangeCancel?: (args: CancelFormEvent) => void; +} + +/** + * Provides context for customizing row appearance or behavior. + * Includes row type, index, and optional complete row data. + */ +export interface RowClassEvent { + /** + * Type of the row: 'header', 'content', or 'aggregate'. + * Useful for applying different styles based on row category. + * + * @default - + */ + rowType: 'header' | 'content' | 'aggregate'; + + /** + * The index of the row in the grid. + * Useful for alternating styles or row-specific logic. + * + * @default - + */ + rowIndex: number; + + /** + * The complete data object for the row. + * Optional, used for conditional styling based on row values. + * + * @default - + */ + rowData?: Object; +} + +/** + * The Syncfusion React Grid component is a feature-rich, customizable data grid for building responsive, high-performance applications. + * It supports advanced functionalities like sorting, filtering, paging, and editing, with flexible data binding to local or remote data sources. + * Key features include customizable columns, aggregates, row templates, and built-in support for localization. + * The component offers a robust API with methods for dynamic data manipulation and events for handling user interactions. + */ +export interface IGrid extends GridProps { + /** + * Displays a loading spinner overlay on the grid to indicate an ongoing operation. + * Used to enhance the user experience during asynchronous or time-consuming operations. + * + * @returns {void} + */ + showSpinner(): void; + + /** + * Hides the loading spinner overlay previously shown on the grid. + * Used to update the UI after completing asynchronous or time-consuming operations. + * + * @returns {void} + */ + hideSpinner(): void; + + /** + * Refreshes the grid’s data and view to reflect the latest state. + * Updates the grid’s display by re-rendering data based on current settings, such as filters, sorting, or pagination. + * Used to synchronize the grid’s UI with changes in the data source or configuration. + */ + refresh(): void; + + /** + * Retrieves the column configuration object for a specified field name. + * Returns the ColumnProps object matching the provided field, enabling access to column metadata like field, header text, or formatting. + * Used for dynamically accessing or modifying column properties at runtime. + * + * @param {string} field - The field name of the column to retrieve. + * @returns {ColumnProps} The column configuration object for the specified field. + */ + getColumnByField(field: string): ColumnProps; + + /** + * Retrieves an array of configuration objects for all currently visible columns in the grid. + * Used to access metadata for visible columns for dynamic processing or UI updates. + * + * @returns {ColumnProps[]} An array of configuration objects for visible columns. + */ + getVisibleColumns(): ColumnProps[]; + + /** + * Retrieves the column configuration object for a specified unique identifier (UID). + * Used for dynamically accessing or modifying column settings at runtime using a unique identifier. + * + * @private + * @param {string} uid - The unique identifier of the column to retrieve. + * @returns {ColumnProps} The column configuration object for the specified UID. + */ + getColumnByUid(uid: string): ColumnProps; + + /** + * Retrieves all records from the grid based on current settings. + * Returns an array of data objects reflecting applied pagination, filters, sorting, and searching settings. + * For remote data sources, returns only the current view data. + * + * @param {boolean} skipPage - Optional. If true, excludes pagination information from the returned data. + * @param {boolean} requiresCount - Optional. If true, includes the total record count in the response. + * @returns {Object[] | Promise} An array of records or a promise for remote data. + */ + getData(skipPage?: boolean, requiresCount?: boolean): Object[] | Promise; + + /** + * Retrieves an array of configuration objects for all currently hidden columns in the grid. + * Used to access metadata for hidden columns for dynamic processing or UI updates. + * + * @returns {ColumnProps[]} An array of configuration objects for hidden columns. + */ + getHiddenColumns(): ColumnProps[]; + + /** + * Retrieves detailed information about the row containing a specified cell element or event target. + * Returns a `RowInfo` object with metadata about the associated row, such as its index or data. + * Used to access row-specific details for dynamic processing or event handling. + * + * @param {Element} target - The cell element or event target used to identify the row. + * @returns {RowInfo} A RowInfo object containing details about the associated row. + */ + getRowInfo(target: Element): RowInfo + + /** + * Retrieves the `field` names of the primary key columns defined in the grid. + * Used to identify the primary keys for data operations like updates or deletions. + * + * @returns {string[]} An array of field names for the grid’s primary key columns. + */ + getPrimaryKeyFieldNames(): string[]; + + /** + * Updates and refreshes a specific row’s data based on its primary key value. + * Replaces the row’s data for the record matching the provided key, optionally updating the data source. + * + * Requires a primary key column defined via `columns.isPrimaryKey`. + * + * @param {string | number} key - The primary key value of the record to update. + * @param {Object} data - The new data object for the row. + * @param {boolean} isDataSourceChangeRequired - Optional. If true, updates the underlying data source. + * @returns {void} + */ + setRowData(key: string | number, data: Object, isDataSourceChangeRequired?: boolean): void; + + /** + * Updates a specific cell’s value in a row identified by its primary key. + * Modifies the cell value for the specified field in the record matching the provided key, optionally updating the data source. + * + * Requires a primary key column defined via `columns.isPrimaryKey`. + * + * @param {string | number} key - The primary key value of the record containing the cell. + * @param {string} field - The field name of the column to update. + * @param {string | number | boolean | Date | null} value - The new value for the cell. + * @param {boolean} isDataSourceChangeRequired - Optional. If true, updates the underlying data source. + * @returns {void} + */ + setCellValue(key: string | number, field: string, value: string | number | boolean | Date | null, + isDataSourceChangeRequired?: boolean): void; + + /** + * Retrieves the current configuration of all columns in the grid. + * Returns an array of `ColumnProps` objects representing the grid’s column settings. + * Used to access column data for dynamic processing or modifications. + * + * @returns {ColumnProps[]} An array of column configuration objects. + */ + getColumns(): ColumnProps[]; + + /** + * Retrieves the data objects of the currently selected rows in the grid. + * Used to access selected row data for further processing or display. + * + * @returns {Object[]} An array of selected row data objects. + */ + getSelectedRows(): Object[]; + + /** + * Selects a single row by its index in the grid. + * Updates the grid’s selection state to highlight the specified row, optionally toggling the existing selection. + * Used to programmatically select a row based on its position. + * + * @param {number} rowIndex - The zero-based index of the row to select. + * @param {boolean} isToggle - Optional. Specifies whether to toggle the existing selection. + * @returns {void} + */ + selectRow(rowIndex: number, isToggle?: boolean): void; + + /** + * Selects multiple rows by their indexes in the grid. + * Updates the grid’s selection state to highlight the specified rows, typically used in multi-selection mode. + * Used to programmatically select a collection of rows. + * + * @param {number[]} rowIndexes - An array of zero-based row indexes to select. + * @returns {void} + */ + selectRows(rowIndexes: number[]): void; + + /** + * Selects a range of rows from a start index to an optional end index in the grid. + * Updates the grid’s selection state to highlight all rows within the specified range. + * Used to programmatically select a continuous set of rows. + * + * @param {number} startIndex - The zero-based index of the first row in the range. + * @param {number} endIndex - Optional. The zero-based index of the last row in the range. + * @returns {void} + */ + selectRowByRange(startIndex: number, endIndex?: number): void; + + /** + * Retrieves the indexes of the currently selected rows in the grid. + * Used to determine which rows are currently selected for further processing. + * + * @returns {number[]} An array of selected row indexes. + */ + getSelectedRowIndexes(): number[]; + + /** + * Retrieves the data objects of the currently selected rows in the grid. + * Used to access the data of selected rows for processing or display. + * + * @returns {Object[] | null} An array of selected row data objects or null if none are selected. + */ + getSelectedRecords(): Object[] | null; + + /** + * Deselects specific rows by their indexes in the grid. + * Removes the specified rows from the current selection, updating the grid’s UI accordingly. + * Used to programmatically remove selection from specific rows. + * + * @param {number[]} indexes - An array of zero-based row indexes to deselect. + * @returns {void} + */ + clearRowSelection(indexes: number[]): void; + + /** + * Clears all currently selected rows in the grid. + * Removes the selection state from all rows, resetting the grid’s selection UI. + * Used to programmatically clear all row selections. + * + * @returns {void} + */ + clearSelection(): void; + + /** + * Sorts a specified column in the grid with given options. + * Applies sorting to the column identified by its name, using the specified direction and multi-sort behavior. + * Used to programmatically sort grid data by a column. + * + * @param {string} columnName - The name of the column to sort (e.g., field name). + * @param {SortDirection | string} direction - The sorting direction ('Ascending' or 'Descending'). + * @param {boolean} isMultiSort - Optional. Specifies whether to maintain previously sorted columns. + * @returns {void} + */ + sortByColumn(columnName: string, sortDirection: SortDirection | string, isMultiSort?: boolean): void; + + /** + * Removes sorting from a specified column in the grid. + * Clears the sorting applied to the column identified by its name, reverting it to an unsorted state. + * Used to programmatically remove sorting from a specific column. + * + * @private + * @param {string} columnName - The name of the column to remove sorting from (e.g., field name). + * @returns {void} + */ + removeSortColumn(columnName: string): void; + + /** + * Clears sorting from all columns in the grid. + * Resets the grid to an unsorted state, removing all sorting applied to any columns. + * Used to programmatically revert the grid to its original data order. + * + * @returns {void} + */ + clearSort(): void; + + + /** + * Filters grid rows by a specified column with given options. + * Applies a filter to the column identified by its `field` name, using the provided operator and value, with optional predicate and sensitivity settings. + * Used to programmatically filter grid data based on column-specific criteria. + * + * @param {string} fieldName - The `field` name of the column to filter. + * @param {string} filterOperator - The operator to apply (e.g., 'contains', 'equal'). + * @param {string | number | Date | boolean | number[] | string[] | Date[] | boolean[]} filterValue - The value to filter against. + * @param {string} predicate - Optional. The relationship between filter queries ('AND' or 'OR'). + * @param {boolean} caseSensitive - Optional. If true, performs case-sensitive filtering. If false, ignores case. + * @param {boolean} ignoreAccent - Optional. If true, ignores diacritic characters during filtering. + * @returns {void} + */ + filterByColumn(fieldName: string, filterOperator: string, + filterValue: string | number | Date | boolean| number[]| string[]| Date[]| boolean[], + predicate?: string, caseSensitive?: boolean, + ignoreAccent?: boolean): void; + + /** + * Clears filters applied to the specified fields or all columns in the grid. + * Removes filtering conditions, restoring the grid to display all data or data for specified fields. + * Used to programmatically reset filtering for a fresh data view. + * + * @param {string[]} fields - Optional. An array of field names to clear filters for. If omitted, clears all filters. + * @returns {void} + */ + clearFilter(fields?: string[]): void; + + /** + * Removes the filter applied to a specific column by its field name. + * Clears the filter for the specified column, optionally resetting the filter bar’s input value. + * Used to programmatically remove filtering from a single column. + * + * @private + * @param {string} field - Optional. The field name of the column to remove the filter from. + * @param {boolean} isClearFilterBar - Optional. If true, clears the filter bar’s input value. + * @returns {void} + */ + removeFilteredColsByField(field?: string, isClearFilterBar?: boolean): void; + + /** + * Searches grid records using a specified search string. + * Applies a search across the grid’s data based on the configured search settings, such as fields or operators. + * Used to programmatically filter data using a search term. + * + * @param {string} searchString - Optional. The search term to apply. if omitted, clears the search. + * @returns {void} + */ + search(searchString?: string): void; + + /** + * Navigates to a specific page in the grid’s paginated data. + * Updates the grid to display the data for the specified page number. + * + * @param {number} pageNumber - The page number to navigate to. + * @returns {void} + */ + goToPage(pageNumber: number): void; + + /** + * Updates the text of an external message displayed in the grid. + * Sets or clears a custom message, typically used for notifications or status updates in the grid’s UI. + * + * @param {string} message - Optional. The message text to display. + * @returns {void} + */ + updatePagerMessage(message?: string): void; + + /** + * Retrieves the DOM element containing the grid’s header content. + * Used for programmatic access or manipulation of the grid’s header area. + * + * @returns {HTMLDivElement} The header content element. + */ + getHeaderContent(): HTMLDivElement; + + /** + * Retrieves the DOM element containing the grid’s content area. + * Used for programmatic access or manipulation of the grid’s content area. + * + * @returns {HTMLDivElement} The content area element. + */ + getContent(): HTMLDivElement; + + /** + * Initiates editing for a specified row or the currently selected row. + * Used to programmatically trigger the editing mode for a specific or selected row. + * + * @param {HTMLTableRowElement} rowElement - Optional. The row element to edit. If omitted, edits the selected row. + * @returns {void} + */ + editRow(rowElement?: HTMLTableRowElement): void; + + /** + * Completes the editing process and saves changes to the grid’s data source. + * Returns a promise indicating whether the save operation was successful. + * + * @returns {Promise} A promise resolving to true if the save was successful, false otherwise. + */ + saveChanges(): Promise; + + /** + * Closes the editing mode and reverts any modifications made to the row. + * Used to programmatically discard edits and restore the original data. + * + * @returns {void} + */ + cancelChanges(): void; + + /** + * Adds a new record to the grid’s data source. + * Inserts a new row with the provided data at the specified index or at the start if no index is provided. + * + * @param {Object} data - Optional. The data object for the new record. + * @param {number} index - Optional. The index at which to insert the new record. + * @returns {void} + */ + addRecord(data?: Object, index?: number): void; + + /** + * Deletes a record from the grid’s data source based on specified criteria or the selected row. + * Removes a record matching the provided field name and data, or deletes the currently selected row if no parameters are provided. + * Used to programmatically remove records, updating the grid’s display and data source accordingly. + * + * @param {string} fieldName - Optional. The field name to match for identifying the record to delete. + * @param {Record | Object[]} data - Optional. The data object or array of objects to match for deletion. + * @returns {void} + */ + deleteRecord(fieldName?: string, data?: Record | Object[]): void; + + /** + * Updates a specific row in the grid with new data. + * Replaces the data of the row at the specified index with the provided data object. + * Used to programmatically modify existing row data in the grid. + * + * @param {number} index - The zero-based index of the row to update. + * @param {Object} data - The new data object for the row. + * @returns {void} + */ + updateRow(index: number, data: Object): void; + + /** + * Validates all fields in the current edit or add form against their defined rules. + * Checks the input values in the editing form to ensure they meet column validation criteria. + * Used to verify data integrity before saving changes. + * + * @returns {boolean} True if all fields pass validation, false otherwise. + */ + validateEditForm(): boolean; + + /** + * Validates a specific field against its defined column validation rules. + * Checks the value of the specified field to ensure it meets the configured validation criteria. + * Used to verify the validity of a single field during editing. + * + * @param {string} field - The name of the field to validate. + * @returns {boolean} True if the field is valid, false otherwise. + */ + validateField(field: string): boolean; +} + +/** + * Combined interface for grid base properties + * + * @private + */ +export type IGridBase = MutableGridBase & IGrid; + +/** + * Defines the structure of the event arguments triggered when a row is double-clicked in the grid. + * + * Provides contextual information about the target element, cell, row, and associated data, + * enabling precise handling of double-click interactions within grid components. + */ +export interface RecordDoubleClickEvent { + /** + * The DOM element that was the target of the double-click event. + * + * Represents the specific HTML element that received the double-click, + * allowing access to its attributes and structural context. + * + * @default - + */ + target?: Element; + + /** + * The cell element within the row where the double-click occurred. + * + * Refers to the HTML element representing the cell, which can be used + * for styling, attribute inspection, or interaction logic. + * + * @default - + */ + cell?: Element; + + /** + * The zero-based index of the clicked cell within its parent row. + * + * Indicates the position of the cell in the row, useful for identifying + * column alignment or applying cell-specific operations. + * + * @default - + */ + columnIndex?: number; + + /** + * The column configuration object associated with the clicked cell. + * + * Contains metadata such as field name, header text, formatting rules, + * and other column-level settings defined in the grid configuration. + * + * @default - + */ + column?: ColumnProps; + + /** + * The name of the event triggered. + * + * Identifies the event type for internal processing or conditional logic + * in event handler implementations. + * + * @private + * @default - + */ + name?: string; + + /** + * The row element where the double-click occurred. + * + * Refers to the HTML element representing the row, which can be accessed + * for styling, DOM traversal, or row-level manipulation. + * + * @default - + */ + row?: Element; + + /** + * The data object bound to the clicked row. + * + * Represents the complete data associated with the row, enabling + * contextual operations such as editing, selection, or detail expansion. + * + * @default - + */ + rowData?: Object; + + /** + * The zero-based index of the clicked row within the grid. + * + * Indicates the row's position in the grid's data source, supporting + * navigation, selection, and programmatic access to row data. + * + * @default - + */ + rowIndex?: number; +} + +/** + * Represents event arguments for data loading start events in the grid. + * Contains information about the data being loaded and allows cancellation of the operation. + * + * @private + */ +export interface DataLoadStartEvent { + /** + * The array of data objects to be bound to the grid. Represents the raw data for rendering or processing. + */ + result: Object[]; + /** The total number of data records available, used for pagination or display purposes. */ + count?: number; + /** + * Indicates whether to cancel the data binding operation. + * + * @private + */ + cancel?: boolean; + /** An array of aggregate values (e.g., sum, average) calculated for the data, if aggregates are defined. */ + aggregates?: Object[]; + /** + * The action arguments providing context for the data binding operation, such as filters or sorting criteria. + * + * @private + */ + actionArgs?: Object; + /** The query object defining the data retrieval parameters, such as filtering or sorting queries. */ + query: Query; + /** + * Defines the name of the event. + * + * @private + */ + name?: string; + /** + * The actual result and count of the data, providing raw data before processing or transformation. + * + * @private + */ + actual?: Object; + /** + * The type of request associated with the data binding operation. + * + * @private + */ + request?: string; +} + +/** + * Configures text wrapping behavior in grid cells and headers. When enabled, text content wraps automatically to fit within the available cell width, ensuring full visibility. + */ +export interface TextWrapSettings { + /** + * The `wrapMode` property defines how the text in the grid cells should be wrapped. The available modes are: + * * `Both`: Wraps text in both the header and content cells. + * * `Content`: Wraps text in the content cells only. + * * `Header`: Wraps texts in the header cells only. + * + * @default WrapMode.Both | 'Both' + */ + wrapMode?: WrapMode | string; + + /** + * Enables text wrapping in grid cells. + * + * When enabled, this property allows text in grid cells to wrap to multiple lines if it exceeds the column width. + * This is especially useful for columns containing lengthy content. + * + * @default false + * + * @example + * ```tsx + * + * ``` + */ + enabled?: boolean; +} + +/** + * Represents event arguments for general grid action events. + * Contains information about the current action being performed on the grid. + * + * @private + */ +export interface GridActionEvent { + /** + * Defines the current action. + * + * @private + */ + requestType?: Action; + /** + * Defines the type of event. + * + * @private + */ + type?: string; + /** + * Cancel the current grid action + * + * @private + */ + cancel?: boolean; + /** @private */ + name?: string; +} diff --git a/components/grids/src/grid/types/index.ts b/components/grids/src/grid/types/index.ts new file mode 100644 index 0000000..fadfa88 --- /dev/null +++ b/components/grids/src/grid/types/index.ts @@ -0,0 +1,13 @@ +export * from './enum'; +export * from './interfaces'; +export * from './aggregate.interfaces'; +export * from './column.interfaces'; +export * from './edit.interfaces'; +export * from './filter.interfaces'; +export * from './focus.interfaces'; +export * from './grid.interfaces'; +export * from './page.interfaces'; +export * from './search.interfaces'; +export * from './selection.interfaces'; +export * from './sort.interfaces'; +export * from './toolbar.interfaces'; diff --git a/components/grids/src/grid/types/interfaces.ts b/components/grids/src/grid/types/interfaces.ts new file mode 100644 index 0000000..315f7fe --- /dev/null +++ b/components/grids/src/grid/types/interfaces.ts @@ -0,0 +1,1460 @@ +import { Dispatch, HTMLAttributes, JSX, ReactElement, ReactNode, RefObject, SetStateAction, MouseEvent, FocusEvent, CSSProperties, UIEvent } from 'react'; +import { ReturnType, DataManager, Predicate, Query, DataResult } from '@syncfusion/react-data'; +import { DateFormatOptions, NumberFormatOptions } from '@syncfusion/react-base'; +import { CellType, RenderType } from '../types/enum'; +import { FilterSettings, FilterEvent, filterModule } from '../types/filter.interfaces'; +import { ColumnProps, IColumnBase } from '../types/column.interfaces'; +import { FocusStrategyModule } from '../types/focus.interfaces'; +import { InlineEditFormRef, editModule } from '../types/edit.interfaces'; +import { selectionModule } from '../types/selection.interfaces'; +import { PagerRef } from '@syncfusion/react-pager'; +import { AggregateColumnProps, AggregateRowProps } from '../types/aggregate.interfaces'; +import { GridActionEvent, IGrid, IGridBase } from '../types/grid.interfaces'; +import { PageEvent } from '../types/page.interfaces'; +import { searchModule, SearchSettings, SearchEvent } from './search.interfaces'; +import { SortSettings, Sorts, SortModule, SortEvent } from '../types/sort.interfaces'; +import { ToolbarAPI } from './toolbar.interfaces'; + +/** + * IValueFormatter interface defines the methods for value formatting services + * + * @private + */ +export interface IValueFormatter { + /** + * Converts a string value from the view to a typed value + * + * @param {string} value - The string value to convert + * @param {Function} format - The format function to use + * @param {string} [target] - Optional target type + * @returns {string | number | Date} The converted value + */ + fromView(value: string, format: Function, target?: string): string | number | Date; + + /** + * Converts a typed value to a string for display in the view + * + * @param {number | Date} value - The value to format + * @param {Function} format - The format function to use + * @returns {string | Object} The formatted value + */ + toView(value: number | Date, format: Function): string | Object; + + /** + * Sets the culture for formatting + * + * @param {string} cultureName - The culture name to set + */ + setCulture?(cultureName: string): void; + + /** + * Gets a format function for the specified format options + * + * @param {NumberFormatOptions | DateFormatOptions} format - The format options + * @returns {Function} The format function + */ + getFormatFunction?(format: NumberFormatOptions | DateFormatOptions): Function; + + /** + * Gets a parser function for the specified format options + * + * @param {NumberFormatOptions | DateFormatOptions} format - The format options + * @returns {Function} The parser function + */ + getParserFunction?(format: NumberFormatOptions | DateFormatOptions): Function; +} +/** + * Interface defining cell properties and metadata for grid rendering. + * + * @private + */ +export interface ICell { + /** + * Specifies the number of columns the cell should span. + * + * @default 1 + */ + colSpan?: number; + + /** + * Specifies the number of rows the cell should span. + * + * @default 1 + */ + rowSpan?: number; + + /** + * Defines the type of cell content. + * + * @default - + */ + cellType?: CellType; + + /** + * Specifies whether the cell is visible. + * + * @default true + */ + visible?: boolean; + + /** + * Indicates whether the cell contains template content. + * + * @default false + */ + isTemplate?: boolean; + + /** + * Indicates whether this is a data cell. + * + * @default false + */ + isDataCell?: boolean; + + /** + * Indicates whether the cell is spanned by another cell. + * + * @default false + */ + isSpanned?: boolean; + + /** + * The column associated with this cell. + * + * @default null + */ + column?: T; + + /** + * The aggregate column definition if this is an aggregate cell. + * + * @default null + */ + aggregateColumn?: AggregateColumnProps; + + /** + * The unique identifier for the row containing this cell. + * + * @default - + */ + rowID?: string; + + /** + * The index of the cell within the row. + * + * @default 0 + */ + index?: number; + + /** + * The column index of the cell. + * + * @default 0 + */ + colIndex?: number; + + /** + * Additional CSS class names for the cell. + * + * @default - + */ + className?: string; +} +/** + * Interface defining row properties and metadata for grid rendering. + * + * @private + */ +export interface IRow { + /** + * Function to set the row object state. + * + * @default null + */ + setRowObject?: Dispatch>>; + + /** + * Unique identifier for the row. + * + * @default - + */ + uid?: string; + + /** + * Data object associated with the row. + * + * @default null + */ + data?: Object; + + /** + * Group summary count for the row. + * + * @default 0 + */ + gSummary?: number; + + /** + * Template index for the row. + * + * @default 0 + */ + tIndex?: number; + + /** + * Indicates whether this is a caption row. + * + * @default false + */ + isCaptionRow?: boolean; + + /** + * Indicates whether this is an alternate row. + * + * @default false + */ + isAltRow?: boolean; + + /** + * Indicates whether this is a data row. + * + * @default false + */ + isDataRow?: boolean; + + /** + * Indicates whether this is an aggregate row. + * + * @default false + */ + isAggregateRow?: boolean; + + /** + * Indicates whether the row is selected. + * + * @default false + */ + isSelected?: boolean; + + /** + * Indicates whether the row is expanded. + * + * @default false + */ + isExpand?: boolean; + + /** + * Specifies whether the row is visible. + * + * @default true + */ + visible?: boolean; + + /** + * Specifies the number of rows this row should span. + * + * @default 1 + */ + rowSpan?: number; + + /** + * Array of cells contained in this row. + * + * @default [] + */ + cells?: ICell[]; + + /** + * Index of the row. + * + * @default 0 + */ + index?: number; + + /** + * Indentation level for hierarchical rows. + * + * @default 0 + */ + indent?: number; + + /** + * Height of the row. + * + * @default - + */ + height?: string; + + /** + * Parent row unique identifier for hierarchical rows. + * + * @default - + */ + parentUid?: string; + + /** + * Reference to the row's DOM element. + * + * @default null + */ + element?: HTMLTableRowElement | null; + + /** + * Reference to the inline edit form for this row. + * + * @default null + */ + editInlineRowFormRef?: RefObject; +} + +/** + * @private + */ +export interface Scroll { + /** + * Sets padding for scroll elements + */ + setPadding?: () => void; +} + +/** + * @private + */ +export interface MutableGridBase { + /** + * Column directives element + */ + columnsDirective?: ReactElement; + + /** + * State changed UI Column updated properties + * + * @private + */ + uiColumns?: ColumnProps[]; + + /** + * Sort settings model + */ + sortSettings?: SortSettings; + + /** + * Default locale object + */ + defaultLocale?: Object; + + /** + * Sets the current view data + * + * @param {Object[]} data - The data to set + */ + setCurrentViewData?: (data: Object[]) => void; + + /** + * Current view data + */ + currentViewData?: Object[]; + + /** + * Header row depth for stacked headers + */ + headerRowDepth?: number; + + /** + * Column elements for the grid + */ + colElements?: JSX.Element[]; + isInitialLoad?: boolean; + + /** + * Focus strategy module for centralized focus management + */ + focusModule?: FocusStrategyModule; + + /** + * The `selectionModule` is used to selecting the row in the Grid. + */ + selectionModule?: selectionModule; + + /** + * The `sortModule` is used to manipulate sorting in the Grid. + */ + sortModule?: SortModule; + + /** + * The `filterModule` is used to manipulate filtering in the Grid. + */ + filterModule?: filterModule; + + /** + * The `searchModule` is used to manipulate searching in the Grid. + */ + searchModule?: searchModule; + + /** + * The `editModule` is used to manipulate editing in the Grid. + */ + editModule?: editModule; + + gridAction?: GridActionEvent; + filterSettings?: FilterSettings; + searchSettings?: SearchSettings; + currentPage?: number; + totalRecordsCount?: number; + responseData?: Object; + setResponseData?: Dispatch>; + /** + * Get the parent element + */ + getParentElement?: () => HTMLElement; + /** + * To evaluate sf-ellipsistooltip class required or not + * + * @param {HTMLElement} element - Defines the original cell reference element + * @param {HTMLDivElement} htable - Dummy class based header table + * @param {HTMLDivElement} ctable - Dummy class based content table + * @returns {boolean} Define whether sf-ellipsistooltip class required for cell or not. + * @private + */ + evaluateTooltipStatus?: (element: HTMLElement) => boolean; + isInitialBeforePaint?: RefObject; + isStateChangeEventTriggeringRequired?: RefObject; + cssClass?: string; + + /** + * The data module for data operations (similar to original getDataModule()) + */ + dataModule?: UseDataResult; + + /** + * The toolbar module for toolbar operations + */ + toolbarModule?: ToolbarAPI | Record; +} + +/** + * Interface for row reference and operations. + * + * @private + */ +export interface RowRef { + /** + * Reference to the row DOM element. + * + * @default null + */ + readonly rowRef: RefObject; + + /** + * Gets the cell options objects. + * + * @returns {ICell[]} The cell options objects + */ + getCells?: () => ICell[]; + + /** + * Reference to the inline edit form for this row. + * + * @default null + */ + editInlineRowFormRef?: RefObject; + + /** + * Function to set the row object state. + * + * @default null + */ + setRowObject?: Dispatch>>; +} + +/** + * Interface defining custom operators for different data types in grid filtering. + * + * @private + */ +export interface ICustomOptr { + /** + * Custom operators for string type filtering. + * + * @default [] + */ + stringOperator?: { [key: string]: Object }[]; + /** + * Custom operators for number type filtering. + * + * @default [] + */ + numberOperator?: { [key: string]: Object }[]; + /** + * Custom operators for date type filtering. + * + * @default [] + */ + dateOperator?: { [key: string]: Object }[]; + /** + * Custom operators for datetime type filtering. + * + * @default [] + */ + datetimeOperator?: { [key: string]: Object }[]; + /** + * Custom operators for boolean type filtering. + * + * @default [] + */ + booleanOperator?: { [key: string]: Object }[]; +} + +/** + * Extended row interface with additional properties + * + * @private + */ +export interface IRowBase extends Omit, 'children'> { + /** + * Type of row rendering + */ + rowType?: RenderType; + + /** + * Row data and metadata + */ + row?: IRow; + + /** + * Child elements of the row + */ + children?: ReactNode | ReactElement[]; + + /** + * Controls the padding area around the table scrollable region + */ + tableScrollerPadding?: boolean; + + /** + * Defines the aggregate row information. + */ + aggregateRow?: AggregateRowProps; + + /** + * column cells of the row + */ + column?: ColumnProps; +} + +/** + * Defines the supported data types for grid values, used in filtering, sorting, and other operations. + * ```props + * * number :- Represents numeric values. + * * string :- Represents text values. + * * Date :- Represents date values. + * * boolean :- Represents true/false values. + * * Object :- Represents complex object values. + * ``` + * + * @private + */ +export type ValueType = number | string | Date | boolean | Object; + +/** + * Interface for render reference + * + * @private + */ +export interface RenderRef extends HeaderPanelRef, ContentPanelRef, FooterPanelRef { + /** + * Refreshes the grid view by getting the updated data + */ + refresh(): void; + /** Shows a loading spinner overlay on the grid to indicate that an operation is in progress. */ + showSpinner(): void; + /** Hides the loading spinner overlay that was previously shown on the grid. */ + hideSpinner(): void; + /** + * Scroll module reference + */ + scrollModule?: Scroll; + + /** + * Pager module reference + */ + pagerModule?: PagerRef; +} + +/** + * Base interface for render components + * + * @private + */ +export interface IRenderBase { + /** + * Child elements + */ + children?: ReactNode; +} + +/** + * Interface for header panel reference + * + * @private + */ +export interface HeaderPanelRef extends HeaderTableRef { + /** + * Reference to the header panel element + */ + readonly headerPanelRef?: HTMLDivElement | null; + + /** + * Reference to the header scroll element + */ + readonly headerScrollRef?: HTMLDivElement | null; +} + +/** + * Base interface for header panel properties + * + * @private + */ +export interface IHeaderPanelBase { + /** + * Attributes for the panel element + */ + panelAttributes?: HTMLAttributes; + + /** + * Attributes for the scroll content element + */ + scrollContentAttributes?: HTMLAttributes; +} + +/** + * Interface for header table reference + * + * @private + */ +export interface HeaderTableRef extends HeaderRowsRef { + /** + * Reference to the header table element + */ + readonly headerTableRef?: HTMLTableElement | null; + getHeaderTable?: () => HTMLTableElement | null; +} + +/** + * Base interface for header table properties + * + * @private + */ +export type IHeaderTableBase = HTMLAttributes; + +/** + * Interface for header rows reference + * + * @private + */ +export interface HeaderRowsRef { + /** + * Reference to the header section element + */ + readonly headerSectionRef?: HTMLTableSectionElement | null; + + /** + * Gets the header rows collection + * + * @returns {HTMLCollectionOf | undefined} The header rows + */ + getHeaderRows?: () => HTMLCollectionOf | undefined; + + /** + * Gets the row options objects with DOM element references + * + * @returns {IRow[]} The row options objects + */ + getHeaderRowsObject?: () => IRow[]; +} + +/** + * Base interface for header rows properties + * + * @private + */ +export type IHeaderRowsBase = HTMLAttributes; + +/** + * Interface for content panel reference + * + * @private + */ +export interface ContentPanelRef extends ContentTableRef { + /** + * Reference to the content panel element + */ + readonly contentPanelRef?: HTMLDivElement | null; + + /** + * Reference to the content scroll element + */ + readonly contentScrollRef?: HTMLDivElement | null; +} + +/** + * Base interface for content panel properties + * + * @private + */ +export interface IContentPanelBase { + /** + * Attributes for the panel element + */ + panelAttributes?: HTMLAttributes; + + /** + * Attributes for the scroll content element + */ + scrollContentAttributes?: HTMLAttributes; + + /** + * Sets padding for the header + */ + setHeaderPadding?: () => void; +} + +/** + * Interface for content table reference + * + * @private + */ +export interface ContentTableRef extends ContentRowsRef { + /** + * Reference to the content table element + */ + readonly contentTableRef?: HTMLTableElement | null; + getContentTable?: () => HTMLTableElement | null; + + /** + * @private + */ + addInlineRowFormRef?: RefObject; + /** + * @private + */ + editInlineRowFormRef?: RefObject; +} + +/** + * Base interface for content table properties + * + * @private + */ +export type IContentTableBase = HTMLAttributes; + +/** + * Interface for content rows reference + * + * @private + */ +export interface ContentRowsRef { + /** + * Reference to the content section element + */ + readonly contentSectionRef?: HTMLTableSectionElement | null; + + /** + * Gets the current view records + * + * @returns {Object[]} The current records + */ + getCurrentViewRecords(): Object[]; + + /** + * Gets the rows collection + * + * @returns {HTMLCollectionOf | undefined} The rows + */ + getRows(): HTMLCollectionOf | undefined; + + /** + * Gets the row options objects with DOM references + * + * @returns {IRow[]} The row options objects with element references + */ + getRowsObject(): IRow[]; + + /** + * Gets the row by index + * + * @returns {HTMLTableRowElement} + */ + getRowByIndex(rowIndex: number): HTMLTableRowElement; + + /** + * Gets the row by uid + * + * @returns {IRow} + */ + getRowObjectFromUID(uid: string): IRow; +} + +/** + * Interface defining the foundational properties and attributes for footer panel component + * + * @private + */ +export interface IFooterPanelBase { + /** + * Attributes for the panel element + */ + panelAttributes?: HTMLAttributes; + + /** + * Attributes for the scroll content element + */ + scrollContentAttributes?: HTMLAttributes; + + /** + * Controls the padding area around the table scrollable region + */ + tableScrollerPadding?: boolean; +} + +/** + * Interface defining the foundational properties and attributes for footer table component + * + * @private + */ +export interface IFooterTableBase extends HTMLAttributes { + /** + * Controls the padding area around the table scrollable region + */ + tableScrollerPadding?: boolean; +} + +/** + * Interface defining the foundational properties and attributes for footer rows component + * + * @private + */ +export interface IFooterRowsBase extends HTMLAttributes { + /** + * Controls the padding area around the table scrollable region + */ + tableScrollerPadding?: boolean; +} + +/** + * Interface for footer panel reference + * + * @private + */ +export interface FooterPanelRef extends FooterTableRef { + /** + * Reference to the footer panel element + */ + readonly footerPanelRef?: HTMLDivElement | null; + + /** + * Reference to the footer scroll element + */ + readonly footerScrollRef?: HTMLDivElement | null; +} + +/** + * Interface for footer table reference + * + * @private + */ +export interface FooterTableRef extends FooterRowsRef { + /** + * Reference to the footer table element + */ + readonly footerTableRef?: HTMLTableElement | null; + getFooterTable?: () => HTMLTableElement | null; +} + +/** + * Interface for footer rows reference + * + * @private + */ +export interface FooterRowsRef { + /** + * Reference to the footer section element + */ + readonly footerSectionRef?: HTMLTableSectionElement | null; + + /** + * Gets the footer rows collection + * + * @returns {HTMLCollectionOf | undefined} The footer rows + */ + getFooterRows?: () => HTMLCollectionOf | undefined; + + /** + * Gets the row options objects with DOM element references + * + * @returns {IRow[]} The row options objects + */ + getFooterRowsObject?: () => IRow[]; +} + +/** + * Base interface for content rows properties + * + * @private + */ +export type IContentRowsBase = HTMLAttributes; + +/** + * Defines event arguments for custom data service requests in the Grid component. + * Provides parameters for querying data from a remote or custom data source, including pagination, filtering, sorting, and searching. + * Used to configure and execute data retrieval operations for the grid. + */ +export interface DataRequestEvent { + /** + * Specifies the number of records to skip in the data source for pagination. + * Used to navigate to specific pages or subsets of data. + * + * @default 0 + */ + skip?: number; + + /** + * Specifies the number of records to retrieve per page from the data source. + * Used to limit the data fetched in a single request. + * + * @default 12 + */ + take?: number; + + /** + * Indicates whether the data service should return both records and the total record count. + * When true, requires the service to include the total count for pagination. + * + * @default false + */ + requiresCounts?: boolean; + + /** + * Contains an array of filter criteria for querying the data source. + * Specifies conditions to filter data, such as equality or range checks on specific fields. + * Used to apply user-defined or programmatic filters to the grid’s data. + * + * @default [] + */ + where?: Predicate[]; + + /** + * Contains an array of sort criteria defining the fields and directions for sorting. + * Specifies how data should be ordered, such as ascending or descending by column. + * Used to apply sorting to the grid’s data based on user or programmatic input. + * + * @default [] + */ + sorted?: Sorts[]; + + /** + * Contains an array of search criteria for full-text or field-specific searches. + * Specifies search terms or conditions to filter data across one or more fields. + * Used to implement search functionality within the grid. + * + * @default [] + */ + search?: SearchSettings[]; + + /** + * Contains an array of aggregation criteria for summarizing data in the grid. + * Specifies functions like sum, average, min, max, or count to be applied on specific fields. + * Used to compute and display aggregated values in the grid. + * + * @default [] + */ + aggregates?: Object[]; + + /** + * Contains details about the grid action (e.g., paging, grouping, filtering, sorting, searching) that triggered the request. + * Provides context about the user or programmatic action driving the data query. + * Used to handle specific action-related logic in the data service. + * + * @default null + */ + action?: FilterEvent | SortEvent | SearchEvent | PageEvent; + + /** + * Specifies the name of the action associated with the data request. + * + * @default - + */ + name?: string; +} + +/** + * Defines event arguments for custom data source change requests in the Grid component. + * Provides parameters for handling data modifications, such as adding, updating, or deleting records. + * Used to manage data change operations and their associated actions in the grid. + */ +export interface DataChangeRequestEvent { + /** + * Specifies the type of action being performed, such as add, edit, or delete. + * + * @default - + */ + action?: string; + + /** + * Specifies the primary column field used to identify records in the data source. + * Defines the unique key for operations like updating or deleting specific records. + * Used to target specific rows for data modifications. + * + * @default - + */ + key?: string | string[]; + + /** + * Contains the state of the grid’s data request, including pagination, filtering, or sorting criteria. + * Provides context for the data change operation, such as the current grid state. + * Used to align the change with the grid’s current data configuration. + * + * @default null + */ + state?: DataRequestEvent; + + /** + * Contains the modified data for the operation. + * Represents the record affected by the data change, such as new or updated rows. + * Used to process or validate the data being modified. + * + * @default null + */ + data?: Object | Object[]; + + /** + * Specifies the index of the row affected by the data change operation. + * Identifies the row’s position in the grid for operations like insertion or selection. + * Used to manage row-specific actions or UI updates. + * + * @default 0 + */ + rowIndex?: number; + + /** + * Specifies a function to finalize the editing process after a data change. + * Executes logic to complete the edit operation. + * Used to manage the conclusion of editing in the grid. + * + * @default null + */ + saveChanges?: Function; + + /** + * Specifies a function to cancel the editing process. + * Executes logic to discard changes and revert the grid to its previous state. + * Used to handle cancellation of inline editing operations. + * + * @default null + */ + cancelChanges?: Function; + + /** + * Specifies a promise that resolves with the result of the data change operation. + * Enables asynchronous handling of data modifications, such as API responses. + * Used to manage the outcome of remote data operations. + * + * @default null + */ + promise?: Promise; +} + +/** + * Defines the pending state for Custom Service Data + * + * @private + */ +export interface PendingState { + /** + * The function which resolves the current action's promise. + */ + resolver?: Function; + /** + * Defines the current state of the action. + */ + isPending?: boolean; + /** + * Defines the edit action. + */ + isEdit?: boolean; +} + +/** + * Interface for header cell render event arguments. + * + * @private + */ +export interface HeaderCellRenderEvent { + /** + * Defines the cell metadata. + * + * @private + */ + cell: ICell; + /** + * Defines the column object associated with this cell. + * + * @default - + */ + column: ColumnProps; + /** Defines the cell element. */ + node: Element; +} + +/** + * Interface for cell render event arguments. + * + * @private + */ +export interface CellRenderEvent { + /** + * Defines the row data associated with this cell. + */ + rowData: Object; + /** Defines the cell element. + * + * @blazorType CellDOM + */ + cell: Element; + /** + * Defines the column object associated with this cell. + */ + column: ColumnProps; + /** + * Defines the number of columns to be spanned. + */ + colSpan: number; + /** + * Defines the number of rows to be spanned. + */ + rowSpan: number; +} + +/** + * Interface for row render event arguments. + * + * @private + */ +export interface RowRenderEvent { + /** + * Defines the current row data. + */ + data: Object; + /** Defines the row element. */ + row?: Element; + /** Defines the row height */ + rowHeight: number; + /** + * Defines whether the row should be selectable or not. + */ + isSelectable: boolean; +} + +/** + * CSS properties for scroll customization + * + * @private + * @interface ScrollCss + */ +export interface ScrollCss { + /** Padding direction based on RTL/LTR mode */ + padding?: 'paddingLeft' | 'paddingRight'; + /** Border direction based on RTL/LTR mode */ + border?: 'borderLeftWidth' | 'borderRightWidth'; +} + +/** + * References to scroll-related DOM elements + * + * @interface ScrollElements + * @private + */ +export interface ScrollElements { + /** Reference to the header scroll container element */ + headerScrollElement: HTMLElement | null; + /** Reference to the content scroll container element */ + contentScrollElement: HTMLElement | null; + /** Reference to the footer scroll container element */ + footerScrollElement: HTMLElement | null; +} + +/** + * Return type for useScroll hook + * + * @private + * @interface UseScrollResult + */ +export interface UseScrollResult { + /** Public API exposed to consumers */ + publicScrollAPI: Partial; + /** Private API for internal component use */ + privateScrollAPI: { + /** Get CSS properties based on RTL/LTR mode */ + getCssProperties: ScrollCss; + /** Border styles for header content */ + headerContentBorder: CSSProperties; + /** Padding styles for header */ + headerPadding: CSSProperties; + /** Event handler for content scroll events */ + onContentScroll: (args: UIEvent) => void; + /** Event handler for header scroll events */ + onHeaderScroll: (args: UIEvent) => void; + /** Event handler for footer scroll events */ + onFooterScroll: (args: UIEvent) => void; + }; + /** Protected API for extended components */ + protectedScrollAPI: { + /** Method to set padding based on scrollbar width */ + setPadding: () => void; + }; + /** Method to set header scroll element reference */ + setHeaderScrollElement: (element: HTMLElement | null) => void; + /** Method to set content scroll element reference */ + setContentScrollElement: (element: HTMLElement | null) => void; + /** Method to set footer scroll element reference */ + setFooterScrollElement: (element: HTMLElement | null) => void; +} + +/** + * The `Sort` module internal properties + * + * @private + */ +export interface SortProperties { + currentTarget: Element | null; + isMultiSort: boolean; + sortSettings: SortSettings; + contentRefresh: boolean; + sortedColumns: string[]; +} + +/** + * Interface for the result of useData hook + * + * @private + */ +export interface UseDataResult { + /** + * The function is used to generate updated Query from Grid model. + */ + generateQuery: () => Query; + + /** + * The DataManager instance for data operations + */ + dataManager: DataManager | DataResult; + + /** + * Check if the data source is remote + */ + isRemote: () => boolean; + + /** + * Perform data operations through DataManager + */ + getData: (args?: { requestType?: string; data?: Object; index?: number }, query?: Query) => + Promise; + dataState: RefObject; +} + +/** + * ServiceLocator for React components + * Provides dependency injection capabilities for various services + * + * @private + */ +export interface ServiceLocator { + /** + * Register a service with the given name + * + * @param name - The name of the service + * @param type - The service implementation + */ + register(name: string, type: T): void; + /** + * Unregister all services + * Used for cleanup + */ + unregisterAll(): void; + + /** + * Get a service by name + * + * @param name - The name of the service to retrieve + * @returns The requested service + * @throws Error if the service is not registered + */ + getService(name: string): T; + + /** + * Dictionary of registered services + */ + readonly services: { + readonly [x: string]: Object; + }; +} + +/** + * Interface for mutable grid state setters. + * + * @private + */ +export interface MutableGridSetter { + /** + * Function to set the current view data. + */ + setCurrentViewData: Dispatch>; + /** + * Function to set the initial load state. + */ + setInitialLoad: Dispatch>; + /** + * Function to set the current page number. + */ + setCurrentPage: Dispatch>; + /** + * Function to set the total records count. + */ + setTotalRecordsCount: Dispatch>; + /** + * Function to set the grid action. + */ + setGridAction: Dispatch>; +} + +/** + * Result interface for the useGridComputedProps hook + * + * @private + */ +export interface GridResult { + /** + * Public API exposed to consumers of the grid + */ + publicAPI: IGrid; + + /** + * Private API for internal grid operations + */ + privateAPI: { + /** + * CSS styles for the grid container + */ + styles: CSSProperties; + isEllipsisTooltip: boolean; + + /** + * Function to update the current view data + */ + setCurrentViewData: Dispatch>; + setInitialLoad: Dispatch>; + handleGridClick: (e: MouseEvent) => void; + handleGridDoubleClick: (e: MouseEvent) => void; + handleGridMouseDown: (e: MouseEvent) => void; + handleGridMouseOut: (e: MouseEvent) => void; + handleGridMouseOver: (e: MouseEvent) => void; + getEllipsisTooltipContent: () => string; + handleGridFocus: (e: FocusEvent) => void; + handleGridBlur: (e: FocusEvent) => void; + handleGridKeyDown: (e: React.KeyboardEvent) => void; + handleGridKeyUp: (e: React.KeyboardEvent) => void; + setCurrentPage: Dispatch>; + setTotalRecordsCount: Dispatch>; + setGridAction: Dispatch>; + }; + + /** + * Protected API for internal grid components + */ + protectedAPI: Partial; +} + + +/** + * Interface for the result of useRender hook + * + * @private + */ +export interface UseRenderResult { + /** + * Public API exposed to consumers + */ + publicRenderAPI: Partial; + + /** + * Private API for internal operations + */ + privateRenderAPI: { + /** + * CSS styles for content panel + */ + contentStyles: CSSProperties; + + /** + * Whether the layout is rendered + */ + isLayoutRendered: boolean; + + /** + * Whether the content is busy + */ + isContentBusy: boolean; + }; + + /** + * Protected API for internal components + */ + protectedRenderAPI: { + /** + * Method to refresh the data + */ + refresh: () => void; + /** Shows a loading spinner overlay on the grid to indicate that an operation is in progress. */ + showSpinner: () => void; + /** Hides the loading spinner overlay that was previously shown on the grid. */ + hideSpinner: () => void; + }; +} + +/** + * Interface for column children props + * + * @private + */ +export interface ColumnsChildren { + children: ReactElement[]; +} + +/** + * Interface for summary data + * + * @private + */ +export interface SummaryData { + aggregates?: Object; + level?: number; + parentUid?: string; +} + +/** + * Interface for group data + * + * @private + */ +export interface Group { + GroupGuid?: string; + level?: number; + childLevels?: number; + records?: Object[]; + key?: string; + count?: number; + items?: Object[]; + aggregates?: Object; + field?: string; + result?: Object; +} diff --git a/components/grids/src/grid/types/page.interfaces.ts b/components/grids/src/grid/types/page.interfaces.ts new file mode 100644 index 0000000..4fb26b1 --- /dev/null +++ b/components/grids/src/grid/types/page.interfaces.ts @@ -0,0 +1,142 @@ +import { ReactElement } from 'react'; +import { GridActionEvent } from '../types/grid.interfaces'; + +/** + * Configures pagination settings for the Grid component. + * Controls the behavior and appearance of pagination, including page size and navigation. + * Enables customization of how data is paginated and displayed in the grid. + */ +export interface PageSettings { + /** + * Determines whether pagination is enabled for the grid. + * When set to true, splits the grid’s data into pages with navigation controls. When false, displays all data without pagination. + * Affects the visibility and functionality of the pager component. + * + * @default false + */ + enabled?: boolean; + + /** + * Specifies the number of records to display on each page of the grid. + * Impacts the pagination calculations and user experience. + * + * @default 12 + * @example + */ + pageSize?: number; + + /** + * Controls the range of page numbers displayed for navigation, enhancing usability. + * Affects the pager’s visual layout and navigation options. + * + * @default 8 + */ + pageCount?: number; + + /** + * Sets the current page number to display in the grid. + * Determines which page of data is actively shown, based on the page size and total records. + * Used to initialize or programmatically navigate to a specific page. + * + * @default 1 + */ + currentPage?: number; + + /** + * Stores the total number of records in the grid’s data source. + * Used internally to calculate the number of pages and display pagination information. + * Automatically set based on the data source and not typically configured by users. + * + * @default 0 + * @private + */ + totalRecordsCount?: number; + + /** + * Defines a custom template for rendering the pager component. + * Allows replacement of the default pager UI with custom strings, React elements, or functions for dynamic rendering. + * Enables advanced customization of the pager’s appearance and behavior. + * + * @default null + */ + template?: string | ReactElement | Function; +} + +/** + * Represents the event triggered when a page change operation completes in the Grid component. + * Provides details about the completed navigation, including page transitions and record count. + * Used to handle post-navigation logic or UI updates in the pager component. + */ +export interface PageEvent extends GridActionEvent { + /** + * Specifies the page number before the navigation action was completed. + * Provides context about the previous state for comparison or logging. + * Useful for tracking page transitions after navigation. + * + * @default 1 + */ + previousPage?: string | number; + + /** + * Specifies the page number after the navigation action is completed. + * Indicates the current page now displayed in the grid. + * Used to update the grid’s data and UI state in the pager component. + * + * @default 1 + */ + currentPage?: string | number; + + /** + * Contains the total number of records in the grid’s data source. + * Used to calculate the total number of pages and update pagination information. + * + * @default 0 + */ + totalRecordsCount?: number; + + /** + * Allows cancellation of the page change action before it is executed. + * When set to true, prevents the grid from navigating to the new page, useful for validation. + * Typically used in event handlers to control pagination behavior. + * + * @private + * @default false + */ + cancel?: boolean; +} + +/** + * Represents arguments for pager events during page navigation in the Grid. + * Provides detailed context about the pagination process, including loading state. + * Used internally to manage pager interactions and state. + * + * @private + */ +export interface PagerArgsInfo extends PageEvent { + /** + * Specifies the page number targeted or active after navigation. + * Reflects the current page being displayed or requested. + * Aligns with the currentPage in other pagination events. + * + * @default 1 + */ + currentPage?: number | string; + + /** + * Specifies the page number before the navigation action. + * Provides the previous page context for tracking transitions. + * Aligns with the previousPage in other pagination events. + * + * @default 1 + */ + oldPage?: number | string; + + /** + * Indicates whether a page request is currently in progress. + * When true, signifies that the grid is loading data for the new page. + * Used to manage UI feedback during asynchronous page loading. + * + * @default false + */ + isPageLoading?: boolean; +} diff --git a/components/grids/src/grid/types/search.interfaces.ts b/components/grids/src/grid/types/search.interfaces.ts new file mode 100644 index 0000000..46f3b55 --- /dev/null +++ b/components/grids/src/grid/types/search.interfaces.ts @@ -0,0 +1,138 @@ +import { Dispatch, SetStateAction } from 'react'; +import { GridActionEvent } from '../types/grid.interfaces'; +import { useSearch } from '../hooks'; + +/** + * Configures search functionality for the Grid component. + * Defines settings for enabling search, specifying search fields, and controlling search behavior. + * Manages how data is filtered based on user search input across grid columns. + */ +export interface SearchSettings { + /** + * Determines whether the search bar is enabled for the grid. + * When set to true, displays the search bar and allows to filter grid data. when false, disables search functionality. + * Affects the visibility and usability of the search UI in the grid. + * + * @default false + */ + enabled?: boolean; + + /** + * Specifies an array of column field names to include in search operations. + * By default, includes all bounded columns in the grid. An empty array means no fields are searched. + * Allows targeting specific columns for search to optimize performance or relevance. + * + * @default [] + */ + fields?: string[]; + + /** + * Defines the initial search value to filter grid records at rendering or the current search value. + * Used to pre-apply a search query on grid initialization or to retrieve the active search string. + * Supports dynamic updates to reflect user input in the search bar. + * + * @default - + */ + value?: string; + + /** + * Specifies the operator used for search operations, such as 'contains', 'startswith', 'endswith', 'equal', or 'notequal'. + * Determines how the search value is matched against column data, affecting search precision. + * Must be compatible with the data types of the searched fields. + * + * @default 'contains' + */ + operator?: string; + + /** + * Controls case sensitivity for string searches in the grid. + * When true, searches are case-insensitive, treating uppercase and lowercase letters the same. When false, searches require exact case matching. + * Only affects string fields and not numbers, dates, or booleans. + * + * @default true + */ + caseSensitive?: boolean; + + /** + * Enables accent-insensitive searching for string fields. + * When true, diacritic characters (e.g., accents like é, ñ) are ignored, treating them as their base characters. + * Enhances search usability for multilingual datasets. + * + * @default false + */ + ignoreAccent?: boolean; +} + + +/** + * Represents the event triggered when a search operation completes in the Grid. + * Provides details about the completed search, including the applied search string. + * Used to handle post-search logic or UI updates. + */ +export interface SearchEvent extends GridActionEvent { + /** + * Specifies the string value used in the completed search operation. + * Reflects the search input that was applied to filter the grid’s records. + * Useful for logging or updating UI after a search. + * + * @default - + */ + value?: string; + + /** + * Allows cancellation of the search action before it is executed. + * When set to true, prevents the search from being applied, useful for validation or conditional logic. + * Typically used in event handlers to control search behavior. + * + * @private + * @default false + */ + cancel?: boolean; +} + +/** + * Defines the type for the search strategy module in the Grid. + * Represents the return type of the useSearch hook for managing search operations. + * Used internally to encapsulate search functionality. + * + * @private + */ +export type searchModule = ReturnType; + +/** + * Defines the API for managing search operations in the Grid. + * Provides methods and properties to control search behavior, state, and updates. + * Used internally to handle search interactions and configuration. + * + * @private + */ +export interface SearchAPI { + /** + * Initiates a search operation with the specified key to filter grid records. + * Applies the search value across the configured fields using the specified operator and settings. + * Triggers a grid refresh to display the filtered results. + * + * @param {string} key - The search key to filter grid records. + * @returns {void} + */ + search: (key: string) => void; + + /** + * Stores the current search settings configuration for the grid. + * Contains properties like enabled state, fields, operator, and case/accent sensitivity. + * Used to access or update the grid’s search configuration. + * + * @default { enabled: false, caseSensitive: true, ignoreAccent: false, operator: 'contains' } + */ + searchSettings: SearchSettings; + + /** + * Provides a function to update the search settings state. + * Used with React’s useState to programmatically modify search configurations. + * Enables dynamic updates to search behavior or UI. + * + * @param {SetStateAction} value - The new search settings or a function to update them. + * @returns {void} + */ + setSearchSetting: Dispatch>; +} diff --git a/components/grids/src/grid/types/selection.interfaces.ts b/components/grids/src/grid/types/selection.interfaces.ts new file mode 100644 index 0000000..d8cec46 --- /dev/null +++ b/components/grids/src/grid/types/selection.interfaces.ts @@ -0,0 +1,291 @@ +import { useSelection } from '../hooks'; +import { CellFocusEvent } from './focus.interfaces'; + +/** + * Configures selection behavior in the Grid component. + * Defines settings for enabling selection, specifying selection mode, and controlling selection type. + * Manages how to interact with row selection in the grid. + */ +export interface SelectionSettings { + /** + * Determines whether row selection is enabled in the grid. + * When set to true, allows to select rows via clicks or keyboard interactions. When false, disables all selection functionality. + * Affects the grid’s interactivity for row-based operations. + * + * @default true + */ + enabled?: boolean; + + /** + * Specifies whether selection toggling is permitted for a selected row. + * When set to false, a selected row cannot be deselected through interaction. + * + * @default true + */ + enableToggle?: boolean; + + /** + * Specifies the selection mode for the grid, controlling how many rows can be selected. + * Supports `Single` for selecting one row at a time or `Multiple` for selecting multiple rows using CTRL or SHIFT keys. + * + * @default 'Single' + */ + mode?: string; + + /** + * Defines the type of selection, such as `Row`, to specify the selection target. + * Used internally to determine whether selection applies to rows or other elements like cells. + * + * @default 'Row' + * @private + */ + type?: string; +} + +/** + * Defines methods and properties for managing row selection state and behavior in the Grid. + * Provides functionality for selecting, deselecting, and retrieving selected rows and their data. + * Used internally to encapsulate selection logic and state management. + * + * @private + */ +export interface SelectionModel { + /** + * Clears all currently selected rows in the grid. + * Resets the selection state, removing any highlighted rows and updating the grid’s UI. + * Useful for programmatically resetting user selections. + * + * @returns {void} + */ + clearSelection: () => void; + + /** + * Deselects specific rows based on their provided indexes. + * Removes the specified rows from the current selection, updating the grid’s visual state. + * Supports partial deselection in multiple selection scenarios. + * + * @param {number[]} indexes - Array of row indexes to deselect. + * @returns {void} + */ + clearRowSelection: (indexes?: number[]) => void; + + /** + * Selects a single row by its index in the grid. + * Highlights the specified row and updates the selection state, optionally toggling selection in multiple mode. + * Used for programmatic row selection or user-initiated clicks. + * + * @param {number} rowIndex - Index of the row to select. + * @param {boolean} isToggle - Whether to toggle selection (for multiple selection). + * @returns {void} + */ + selectRow: (rowIndex: number, isToggle?: boolean) => void; + + /** + * Retrieves the indexes of all currently selected rows in the grid. + * Returns an array of zero-based indexes representing the selected rows. + * Useful for tracking or processing the current selection state. + * + * @returns {number[]} The selected row indexes. + */ + getSelectedRowIndexes: () => number[]; + + /** + * Retrieves the data objects for the currently selected rows. + * Returns the record(s) associated with the selected rows or null if no rows are selected. + * Enables access to selected data for further processing or display. + * + * @returns {object | null} The selected row data. + */ + getSelectedRecords: () => object | null; + + /** + * Processes grid click events to handle row selection. + * Determines whether a click should trigger row selection based on the target element and selection settings. + * Updates the selection state and grid UI accordingly. + * + * @param {React.MouseEvent} event - The mouse event triggered on grid click. + * @returns {void} + */ + handleGridClick: (event: React.MouseEvent) => void; + + /** + * Selects multiple rows by their indexes in the grid. + * Highlights the specified rows and updates the selection state, typically used in multiple selection mode. + * Supports programmatic bulk selection of rows. + * + * @param {number[]} rowIndexes - Specifies an array of row indexes. + * @returns {void} + */ + selectRows: (rowIndexes: number[]) => void; + + /** + * Selects a range of rows from a start index to an optional end index. + * Highlights all rows within the specified range, useful for SHIFT-based range selection. + * Updates the selection state for multiple selection scenarios. + * + * @param {number} startIndex - Specifies the start row index. + * @param {number} endIndex - Specifies the end row index. + * @returns {void} + */ + selectRowByRange: (startIndex: number, endIndex?: number) => void; + + /** + * Adds multiple rows to the current selection by their indexes. + * Expands the existing selection without clearing previously selected rows, used in multiple selection mode. + * Updates the grid’s visual and selection state accordingly. + * + * @param {number[]} rowIndexes - Array of row indexes to select. + * @returns {void} + */ + addRowsToSelection: (rowIndexes: number[]) => void; + + /** + * Stores an array of zero-based indexes for the currently selected rows. + * Tracks the selection state, reflecting which rows are highlighted in the grid. + * Updated dynamically as users or code modify the selection. + * + * @default [] + */ + selectedRowIndexes: number[]; + + /** + * Stores an array of data objects for the currently selected rows. + * Contains the record data for each selected row, enabling access to selected content. + * Updated as the selection changes to reflect the current state. + * + * @default [] + */ + selectedRecords: Object[]; + + /** + * References the currently active target element involved in selection. + * Tracks the DOM element (e.g., a cell or row) that triggered the latest selection action. + * Used internally to manage selection interactions and focus. + * + * @default null + */ + activeTarget: Element; + + /** + * Handles cell focus events to support selection-related behavior. + * Processes focus events to update the selection state or UI when a cell gains focus. + * Used to coordinate keyboard navigation and selection in the grid. + * + * @param {CellFocusEvent} e - The cell focus event arguments. + * @returns {void} + */ + onCellFocus: (e: CellFocusEvent) => void; +} + +/** + * Defines the type for the selection module hook return value in the Grid. + * Represents the return type of the useSelection hook for managing selection operations. + * Used internally to encapsulate selection functionality. + * + * @private + */ +export type selectionModule = ReturnType; + +/** + * Represents event arguments for row selection events in the Grid component. + * Provides detailed context about selected rows, including data and DOM elements. + * Used to handle post-selection logic or UI updates in the row. + */ +export interface RowSelectEvent { + /** + * Contains the data object associated with the selected row. + * Provides access to the record data for single or multiple selected rows for processing or display. + * Returns a single object for single selection or an array for multiple selections. + * + * @default - + */ + data?: object | object[]; + + /** + * Specifies the zero-based index of the selected row in the grid. + * Identifies the position of the selected row within the data source for reference or manipulation. + * Used in single selection mode or to track the primary selected row. + * + * @default - + */ + rowIndex?: number; + + /** + * Contains an array of zero-based indexes for all selected rows. + * Used in multiple selection mode to track all rows currently highlighted. + * Enables bulk processing of selected row positions. + * + * @default [] + */ + rowIndexes?: number[]; + + /** + * References the DOM elements of the selected rows. + * Provides access to the row elements for styling, manipulation, or other DOM operations. + * Returns a single element or an array based on selection mode. + * + * @default null + */ + row?: Element | Element[]; + + /** + * References the DOM element that triggered the row selection event. + * Typically a cell or other element clicked by the user to initiate selection. + * Used to identify the source of the selection action. + * + * @default null + */ + target?: Element; + + /** + * Specifies the zero-based index of the previously selected row, if any. + * Tracks the prior selection state to monitor changes or transitions in selection. + * Useful for comparing current and previous selections. + * + * @default - + */ + previousRowIndex?: number; + + /** + * References the DOM element of the previously selected row, if any. + * Provides access to the prior row’s DOM properties for manipulation or comparison. + * Used to handle transitions between selections. + * + * @default null + */ + previousRow?: Element; +} + +/** + * Represents event arguments for row selecting events in the Grid, extending RowSelectEvent. + * Includes additional properties to control selection behavior, such as key modifiers and cancellation. + * Used internally to manage the selection process before it is finalized. + * + * @private + */ +export interface RowSelectingEvent extends RowSelectEvent { + /** + * Indicates whether the CTRL key was pressed during the selection event. + * When true, enables additive selection in multiple selection mode, allowing users to select multiple rows. + * Used to detect user intent for multi-selection behavior. + * + * @default false + */ + isCtrlPressed?: boolean; + + /** + * Indicates whether the SHIFT key was pressed during the selection event. + * When true, enables range selection in multiple selection mode, selecting all rows between two points. + * Used to detect user intent for range-based selection. + * + * @default false + */ + isShiftPressed?: boolean; + + /** + * Determines whether the selection event should be canceled. + * When set to true, prevents the row(s) from being selected, allowing validation or conditional logic. + * Used in event handlers to control selection outcomes. + */ + cancel: boolean; +} diff --git a/components/grids/src/grid/types/sort.interfaces.ts b/components/grids/src/grid/types/sort.interfaces.ts new file mode 100644 index 0000000..74c81c1 --- /dev/null +++ b/components/grids/src/grid/types/sort.interfaces.ts @@ -0,0 +1,247 @@ +import { Dispatch, SetStateAction } from 'react'; +import { SortDirection } from './enum'; +import { GridActionEvent } from './grid.interfaces'; +import { useSort } from '../hooks'; + +/** + * Defines the configuration for a sort descriptor in the Grid component. + * Specifies the column `field` and `direction` for sorting operations. + * Used to describe individual column sorting rules within the grid. + */ +export interface SortDescriptorModel { + /** + * Identifies the column `field` in the data source to apply sorting operations. + * Determines which column’s data is sorted during the operation. + * + * @default - + */ + field?: string; + + /** + * Specifies the `direction` of the sort operation for the column. + * Supports values like ascending or descending, typically defined by the SortDirection enum. + * Controls whether the column data is sorted in ascending or descending order. + * + * @default SortDirection.Ascending | 'Ascending' + */ + direction?: SortDirection | string; +} + +/** + * Enumerates the sorting modes supported by the Grid. + * Defines whether sorting is restricted to a single column or allows multiple columns. + * Used internally to configure sorting behavior. + * + * @private + */ +export type SortMode = 'single' | 'multiple'; + +/** + * Configures sorting behavior in the Grid component. + * Manages settings for enabling sorting, defining sorted columns, and controlling sort persistence. + * Determines how to interact with column sorting via headers or programmatically. + */ +export interface SortSettings { + /** + * Contains an array of `SortDescriptorModel` objects to define initial or active sort conditions. + * Specifies which columns are sorted and in what direction at grid initialization or during runtime. + * Enables pre-sorting data or retrieving the current sort state. + * + * @default [] + */ + columns?: SortDescriptorModel[]; + + /** + * Determines whether clicking a sorted column header can clear its sort state. + * When false, prevents the grid from remove the sorting a column, maintaining the sort order. When true, allows toggling to an unsorted state. + * Affects user interaction with sorted column headers. + * + * @default true + */ + allowUnsort?: boolean; + + /** + * Enables or disables sorting functionality for grid columns. + * When true, allows to sort data by clicking column headers, with support for multiple columns using the Ctrl key. + * When false, disables all sorting interactions and programmatic sorting. + * + * @default false + */ + enabled?: boolean; + + /** + * Specifies whether sorting is restricted to a single column or allows multiple columns. + * Supports `single` for sorting one column at a time or `multiple` for sorting multiple columns simultaneously. + * Influences the grid’s sorting behavior and user experience. + * + * @default 'multiple' + */ + mode?: SortMode; +} + +/** + * Defines sorting properties for custom data services in the Grid. + * Specifies the field and direction for sorting operations in external data handling. + * Used internally to integrate with custom data sources or APIs. + * + * @private + */ +export interface Sorts { + /** + * Identifies the field name of the column to be sorted in the data source. + * Maps to the column field used for sorting in custom data service operations. + * Ensures accurate targeting of the column for external sorting logic. + * + * @default - + */ + name?: string; + + /** + * Specifies the direction of the sort operation for the column. + * Supports values like 'asc' for ascending or 'desc' for descending order. + * Determines the order in which data is sorted for the specified field. + * + * @default 'asc' + */ + direction?: string; +} + +/** + * Represents event arguments for sort complete events in the Grid. + * Provides details about the completed sort operation, including column and direction. + * Used to handle post-sort logic or UI updates in the grid header. + */ +export interface SortEvent extends GridActionEvent { + /** + * Specifies the `field` name of the column that was sorted. + * Identifies the column in the data source affected by the completed sort operation. + * Useful for logging or updating UI after sorting. + * + * @default - + */ + field?: string; + + /** + * Defines the direction of the completed sort operation for the column. + * Indicates whether the column was sorted in ascending or descending order, typically from the `SortDirection` enum. + * + * @default SortDirection.Ascending | 'Ascending' + */ + direction?: SortDirection | string; + + /** + * References the DOM element associated with the completed sort action, such as the column header. + * Identifies the element that triggered the sort operation, typically a clicked header. + * Used to manage UI feedback or post-sort interactions. + * + * @default null + */ + target?: Element; + + /** + * Allows cancellation of the sort action before it is applied. + * When set to true, prevents the sort operation from executing, useful for validation or conditional logic. + * Typically used in event handlers to control sorting behavior. + * + * @private + * @default false + */ + cancel?: boolean; + + /** + * Indicates the type of sort action that was completed (e.g., 'sorting', 'clearSorting'). + * Describes the operation performed, aiding in post-sort processing. + * Helps differentiate between various sort-related actions. + * + * @type {string} + * @default - + */ + action?: 'sorting' | 'clearSorting'; +} + +/** + * Defines the type for the sort strategy module in the Grid. + * Represents the return type of the useSort hook for managing sorting operations. + * Used internally to encapsulate sorting functionality. + * + * @private + */ +export type SortModule = ReturnType; + +/** + * Defines the API for handling sorting actions in the Grid. + * Provides methods and properties to manage sort operations, state, and user interactions. + * Used internally to control sorting behavior and configuration. + * + * @private + */ +export interface SortAPI { + /** + * Initiates a sort operation on a specified column with given options. + * Applies sorting to the column using the provided direction, with an option to maintain previous sorts in multi-sort mode. + * Updates the grid’s data and UI to reflect the new sort order. + * + * @param {string} columnName - Defines the column name to be sorted. + * @param {SortDirection | string} sortDirection - Defines the direction of sorting field. + * @param {boolean} isMultiSort - Specifies whether the previous sorted columns are to be maintained. + * @returns {void} + */ + sortByColumn?(columnName: string, sortDirection: SortDirection | string, isMultiSort?: boolean): void; + + /** + * Removes the sort condition for a specific column by its field name. + * Clears sorting for the specified column, updating the grid’s data and UI to reflect the change. + * Useful for programmatically resetting sort state for individual columns. + * + * @param {string} columnName - Defines the column name to remove sorting from. + * @returns {void} + */ + removeSortColumn?(columnName: string): void; + + /** + * Clears all sorting conditions applied to the grid’s columns. + * Resets the sort state, removing all sorted columns and reverting to the original data order. + * Updates the grid’s UI to reflect the unsorted state. + * + * @returns {void} + */ + clearSort?(): void; + + /** + * Processes grid click events to handle sorting functionality. + * Determines whether a click on a column header should trigger a sort operation based on the target and sort settings. + * Updates the sort state and grid UI accordingly. + * + * @param {React.MouseEvent} event - The mouse event triggered on grid click. + * @returns {void} + */ + handleGridClick: (event: React.MouseEvent) => void; + + /** + * Processes keyboard up events to handle sorting functionality. + * Handles key presses (e.g., Enter or Space) on column headers to trigger sorting actions. + * Enhances accessibility and user interaction with sorting controls. + * + * @param {React.KeyboardEvent} event - The keyboard event triggered on key up. + * @returns {void} + */ + keyUpHandler: (event: React.KeyboardEvent) => void; + + /** + * Stores the current sort settings configuration for the grid. + * Contains properties like enabled state, sorted columns, and sorting mode. + * Used to access or update the grid’s sorting configuration. + * + * @default {} + */ + sortSettings: SortSettings; + + /** + * Provides a function to update the sort settings state. + * Used with React’s useState to programmatically modify sorting configurations. + * Enables dynamic updates to sorting behavior or UI. + * + * @default null + */ + setSortSettings: Dispatch>; +} diff --git a/components/grids/src/grid/types/toolbar.interfaces.ts b/components/grids/src/grid/types/toolbar.interfaces.ts new file mode 100644 index 0000000..b1c50e7 --- /dev/null +++ b/components/grids/src/grid/types/toolbar.interfaces.ts @@ -0,0 +1,372 @@ +import { IToolbar } from '@syncfusion/react-navigations'; + +/** + * Defines the event arguments triggered by toolbar click actions in the Grid component. Includes details about the clicked item, the originating event, and support for preventing default behavior. Used to process user interactions with toolbar commands and apply custom logic before execution. + */ +export interface ToolbarClickEvent { + /** + * Represents the toolbar item that was clicked, containing its unique identifier and display text. + * Provides details about the specific toolbar button or control that triggered the event. + * Enables identification and processing of the clicked item’s properties. + * + * @default null + */ + item?: { + /** + * Specifies the unique identifier of the clicked toolbar item. + * Used to distinguish the item within the toolbar for processing or conditional logic. + * Matches the ID defined in the toolbar item configuration. + * + * @default - + */ + id?: string; + + /** + * Specifies the display text of the clicked toolbar item. + * Reflects the visible label or text shown on the toolbar button or control. + * Useful for identifying the item’s purpose or for UI feedback. + * + * @default - + */ + text?: string; + }; + + /** + * Contains the original browser event that triggered the toolbar click. + * Provides access to native event properties, such as mouse coordinates or event type, for advanced handling. + * Useful for custom event processing or interaction with the DOM. + * + * @default null + */ + event?: Event; + + /** + * Indicates whether the default toolbar action should be canceled. When props.cancel is set to true, the associated command is immediately prevented from executing. + */ + cancel?: boolean; +} + +/** + * Defines methods and properties for managing toolbar behavior in the Grid. + * Provides functionality to control toolbar rendering, item states, and click handling. + * Used internally to encapsulate toolbar operations and state management. + * + * @private + */ +export interface ToolbarAPI { + /** + * Retrieves the DOM element of the toolbar in the grid. + * Returns the HTMLElement representing the toolbar or null if not rendered. + * Enables access to the toolbar for manipulation or inspection. + * + * @returns {HTMLElement | null} The toolbar element. + */ + getToolbar: () => HTMLElement | null; + + /** + * Enables or disables specified toolbar items based on their IDs. + * Updates the interaction state of the items, affecting their appearance and functionality. + * Useful for dynamically controlling toolbar item availability based on grid state. + * + * @param {string[]} items - Array of item IDs to enable or disable. + * @param {boolean} isEnable - Whether to enable or disable the items. + * @returns {void} + */ + enableItems: (items: string[], isEnable: boolean) => void; + + /** + * Refreshes the toolbar items to reflect the current grid state. + * Updates the appearance and state of toolbar items, such as enabled or disabled status. + * Ensures the toolbar UI remains consistent with grid actions or data changes. + * + * @returns {void} + */ + refreshToolbarItems: () => void; + + /** + * Processes click events on toolbar items to trigger associated actions. + * Handles user interactions with the toolbar, passing event details for custom logic. + * Updates the grid or toolbar state based on the clicked item. + * + * @param {ToolbarClickEvent} args - Click event arguments. + * @returns {void} + */ + handleToolbarClick: (args: ToolbarClickEvent) => void; + + /** + * Indicates whether the toolbar has been rendered in the grid. + * When true, confirms the toolbar is visible and functional; when false, indicates it is not rendered. + * Used to check the toolbar’s availability for operations. + * + * @default false + */ + isRendered: boolean; + + /** + * Stores a set of IDs for currently active toolbar items. + * Tracks which items are in an active or highlighted state, such as during user interaction. + * Enables dynamic management of toolbar item states. + * + * @default new Set() + */ + activeItems: Set; + + /** + * Stores a set of IDs for currently disabled toolbar items. + * Tracks which items are disabled and non-interactive, reflecting their state in the UI. + * Used to manage item availability during grid operations. + * + * @default new Set() + */ + disabledItems: Set; + + /** + * References the toolbar component for accessing its internal methods and properties. + * Provides a React ref object to interact with the toolbar’s underlying functionality. + * Used internally for advanced toolbar control and integration. + * + * @default null + */ + toolbarRef: React.RefObject; +} + +/** + * Configures individual toolbar items for the Grid’s toolbar. + * Defines properties for buttons or controls, such as ID, text, and behavior. + * Used to customize the appearance and functionality of toolbar items. + */ +export interface ToolbarItemConfig { + /** + * Specifies a unique identifier for the toolbar item. + * Used to distinguish the item within the toolbar for event handling or state management. + * Ensures accurate targeting of specific items in the toolbar. + * + * @default - + */ + id: string; + + /** + * Specifies the display text for the toolbar item. + * Represents the visible label shown on the button or control in the toolbar UI. + * Enhances user understanding of the item’s purpose or action. + * + * @default - + */ + text: string; + + /** + * Defines an icon for the toolbar item, typically an SVG element for optimal rendering. + * Enhances the visual representation of the item, complementing or replacing the text. + * Used to improve the toolbar’s aesthetic and usability. + * + * @default null + */ + icon?: React.ReactNode; + + /** + * Determines whether the toolbar item is disabled and non-interactive. + * When true, prevents user interaction with the item, visually indicating its disabled state. + * Useful for controlling item availability based on grid context. + * + * @default false + */ + disabled?: boolean; + + /** + * Specifies a custom click handler function for the toolbar item. + * Executes custom logic when the item is clicked, allowing tailored grid interactions. + * Enables developers to define specific actions for toolbar controls. + * + * @default null + */ + onClick?: () => void; + + /** + * Specifies the tooltip text displayed when hovering over the toolbar item. + * Provides additional context or description for the item’s purpose or functionality. + * Enhances user experience by offering guidance on toolbar actions. + * + * @default - + */ + title?: string; +} + +/** + * Configures the entire toolbar component for the Grid. + * Defines settings for toolbar items, event handling, and styling. + * Used internally to manage the toolbar’s setup and behavior. + * + * @private + */ +export interface ToolbarConfig { + /** + * Contains an array of toolbar item definitions or string identifiers for prebuilt items. + * Specifies the collection of buttons or controls to display in the toolbar. + * Enables customization of the toolbar’s content and layout. + * + * @default [] + */ + toolbar?: (string | ToolbarItemConfig)[]; + + /** + * Specifies a unique identifier for the grid to generate unique toolbar item IDs. + * Ensures that toolbar item IDs are distinct across multiple grid instances. + * Used internally to prevent ID conflicts in the toolbar. + * + * @default - + */ + gridId?: string; + + /** + * Defines an event handler for toolbar item click events. + * Processes click events for all toolbar items, receiving detailed event arguments. + * Allows custom logic to be executed based on user interactions with the toolbar. + * + * @default null + */ + onToolbarItemClick?: (args: ToolbarClickEvent) => void; + + /** + * References the ToolbarAPI instance for managing toolbar operations. + * Provides access to methods and properties for controlling the toolbar’s behavior. + * Used internally to integrate the toolbar with grid functionality. + * + * @default null + */ + toolbarAPI?: ToolbarAPI; + + /** + * Specifies a CSS class to apply to the toolbar for custom styling. + * Allows customization of the toolbar’s appearance to match the application’s design. + * Enhances the visual integration of the toolbar with the grid. + * + * @default - + */ + className?: string; +} + +/** + * Defines the internal configuration for toolbar items in the Grid. + * Specifies detailed properties for rendering and managing toolbar buttons or controls. + * Used internally to handle advanced toolbar item settings. + * + * @private + */ +export interface ToolbarItem { + /** + * Specifies the unique identifier for the toolbar item. + * Used to associate the item with its button or input element in the toolbar. + * Ensures accurate targeting for event handling and state management. + * + * @default - + */ + id?: string; + + /** + * Specifies the display text for the toolbar button or control. + * Represents the visible label shown in the toolbar UI to indicate the item’s purpose. + * Enhances user understanding and interaction with the toolbar. + * + * @default - + */ + text?: string; + + /** + * Defines the width of the toolbar button or command, in pixels or as a string. + * Controls the visual size of the item, ensuring proper layout in the toolbar. + * Set to 'auto' to adapt to content or available space. + * + * @default 'auto' + */ + width?: number | string; + + /** + * Determines whether the item is displayed in normal or overflow mode. + * Supports 'Show' for always visible or 'Hide' for overflow menu placement when space is limited. + * Manages toolbar item visibility in constrained layouts. + * + * @default 'Show' + */ + overflow?: 'Show' | 'Hide'; + + /** + * Specifies the alignment of the toolbar item within the toolbar. + * Supports 'Left', 'Center', or 'Right' to position the item in the toolbar layout. + * Controls the visual arrangement of items for better organization. + * + * @default 'Left' + */ + align?: 'Left' | 'Center' | 'Right'; + + /** + * Specifies the type of toolbar command to render, such as button, separator, or input. + * Determines the item’s role and rendering style in the toolbar. + * Affects how the item is displayed and interacted. + * + * @default 'Button' + */ + type?: 'Button' | 'Separator' | 'Input'; + + /** + * Defines a custom template for the toolbar item, as a string or function. + * Allows rendering custom HTML or React components instead of the default button or control. + * Enables advanced customization of the item’s appearance and behavior. + * + * @default null + */ + template?: string | Function; + + /** + * Specifies the tooltip text displayed when hovering over the toolbar item. + * Provides additional context or description for the item’s functionality. + * Enhances user experience by offering guidance on toolbar actions. + * + * @default - + */ + tooltipText?: string; + + /** + * Defines an icon for the toolbar item, typically an SVG element for optimal rendering. + * Enhances the visual representation of the item, complementing or replacing the text. + * Improves the toolbar’s aesthetic and usability. + * + * @default null + */ + icon?: React.ReactNode; + + /** + * Determines whether the toolbar item is disabled and non-interactive. + * When true, prevents user interaction and visually indicates the disabled state. + * Useful for controlling item availability based on grid context. + * + * @default false + */ + disabled?: boolean; + + /** + * Determines whether the toolbar item is visible in the UI. + * When false, hides the item from the toolbar, preventing display and interaction. + * Allows dynamic control of item visibility based on application state. + * + * @default true + */ + visible?: boolean; + + /** + * Specifies the tab order for the toolbar item in keyboard navigation. + * Defines the sequence in which the item is focused when using the Tab key. + * Enhances accessibility by controlling focus order in the toolbar. + * + * @default 0 + */ + tabIndex?: number; + + /** + * Specifies additional HTML attributes to apply to the toolbar item’s element. + * Allows customization of the item’s DOM properties, such as data attributes or ARIA labels. + * Enhances flexibility for styling or accessibility requirements. + * + * @default {} + */ + htmlAttributes?: { [key: string]: string | number | boolean | undefined }; +} diff --git a/components/grids/src/grid/utils/index.ts b/components/grids/src/grid/utils/index.ts new file mode 100644 index 0000000..04bca77 --- /dev/null +++ b/components/grids/src/grid/utils/index.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/components/grids/src/grid/utils/utils.ts b/components/grids/src/grid/utils/utils.ts new file mode 100644 index 0000000..e86fc68 --- /dev/null +++ b/components/grids/src/grid/utils/utils.ts @@ -0,0 +1,435 @@ + +import { DateFormatOptions, isNullOrUndefined, isUndefined, NumberFormatOptions, extend as baseExtend, getDatePattern, removeClass, addClass } from '@syncfusion/react-base'; +import { DataUtil, Predicate } from '@syncfusion/react-data'; +import { EditSettings, IValueFormatter, ValueType } from '../types'; +import { FilterPredicates } from '../types/filter.interfaces'; +import { ServiceLocator } from '../types/interfaces'; +import { ColumnProps, HeaderValueAccessorEvent, ValueAccessorEvent } from '../types/column.interfaces'; + +/** + * Function to get value from provided data + * + * @param {ValueAccessorEvent} props - specifies the onValueAccessor event props. + * @returns {Object} returns the object + * @private + */ + +// eslint-disable-next-line +export function valueAccessor(props: ValueAccessorEvent): Object { + const { field, rowData: data } = props; + return (isNullOrUndefined(field) || field === '') ? '' : DataUtil.getObject(field, data); +} + +/** + * Defines the method used to apply custom header cell values from external function and display this on each header cell rendered. + * + * @param {HeaderValueAccessorEvent} props - specifies the onHeaderValueAccessor event props + * @returns {object} headerValueAccessor + * @private + */ +export function headerValueAccessor(props: HeaderValueAccessorEvent): Object { + const { headerText, column } = props; + return DataUtil.getObject(headerText, column); +} + +/** + * @param {string} field - Defines the Field + * @param {Object} object - Defines the objec + * @returns {string | number | boolean | Object | undefined} Returns the object + * @private + */ +export const getObject: (field: string, object?: Object) => string | number | boolean | Object | undefined = + (field: string, object?: Object): string | number | boolean | Object | undefined => { + let value: { [key: string]: string | number | boolean | Object | undefined } = + object as { [key: string]: string | number | boolean | Object | undefined }; + const splits: string[] = field.split('.'); + for (let i: number = 0; i < splits.length && !isNullOrUndefined(value); i++) { + const key: string = splits[i as number]; + value = value[key as string] as { [key: string]: string | number | boolean | Object | undefined }; + if (isUndefined(value) && object) { + const pascalCase: string = key.charAt(0).toUpperCase() + key.slice(1); + const camelCase: string = key.charAt(0).toLowerCase() + key.slice(1); + value = object[pascalCase as string] || object[camelCase as string]; + } + } + return value; + }; + +export const setStringFormatter: (fmtr: IValueFormatter, type: string, format: string) => Function | undefined = + (fmtr: IValueFormatter, type: string, format: string): Function | undefined => { + let args: object = {}; + if (type === 'date' || type === 'datetime' || type === 'dateonly') { + const actualType: string = type === 'dateonly' ? 'date' : type; + args = { type: actualType, skeleton: format }; + if (typeof format === 'string' && format !== 'yMd') { + (args as { [key: string]: string })['format'] = format; + } + } + switch (type) { + case 'date': + case 'dateonly': + case 'datetime': + return fmtr.getFormatFunction?.(args as DateFormatOptions); + case 'number': + return fmtr.getFormatFunction?.({ format: format } as NumberFormatOptions); + default: + return undefined; + } + }; + +/** + * @param {ValueType} value - Defines the value + * @returns {boolean} - whether value is date or number. + * @private + */ +export const isDateOrNumber: (value: ValueType) => boolean = (value: ValueType): boolean => { + let isDateOrNumber: boolean = false; + if (typeof value === 'number') { + isDateOrNumber = true; + } else if (typeof value === 'string') { + // Check if it's a valid number + const num: number = Number(value); + if (!isNaN(num)) { + isDateOrNumber = true; + } else { + // Check if it's a valid date + const dateValue: Date = new Date(value); + isDateOrNumber = !isNaN(dateValue.getTime()); + } + } else if (typeof value === 'object') { + // Try converting object to date + const dateValue: Date = new Date(value as string); + isDateOrNumber = !isNaN(dateValue.getTime()); + } + return isDateOrNumber; +}; + +/** + * @param {ServiceLocator} serviceLocator - Defines the service locator + * @param {ColumnProps} column - Defines the column + * @returns {void} + * @private + */ +export function setFormatter(serviceLocator?: ServiceLocator, column?: ColumnProps): void { + const fmtr: IValueFormatter = serviceLocator.getService('valueFormatter'); + const format: string = 'format'; + let args: object; + if (column.type === 'date' || column.type === 'datetime' || column.type === 'dateonly') { + args = { type: column.type === 'dateonly' ? 'date' : column.type, skeleton: column.format }; + if ((typeof (column.format) === 'string') && column.format !== 'yMd') { + args[`${format}`] = column.format; + } + } + switch (column.type) { + case 'date': + column.formatFn = fmtr.getFormatFunction(args as DateFormatOptions); + column.parseFn = fmtr.getParserFunction(args as DateFormatOptions); + break; + case 'dateonly': + column.formatFn = fmtr.getFormatFunction(args as DateFormatOptions); + column.parseFn = fmtr.getParserFunction(args as DateFormatOptions); + break; + case 'datetime': + column.formatFn = fmtr.getFormatFunction(args as DateFormatOptions); + column.parseFn = fmtr.getParserFunction(args as DateFormatOptions); + break; + case 'number': + column.formatFn = fmtr.getFormatFunction({ format: column.format } as NumberFormatOptions); + column.parseFn = fmtr.getParserFunction({ format: column.format } as NumberFormatOptions); + break; + } +} + +let uid: number = 0; +/** + * @param {string} prefix - Defines the prefix string + * @returns {string} Returns the uid + * @private + */ +export function getUid(prefix: string): string { + return prefix + uid++; +} + + +/** + * @param {FilterPredicates} filterObject - Defines the filterObject + * @param {string} type - Defines the type + * @param {boolean} isExecuteLocal - Defines whether the data actions performed in client and used for dateonly type field + * @returns {Predicate} Returns the Predicate + * @private + */ +export function getDatePredicate(filterObject: FilterPredicates, type?: string, isExecuteLocal?: boolean): Predicate { + let datePredicate: Predicate; + let prevDate: Date; + let nextDate: Date; + const prevObj: FilterPredicates = baseExtend({}, filterObject) as FilterPredicates; + const nextObj: FilterPredicates = baseExtend({}, filterObject) as FilterPredicates; + if (isNullOrUndefined(filterObject.value) || filterObject.value === '') { + datePredicate = new Predicate(prevObj.field, prevObj.operator, prevObj.value, false); + return datePredicate; + } + const value: Date = new Date(filterObject.value as string); + if (type === 'dateonly' && !isExecuteLocal) { + if (typeof (prevObj.value) === 'string') { + prevObj.value = new Date(prevObj.value); + } + const dateOnlyString: string = (prevObj.value as Date).getFullYear() + '-' + padZero((prevObj.value as Date).getMonth() + 1) + '-' + padZero((prevObj.value as Date).getDate()); + const predicates: Predicate = new Predicate(prevObj.field, prevObj.operator, dateOnlyString, false); + datePredicate = predicates; + } else { + filterObject.operator = filterObject.operator.toLowerCase(); + if (filterObject.operator === 'equal' || filterObject.operator === 'notequal') { + if (type === 'datetime') { + prevDate = new Date(value.setSeconds(value.getSeconds() - 1)); + nextDate = new Date(value.setSeconds(value.getSeconds() + 2)); + filterObject.value = new Date(value.setSeconds(nextDate.getSeconds() - 1)); + } else { + prevDate = new Date(value.setHours(0) - 1); + nextDate = new Date(value.setHours(24)); + } + prevObj.value = prevDate; + nextObj.value = nextDate; + if (filterObject.operator === 'equal') { + prevObj.operator = 'greaterthan'; + nextObj.operator = 'lessthan'; + } else { + prevObj.operator = 'lessthanorequal'; + nextObj.operator = 'greaterthanorequal'; + } + const predicateSt: Predicate = new Predicate(prevObj.field, prevObj.operator, prevObj.value, false); + const predicateEnd: Predicate = new Predicate(nextObj.field, nextObj.operator, nextObj.value, false); + datePredicate = filterObject.operator === 'equal' ? predicateSt.and(predicateEnd) : predicateSt.or(predicateEnd); + } else { + if (type === 'date' && (filterObject.operator === 'lessthanorequal' || filterObject.operator === 'greaterthan')) { + prevObj.value = new Date(value.setHours(24) - 1); + } + if (typeof (prevObj.value) === 'string') { + prevObj.value = new Date(prevObj.value); + } + const predicates: Predicate = new Predicate(prevObj.field, prevObj.operator, prevObj.value, false); + datePredicate = predicates; + } + } + filterObject.ejpredicate = datePredicate; + return datePredicate; +} + +/** + * @param {number} value - Defines the date or month value + * @returns {string} Returns string + * @private + */ +export function padZero(value: number): string { + if (value < 10) { + return '0' + value; + } + return String(value); +} + +/** + * @param {Object} collection - Defines the collection + * @returns {Object} Returns the object + * @private + */ +export function getActualPropFromColl(collection: Object[]): Object[] { + const coll: Object[] = []; + for (let i: number = 0, len: number = collection.length; i < len; i++) { + // eslint-disable-next-line no-prototype-builtins + if (collection[parseInt(i.toString(), 10)].hasOwnProperty('properties')) { + coll.push((collection[parseInt(i.toString(), 10)] as { properties: Object }).properties); + } else { + coll.push(collection[parseInt(i.toString(), 10)]); + } + } + return coll; +} + +/** + * @param {Object[]} collection - Defines the array + * @param {Object} predicate - Defines the predicate + * @returns {Object} Returns the object + * @private + */ +export function iterateArrayOrObject(collection: U[], predicate: (item: Object, index: number) => T): T[] { + const result: T[] = []; + for (let i: number = 0, len: number = collection.length; i < len; i++) { + const pred: T = predicate(collection[parseInt(i.toString(), 10)], i); + if (!isNullOrUndefined(pred)) { + result.push(pred); + } + } + return result; +} + +/** + * @param {FilterPredicates} filter - Defines the FilterPredicates + * @returns {boolean} Returns the object + * @private + */ +export function getCaseValue(filter: FilterPredicates): boolean { + if (isNullOrUndefined(filter.caseSensitive)) { + if (filter.type === 'string' || isNullOrUndefined(filter.type) && typeof (filter.value) === 'string') { + return false; + } else { + return true; + } + } else { + return filter.caseSensitive; + } +} + +/** + * @param {string | Object} format - defines the format + * @param {string} colType - Defines the coltype + * @returns {string} Returns the custom Data format + * @private + */ +export function getCustomDateFormat(format: string | Object, colType: string): string { + let formatvalue: string; + const formatter: string = 'format'; + const type: string = 'type'; + if (colType === 'date') { + formatvalue = typeof (format) === 'object' ? + getDatePattern({ type: format[`${type}`] ? format[`${type}`] : 'date', format: format[`${formatter}`] }, false) : + getDatePattern({ type: 'dateTime', skeleton: format }, false); + } else { + formatvalue = typeof (format) === 'object' ? + getDatePattern({ type: format[`${type}`] ? format[`${type}`] : 'dateTime', format: format[`${formatter}`] }, false) : + getDatePattern({ type: 'dateTime', skeleton: format }, false); + } + return formatvalue; +} + + +/** + * Compare specific properties of two objects for equality + * + * @param {Object} obj1 - First object to compare + * @param {Object} obj2 - Second object to compare + * @param {Array} keys - Array of keys to include in comparison (only these will be compared) + * @returns {boolean} boolean indicating if specified properties are equal + * @private + */ +export function compareSelectedProperties( + obj1: T, + obj2: U, + keys: Array +): boolean { + return keys.every((key: string & (keyof T | keyof U)) => { + // Check if key exists in both objects + const existsInObj1: boolean = obj1 && key in obj1; + const existsInObj2: boolean = obj2 && key in obj2; + + // If key doesn't exist in both objects, they're different + if (existsInObj1 !== existsInObj2) { return false; } + + // If key doesn't exist in either object, they're equal (for this property) + if (!existsInObj1 && !existsInObj2) { return true; } + + // Compare values using type-safe comparison + return compareValues( + (obj1 as Record)[key as string & (keyof T | keyof U)], + (obj2 as Record)[key as string & (keyof T | keyof U)] + ); + }); +} + +/** + * Type-safe comparison of two values of unknown types + * Uses type guards to safely compare values of different types + * + * @param {string | number | object | Date} val1 - first object comapring value + * @param {string | number | object | Date} val2 - second object comparing value + * @returns {boolean} - is values matched + * @private + */ +export function compareValues(val1: string | number | object | Date | boolean, val2: string | number | object | Date | boolean): boolean { + // Handle null/undefined cases + if (val1 == null || val2 == null) { + return val1 === val2; + } + + // Handle primitive types + if (typeof val1 !== 'object' && typeof val2 !== 'object') { + return val1 === val2; + } + + // Handle Date objects + if (val1 instanceof Date && val2 instanceof Date) { + return val1.getTime() === val2.getTime(); + } + + // Handle arrays with type guards + if (Array.isArray(val1) && Array.isArray(val2)) { + if (val1.length !== val2.length) { return false; } + + // Simple array comparison for primitive arrays (faster) + const allPrimitives: boolean = val1.every((item: string | number | object | Date) => typeof item !== 'object' || item === null); + if (allPrimitives) { + return val1.every((item: string | number | object | Date, index: number) => item === val2[index as number]); + } + + // Deep comparison for object arrays + return val1.every((item: string | number | object | Date, index: number) => compareValues(item, val2[index as number])); + } + + // If one is array but other is not + if (Array.isArray(val1) !== Array.isArray(val2)) { return false; } + + // Handle objects + if (typeof val1 === 'object' && typeof val2 === 'object' && val1 !== null && val2 !== null) { + // For nested objects, compare all properties recursively + const keys1: string[] = Object.keys(val1); + const keys2: string[] = Object.keys(val2); + + // If number of keys doesn't match, objects are different + if (keys1.length !== keys2.length) { return false; } + + // Check if all keys in val1 have the same values in val2 + return keys1.every((key: string) => + key in (val2 as Object) && + compareValues( + (val1 as Object)[key as string], + (val2 as Object)[key as string] + ) + ); + } + + // Fallback comparison (should not reach here with proper type guards) + return Object.is(val1, val2); +} +/** + * Parses a CSS-style unit string and extracts the numeric value. + * Supports values like "100px", "50%", "2em", etc. + * Returns 0 if no valid number is found. + * + * @param {string | number} value - The value to parse, can be a number or a string with units. + * @returns {number} - The numeric part of the value. + * @private + */ +export function parseUnit(value: string | number): number { + if (typeof value === 'number') { + return value; + } + + // Use parseFloat directly, which safely extracts leading numeric value + const parsed: number = parseFloat(value); + return isNaN(parsed) ? 0 : parsed; +} + +/** + * @param {HTMLTableElement} contentTableRef - Defines the contentTableRef + * @param {EditSettings} editSettings - Defines the editSettings + * @returns {void} + * @private + */ +export function addLastRowBorder(contentTableRef?: HTMLTableElement, editSettings?: EditSettings): void { + const table: Element = contentTableRef; + removeClass(table?.querySelectorAll?.('td'), 'sf-lastrowcell'); + if (table?.querySelector?.('tr:nth-last-child(2)')) { + if (editSettings?.showAddNewRow && editSettings?.newRowPosition === 'Bottom') { + addClass(table.querySelector('tr:nth-last-child(2)').querySelectorAll('td'), 'sf-lastrowcell'); + } + } + addClass(table?.querySelectorAll?.('tr:last-child td'), 'sf-lastrowcell'); +} diff --git a/components/grids/src/grid/views/Aggregate.tsx b/components/grids/src/grid/views/Aggregate.tsx new file mode 100644 index 0000000..95ba091 --- /dev/null +++ b/components/grids/src/grid/views/Aggregate.tsx @@ -0,0 +1,44 @@ +import { JSX, ReactNode } from 'react'; +import { AggregateRowProps, AggregateColumnProps } from '../types/aggregate.interfaces'; + +/** + * Aggregates component for declarative usage in user code + * + * @returns {JSX.Element} Rendered component + */ +export const Aggregates: React.FC<{ children?: ReactNode }> = (): JSX.Element => { + return null; +}; + +/** + * AggregateRow component for declarative usage in user code + * + * @component + * @example + * ```tsx + * + * ``` + * @param {Partial} _props - Aggregate row configuration properties + * @returns {JSX.Element} Aggregate row component with the provided properties + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const AggregateRow: (props: Partial) => JSX.Element = (_props: Partial): JSX.Element => { + return null; +}; + +/** + * AggregateColumn component for declarative usage in user code + * + * @component + * @example + * ```tsx + * + * ``` + * @param {Partial} _props - Aggregate column configuration properties + * @returns {JSX.Element} Aggregate column component with the provided properties + */ +export const AggregateColumn: (props: Partial) => JSX.Element = + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (_props: Partial): JSX.Element => { + return null; + }; diff --git a/components/grids/src/grid/views/ContentPanel.tsx b/components/grids/src/grid/views/ContentPanel.tsx new file mode 100644 index 0000000..47eeeeb --- /dev/null +++ b/components/grids/src/grid/views/ContentPanel.tsx @@ -0,0 +1,122 @@ +import { + forwardRef, + ForwardRefExoticComponent, + RefAttributes, + useImperativeHandle, + useRef, + useMemo, + memo, + CSSProperties, + RefObject, + JSX +} from 'react'; +import { ContentTableBase } from './index'; +import { + ContentPanelRef, + IContentPanelBase, + ContentTableRef +} from '../types'; +import { + useGridComputedProvider +} from '../contexts'; + +// CSS class constants following enterprise naming convention +const CSS_CONTENT_TABLE: string = 'sf-table'; + +/** + * Default styles for content table to ensure consistent rendering + * + * @type {CSSProperties} + */ +const DEFAULT_TABLE_STYLE: CSSProperties = { + borderCollapse: 'separate', + borderSpacing: '0.25px' +}; + +/** + * ContentPanelBase component renders the scrollable grid content area + * + * @component + * @private + * @param {Partial} props - Component properties + * @param {object} props.panelAttributes - Attributes to apply to the content panel container + * @param {object} props.scrollContentAttributes - Attributes to apply to the scrollable content container + * @param {RefObject} ref - Forwarded ref to expose internal elements and methods + * @returns {JSX.Element} The rendered grid content wrapper + */ +const ContentPanelBase: ForwardRefExoticComponent & RefAttributes> = + memo(forwardRef>( + (props: Partial, ref: RefObject) => { + const { panelAttributes, scrollContentAttributes } = props; + const { id } = useGridComputedProvider(); + + // Refs for DOM elements and child components + const contentPanelRef: RefObject = useRef(null); + const contentScrollRef: RefObject = useRef(null); + const contentTableRef: RefObject = useRef(null); + + /** + * Expose internal elements and methods through the forwarded ref + * Only define properties specific to ContentPanel and forward ContentTable properties + */ + useImperativeHandle(ref, () => ({ + // ContentPanel specific properties + contentPanelRef: contentPanelRef.current, + contentScrollRef: contentScrollRef.current, + + // Forward all properties from ContentTable + ...(contentTableRef.current as ContentTableRef) + }), [contentPanelRef.current, contentScrollRef.current, contentTableRef.current]); + + /** + * Memoized content table component to prevent unnecessary re-renders + */ + const contentTable: JSX.Element = useMemo(() => ( + + ), [id]); + + return ( +
+
+ {contentTable} +
+
+ ); + } + ), (prevProps: Partial, nextProps: Partial) => { + // Custom comparison function for memo to prevent unnecessary re-renders + // Only re-render if styles have changed + const prevStyle: CSSProperties = prevProps.scrollContentAttributes?.style; + const nextStyle: CSSProperties = nextProps.scrollContentAttributes?.style; + const isBusyEqual: boolean = prevProps.scrollContentAttributes?.['aria-busy'] === nextProps.scrollContentAttributes?.['aria-busy']; + prevProps.panelAttributes.className = nextProps.panelAttributes.className; + + // Deep comparison of style objects + const stylesEqual: boolean = JSON.stringify(prevStyle) === JSON.stringify(nextStyle); + + return stylesEqual && isBusyEqual; + }); + +/** + * Set display name for debugging purposes + */ +ContentPanelBase.displayName = 'ContentPanelBase'; + +/** + * Export the ContentPanelBase component for use in other components + * + * @private + */ +export { ContentPanelBase }; diff --git a/components/grids/src/grid/views/ContentRows.tsx b/components/grids/src/grid/views/ContentRows.tsx new file mode 100644 index 0000000..1f3c492 --- /dev/null +++ b/components/grids/src/grid/views/ContentRows.tsx @@ -0,0 +1,387 @@ +import { + forwardRef, + useImperativeHandle, + useRef, + RefAttributes, + ForwardRefExoticComponent, + ReactElement, + useMemo, + useCallback, + memo, + isValidElement, + Children, + JSX, + MemoExoticComponent, + ReactNode, + RefObject, + useEffect, + SetStateAction, + Dispatch +} from 'react'; +import { + ContentRowsRef, + ICell, + IContentRowsBase, + IRow, + RowRef, + CellType, RenderType +} from '../types'; +import { ColumnProps, IColumnBase } from '../types/column.interfaces'; +import { InlineEditFormRef } from '../types/edit.interfaces'; +import { useGridComputedProvider, useGridMutableProvider } from '../contexts'; +import { ColumnBase, RowBase } from '../components'; +import { IL10n, isNullOrUndefined } from '@syncfusion/react-base'; +import { getUid } from '../utils'; +import { InlineEditForm } from './index'; +import { ColumnsChildren, ValueType } from '../types/interfaces'; +const CSS_EMPTY_ROW: string = 'sf-emptyrow'; +const CSS_DATA_ROW: string = 'sf-row'; +const CSS_ALT_ROW: string = 'sf-altrow'; + +/** + * RenderEmptyRow component displays when no data is available + * + * @component + * @private + * @param {Partial} props - Component properties + * @returns {JSX.Element} The rendered empty row component + */ +const RenderEmptyRow: MemoExoticComponent<() => JSX.Element> = + memo(() => { + const { serviceLocator, emptyRecordTemplate } = useGridComputedProvider(); + const localization: IL10n = serviceLocator?.getService('localization'); + const { columnsDirective } = useGridMutableProvider(); + + /** + * Calculate the number of columns to span the empty message + */ + const columnsLength: number = useMemo(() => { + const children: ReactNode = (columnsDirective.props as ColumnsChildren).children; + return Children.count(children); + }, [columnsDirective]); + + const rowRef: RefObject = useRef(null); + + /** + * Render the empty row template based on configuration + */ + const renderEmptyTemplate: string | (() => ReactElement | string) | ReactElement = useMemo(() => { + if (isNullOrUndefined(emptyRecordTemplate)) { + return localization?.getConstant('noRecordsMessage'); + } else if (typeof emptyRecordTemplate === 'string' || isValidElement(emptyRecordTemplate)) { + return emptyRecordTemplate; + } else { + return emptyRecordTemplate(); + } + }, [emptyRecordTemplate, localization]); + + return ( + <> + {useMemo(() => ( + + + + ), [columnsLength, renderEmptyTemplate])} + + ); + }); + +/** + * Set display name for debugging purposes + */ +RenderEmptyRow.displayName = 'RenderEmptyRow'; + +/** + * ContentRowsBase component renders the data rows within the table body section + * + * @component + * @private + * @param {Partial} props - Component properties + * @param {RefObject} ref - Forwarded ref to expose internal elements and methods + * @returns {JSX.Element} The rendered tbody element with data rows + */ +const ContentRowsBase: ForwardRefExoticComponent & RefAttributes> = + memo(forwardRef>( + (_props: Partial, ref: RefObject) => { + const { columnsDirective, currentViewData, editModule, uiColumns } = useGridMutableProvider(); + const { rowHeight, enableAltRow, columns, rowTemplate } = useGridComputedProvider(); + + // Refs for DOM elements and child components + const contentSectionRef: RefObject = useRef(null); + const rowsObjectRef: RefObject[]> = useRef[]>([]); + const rowElementRefs: RefObject> = + useRef>([]); + const addInlineFormRef: RefObject = useRef(null); + const editInlineFormRef: RefObject = useRef(null); + + /** + * Returns the collection of content row elements + * + * @returns {HTMLCollectionOf | undefined} Collection of row elements + */ + const getRows: () => HTMLCollectionOf | undefined = useCallback(() => { + return rowElementRefs.current as HTMLCollectionOf; + }, [contentSectionRef.current?.children]); + + /** + * Returns the row options objects with DOM element references + * + * @returns {IRow[]} Array of row options objects with element references + */ + const getRowsObject: () => IRow[] = useCallback(() => rowsObjectRef.current, [rowsObjectRef.current]); + + /** + * Gets a row by index. + * + * @param {number} index - Specifies the row index. + * @returns {HTMLTableRowElement} returns the element + */ + const getRowByIndex: (index: number) => HTMLTableRowElement = useCallback((index: number) => { + return !isNullOrUndefined(index) ? getRows()[parseInt(index.toString(), 10)] : undefined; + }, []); + + /** + * @param {string} uid - Defines the uid + * @returns {IRow} Returns the row object + * @private + */ + const getRowObjectFromUID: (uid: string) => IRow = useCallback((uid: string) => { + const rows: IRow[] = getRowsObject() as IRow[]; + if (rows) { + for (const row of rows) { + if (row.uid === uid) { + return row; + } + } + } + return null; + }, []); + + /** + * Expose internal elements and methods through the forwarded ref + * Only define properties specific to ContentRows + */ + useImperativeHandle(ref, () => ({ + contentSectionRef: contentSectionRef.current, + getRows, + getRowsObject, + getRowByIndex, + getRowObjectFromUID, + getCurrentViewRecords: () => currentViewData, + addInlineRowFormRef: addInlineFormRef, + editInlineRowFormRef: editInlineFormRef + }), [getRows, getRowsObject, getRowByIndex, getRowObjectFromUID, + currentViewData, addInlineFormRef.current, editInlineFormRef.current]); + + /** + * Memoized empty row component to display when no data is available + */ + const emptyRowComponent: JSX.Element | null = useMemo(() => { + if (!columnsDirective || !currentViewData || currentViewData.length === 0) { + return ; + } + return null; + }, [columnsDirective, currentViewData]); + + /** + * Callback to store row element references directly in the row object + * + * @param {number} index - Row index + * @param {HTMLTableRowElement} element - Row DOM element + */ + const storeRowRef: (index: number, element: HTMLTableRowElement, cellRef: ICell[], + setRowObject: Dispatch>>) => void = + useCallback((index: number, element: HTMLTableRowElement, cellRef: ICell[], + setRowObject: Dispatch>>) => { + // Directly update the element reference in the row object + rowsObjectRef.current[index as number].element = element; + rowElementRefs.current[index as number] = element; + rowsObjectRef.current[index as number].cells = cellRef; + rowsObjectRef.current[index as number].setRowObject = setRowObject; + }, []); + + + const inlineAddForm: JSX.Element = useMemo(() => { + const options: IRow = { + uid: getUid('grid-add-row'), + data: editModule?.originalData, + index: editModule?.editSettings?.newRowPosition === 'Top' ? 0 : currentViewData?.length, // Critical: Use original data index for proper tracking + isDataRow: false + }; + // Enhanced check for showAddNewRow functionality + // This ensures add form is properly visible in all scenarios + const showAddForm: boolean = editModule?.editSettings?.allowAdd && + (editModule?.editSettings?.showAddNewRow || + (!options.data && editModule?.isEdit)); + + return showAddForm ? ( + { + if (editModule?.updateEditData) { + editModule?.updateEditData?.(field, value); + } + }} + onSave={() => editModule?.saveChanges()} + onCancel={editModule?.cancelChanges} + template={editModule?.editSettings?.template} + /> + ) : <>; + }, [ + columnsDirective, currentViewData?.length, rowHeight, + editModule?.isEdit, + editModule?.editRowIndex, + editModule?.isShowAddNewRowActive, + editModule?.isShowAddNewRowDisabled, + editModule?.showAddNewRowData, + editModule?.editSettings?.newRowPosition, + editModule?.editSettings?.template + ]); + + const generateCell: () => IRow[] = useCallback((): IRow[] => { + const cells: ICell[] = []; + const childrenArray: ReactElement[] = columns as ReactElement[]; + for (let index: number = 0; index < childrenArray.length; index++) { + const child: ColumnProps = childrenArray[index as number] as ColumnProps; + const option: ICell = { + visible: child.visible !== false, + isDataCell: !isNullOrUndefined(child.field), + isTemplate: !isNullOrUndefined(child.template), + rowID: child.uid, + column: child, + cellType: CellType.Data, + colSpan: 1 + }; + cells.push(option); + } + return cells; + }, [columns]); + + const processRowData: (rowIndex: number, rowData: Object, rows: JSX.Element[], rowOptions: IRow[], + indent?: number, currentDataRowIndex?: number, + parentUid?: string) => void = (rowIndex: number, rowData: Object, rows: JSX.Element[], rowOptions: IRow[], + indent?: number, currentDataRowIndex?: number, parentUid?: string) => { + + const row: Object = rowData; + const options: IRow = {}; + options.uid = getUid('grid-row'); + options.parentUid = parentUid; + options.data = row; + options.index = currentDataRowIndex ? currentDataRowIndex : rowIndex; + options.isDataRow = true; + options.isCaptionRow = false; + options.indent = indent; + options.isAltRow = enableAltRow ? rowIndex % 2 !== 0 : false; + + if (rowTemplate) { + options.cells = generateCell(); + } + + // Store the options object for getRowsObject + rowOptions.push({ ...options }); + + // Create the row element with a callback ref to store the element reference + rows.push( + { + if (element?.rowRef?.current) { + storeRowRef(rowIndex, element.rowRef.current, element.getCells(), element.setRowObject); + } else if (element?.editInlineRowFormRef?.current) { + rowsObjectRef.current[rowIndex as number].editInlineRowFormRef = element?.editInlineRowFormRef; + editInlineFormRef.current = rowsObjectRef.current[rowIndex as number].editInlineRowFormRef.current; // final single row ref for edit form. + } + }} + key={options.uid} + row={{ ...options }} + rowType={RenderType.Content} + className={CSS_DATA_ROW + (options.isAltRow ? (' ' + CSS_ALT_ROW) : '')} + role="row" + aria-rowindex={rowIndex + 1} + data-uid={options.uid} + style={{ height: `${rowHeight}px` }} + > + {(columnsDirective.props as ColumnsChildren).children} + + ); + }; + + /** + * Memoized data rows to prevent unnecessary re-renders + */ + const dataRows: JSX.Element[] = useMemo(() => { + if (!columnsDirective || !currentViewData || currentViewData.length === 0) { + rowsObjectRef.current = []; + return []; + } + rowElementRefs.current = []; + + const rows: JSX.Element[] = []; + const rowOptions: IRow[] = []; + for (let rowIndex: number = 0; rowIndex < currentViewData.length; rowIndex++) { + processRowData(rowIndex, currentViewData[parseInt(rowIndex.toString(), 10)], rows, rowOptions); + } + + // Store the row options in the ref for access via getRowsObject + rowsObjectRef.current = rowOptions; + return rows; + }, [columnsDirective, currentViewData, storeRowRef, rowHeight, enableAltRow]); + + useEffect(() => { + return () => { + rowsObjectRef.current = []; + rowElementRefs.current = []; + }; + }, []); + + return ( + + {editModule?.editSettings?.allowAdd && editModule?.editSettings?.newRowPosition === 'Top' && inlineAddForm} + {dataRows.length > 0 ? dataRows : emptyRowComponent} + {editModule?.editSettings?.allowAdd && editModule?.editSettings?.newRowPosition === 'Bottom' && inlineAddForm} + + ); + } + )); + +/** + * Set display name for debugging purposes + */ +ContentRowsBase.displayName = 'ContentRowsBase'; + +/** + * Export the ContentRowsBase component for use in other components + * + * @private + */ +export { ContentRowsBase }; diff --git a/components/grids/src/grid/views/ContentTable.tsx b/components/grids/src/grid/views/ContentTable.tsx new file mode 100644 index 0000000..ad32b56 --- /dev/null +++ b/components/grids/src/grid/views/ContentTable.tsx @@ -0,0 +1,106 @@ +import { + forwardRef, + ForwardRefExoticComponent, + RefAttributes, + useImperativeHandle, + useRef, + useMemo, + memo, + JSX, + RefObject +} from 'react'; +import { ContentRowsBase } from './index'; +import { + ContentTableRef, + IContentTableBase, + ContentRowsRef, MutableGridSetter +} from '../types/interfaces'; +import { IGrid } from '../types/grid.interfaces'; +import { + useGridComputedProvider, + useGridMutableProvider +} from '../contexts'; + +/** + * ContentTableBase component renders the table structure for grid content + * + * @component + * @private + * @param {Partial} props - Component properties + * @param {string} [props.className] - Additional CSS class names + * @param {string} [props.role] - ARIA role attribute + * @param {string} [props.id] - ID attribute for the table + * @param {Object} [props.style] - Inline styles for the table + * @param {RefObject} ref - Forwarded ref to expose internal elements and methods + * @returns {JSX.Element} The rendered content table component + */ +const ContentTableBase: ForwardRefExoticComponent & RefAttributes> = + memo(forwardRef>( + (props: Partial, ref: RefObject) => { + // Access grid context providers + const { colElements: ColElements } = useGridMutableProvider(); + const grid: Partial & Partial = useGridComputedProvider(); + const { id } = grid; + + // Refs for DOM elements and child components + const contentTableRef: RefObject = useRef(null); + const rowSectionRef: RefObject = useRef(null); + + /** + * Memoized colgroup element to prevent unnecessary re-renders + * Contains column definitions for the table + */ + const colGroupContent: JSX.Element = useMemo(() => ( + + {ColElements.length ? ColElements : null} + + ), [ColElements, id]); + + /** + * Expose internal elements and methods through the forwarded ref + * Only define properties specific to ContentTable and forward ContentRows properties + */ + useImperativeHandle(ref, () => ({ + // ContentTable specific properties + contentTableRef: contentTableRef.current, + getContentTable: () => contentTableRef.current, + // Forward all properties from ContentRows + ...(rowSectionRef.current) + }), [contentTableRef.current, rowSectionRef.current]); + + /** + * Memoized content rows component to prevent unnecessary re-renders + */ + const contentRows: JSX.Element = useMemo(() => ( + + ), []); + + return ( + + {colGroupContent} + {contentRows} +
+ ); + } + )); + +/** + * Set display name for debugging purposes + */ +ContentTableBase.displayName = 'ContentTableBase'; + +/** + * Export the ContentTableBase component for use in other components + * + * @private + */ +export { ContentTableBase }; diff --git a/components/grids/src/grid/views/FilterBar.tsx b/components/grids/src/grid/views/FilterBar.tsx new file mode 100644 index 0000000..db95450 --- /dev/null +++ b/components/grids/src/grid/views/FilterBar.tsx @@ -0,0 +1,208 @@ +import { + useRef, + useMemo, + memo, + JSX, + RefObject, + MemoExoticComponent, + useEffect, + useState +} from 'react'; +import { + FilterBarType, + IValueFormatter +} from '../types'; +import { MutableGridSetter } from '../types/interfaces'; +import { GridRef } from '../types/grid.interfaces'; +import { FilterPredicates } from '../types/filter.interfaces'; +import { ColumnProps, IColumnBase, ColumnRef } from '../types/column.interfaces'; +import { useColumn } from '../hooks'; +import { NumericChangeEvent, NumericTextBox, NumericTextBoxProps, TextBox, TextBoxChangeEvent, TextBoxProps } from '@syncfusion/react-inputs'; +import { getNumberPattern, isNullOrUndefined } from '@syncfusion/react-base'; +import { + useGridComputedProvider, + useGridMutableProvider +} from '../contexts'; +import { DatePicker, ChangeEvent, DatePickerProps } from '@syncfusion/react-calendars'; +import { getCustomDateFormat } from '../utils'; + +const CSS_FILTER_DIV_INPUT: string = 'sf-filterdiv sf-fltrinputdiv'; + +/** + * FilterBase component renders a table cell (th or td) with appropriate content + * + * @component + * @private + * @param {IColumnBase} props - Component properties + * @param {RefObject} ref - Forwarded ref to expose internal elements + * @returns {JSX.Element} The rendered table cell (th or td) + */ +const FilterBase: MemoExoticComponent<(props: Partial) => JSX.Element> = memo((props: Partial) => { + + // Get column-specific APIs and properties + const { publicAPI, privateAPI } = useColumn(props); + const { cssClass, filterModule } = useGridMutableProvider(); + const CSS_FILTER_INPUT_TEXT: string = 'sf-filtertext' + (cssClass !== '' ? (' ' + cssClass) : ''); + + const { + cellType, + visibleClass, + formattedValue + } = privateAPI; + + const { ...column } = publicAPI; + + const { + index, + field, + customAttributes + } = column; + + const { className, role, title } = customAttributes; + const grid: Partial & Partial = useGridComputedProvider(); + const { serviceLocator } = grid; + const formatter: IValueFormatter = serviceLocator?.getService('valueFormatter'); + const filterBarClass: string = column.filterTemplate ? 'sf-fltrtemp' : ''; + const fltrData: { column: ColumnProps } = { column: column }; + fltrData[column.field] = undefined; + const filterBarType: string | FilterBarType = column.filter.filterBarType; + + // Create ref for the cell element + const cellRef: RefObject = useRef({ + cellRef: useRef(null) + }); + const [inputValue, setInputValue] = useState(null); + + const filterColumn: FilterPredicates = grid.filterSettings?.columns?.find((col: FilterPredicates) => col.field === column.field); + const updateColumn: ColumnProps = grid.getColumns().find((col: ColumnProps) => col.field === column.field); + const filterVal: string | number | boolean | Date | (string | number | boolean | Date)[] = filterColumn?.value; + + useEffect(() => { + let value: string | number | boolean | Date | (string | number | boolean | Date)[] = !isNullOrUndefined(filterVal) ? + filterVal : ''; + if (!isNullOrUndefined(column.format) && !isNullOrUndefined(filterVal) && typeof filterVal !== 'string' + && (updateColumn as IColumnBase).formatFn && filterBarType !== 'datePickerFilter' && filterBarType !== 'numericFilter') { + value = formatter.toView(filterVal as number | Date, (updateColumn as IColumnBase).formatFn).toString(); + } + if (filterBarType === 'datePickerFilter' || filterBarType === 'numericFilter') { + setInputValue(filterVal ? filterVal : null); + } else if (!(updateColumn?.type === 'number' && isNaN(value as number))) { + setInputValue(value.toString()); + } + }, [filterVal]); + + const handleChange: (e: TextBoxChangeEvent | NumericChangeEvent | ChangeEvent) => void = ( + e: TextBoxChangeEvent |NumericChangeEvent | ChangeEvent) => { + setInputValue((e as NumericChangeEvent | ChangeEvent | TextBoxChangeEvent).value); + if (filterBarType === 'datePickerFilter' && grid.filterSettings?.mode === 'Immediate' && e.value instanceof Date) { + filterModule?.filterByColumn(column.field, 'equal', e.value); + } else if (filterBarType === 'datePickerFilter' && grid.filterSettings?.mode === 'Immediate' && e.value === null && + document.activeElement.tagName === 'INPUT' && (document.activeElement as HTMLInputElement).value.length > 1) { + filterModule?.removeFilteredColsByField?.(column.field, true); + } + }; + + + /** + * Render editor component with fallback to HTML input + * This structure allows easy replacement when Syncfusion components become available + * Added disabled state support for non-editable columns + * Primary key fields should be enabled during add operations + * + * @returns {JSX.Element} The rendered editor component as JSX element + */ + const renderFilter: () => JSX.Element = (): JSX.Element => { + const id: string = column.field + '_filterBarcell'; + const isDisabled: boolean = !column.allowFilter; + + switch (filterBarType) { + case 'numericFilter': + return ( + + ); + + case 'datePickerFilter': + return ( + + ); + + + default: + return ( + + ); + } + }; + + /** + * Memoized filter cell content + */ + const filterCellContent: JSX.Element | null = useMemo(() => { + const combinedClassName: string = [ + ...new Set((`${props.cell.className} ${className} ${filterBarClass} ${visibleClass || ''}`.trim()).split(' ')) + ].join(' '); + return ( + + { column.filterTemplate ? (typeof column.filterTemplate === 'function' ? column.filterTemplate(fltrData) : column.filterTemplate) + :
+ {renderFilter()} +
} + + ); + }, [cellType, index, className, visibleClass, formattedValue, field, inputValue, cssClass]); + + // Return the appropriate cell content based on cell type + return filterCellContent; +}); + +/** + * Set display name for debugging purposes + */ +FilterBase.displayName = 'FilterBase'; + +/** + * Export the FilterBase component for internal use + * + * @private + */ +export { FilterBase }; diff --git a/components/grids/src/grid/views/FooterPanel.tsx b/components/grids/src/grid/views/FooterPanel.tsx new file mode 100644 index 0000000..b8239a0 --- /dev/null +++ b/components/grids/src/grid/views/FooterPanel.tsx @@ -0,0 +1,112 @@ +import { forwardRef, ForwardRefExoticComponent, RefAttributes, useImperativeHandle, useRef, useMemo, memo, CSSProperties, RefObject, JSX } from 'react'; +import { FooterTableBase } from './FooterTable'; +import { useGridComputedProvider } from '../contexts'; +import { + FooterPanelRef, FooterTableRef, IFooterPanelBase +} from '../types'; + +// Constant CSS class +const CSS_FOOTER_TABLE: string = 'sf-table'; + +/** + * Default styles for footer table to ensure consistent rendering + * + * @type {CSSProperties} + */ +const DEFAULT_TABLE_STYLE: CSSProperties = { + borderCollapse: 'separate', + borderSpacing: '0.25px' +}; + +/** + * FooterPanelBase component renders the static area for the grid footer. + * This component wraps the FooterTableBase in a scrollable container and + * is responsible for organizing the footer rows and synchronizing scrolling behavior. + * + * @component + * @private + * @param {Partial} props - Component properties + * @param {object} props.panelAttributes - Attributes to apply to the footer panel container + * @param {object} props.scrollContentAttributes - Attributes to apply to the scrollable content container + * @param {RefObject} ref - Forwarded ref to expose internal elements + * @returns {JSX.Element} The rendered footer container with scrollable table + */ +const FooterPanelBase: ForwardRefExoticComponent & RefAttributes> = + memo(forwardRef>( + (props: Partial, ref: RefObject) => { + const { panelAttributes, scrollContentAttributes, tableScrollerPadding } = props; + const { id } = useGridComputedProvider(); + + // Refs for DOM elements and child components + const footerPanelRef: RefObject = useRef(null); + const footerScrollRef: RefObject = useRef(null); + const footerTableRef: RefObject = useRef(null); + + /** + * Expose internal elements and methods through the forwarded ref + * Only define properties specific to FooterPanel and forward FooterTable properties + */ + useImperativeHandle(ref, () => ({ + // FooterPanel specific properties + footerPanelRef: footerPanelRef.current, + footerScrollRef: footerScrollRef.current, + + // Forward all properties from FooterTable + ...(footerTableRef.current) + }), [footerPanelRef.current, footerScrollRef.current, footerTableRef.current]); + + /** + * Memoized footer table component to prevent unnecessary re-renders + */ + const footerTable: JSX.Element = useMemo(() => ( + + ), [tableScrollerPadding]); + + return ( +
+
+ {footerTable} +
+
+ ); + } + ), (prevProps: Partial, nextProps: Partial) => { + // Custom comparison function for memo to prevent unnecessary re-renders + // Only re-render if styles have changed + const prevStyle: CSSProperties = prevProps.panelAttributes?.style; + const nextStyle: CSSProperties = nextProps.panelAttributes?.style; + const prevScrollStyle: CSSProperties = prevProps.scrollContentAttributes?.style; + const nextScrollStyle: CSSProperties = nextProps.scrollContentAttributes?.style; + + // Deep comparison of style objects + const stylesEqual: boolean = + JSON.stringify(prevStyle) === JSON.stringify(nextStyle) && + JSON.stringify(prevScrollStyle) === JSON.stringify(nextScrollStyle); + + return stylesEqual; + }); + +/** + * Set display name for debugging purposes + */ +FooterPanelBase.displayName = 'FooterPanelBase'; + +/** + * Export the FooterPanelBase component for direct usage if needed + * + * @private + */ +export { FooterPanelBase }; diff --git a/components/grids/src/grid/views/FooterRows.tsx b/components/grids/src/grid/views/FooterRows.tsx new file mode 100644 index 0000000..e750da4 --- /dev/null +++ b/components/grids/src/grid/views/FooterRows.tsx @@ -0,0 +1,239 @@ +import { + forwardRef, + ForwardRefExoticComponent, + RefAttributes, + useImperativeHandle, + useRef, + useMemo, + useCallback, + memo, + RefObject, + JSX, + useEffect +} from 'react'; +import { + FooterRowsRef, + IFooterRowsBase, + RowRef, + IRow, + ICell, + IValueFormatter, + AggregateType, RenderType +} from '../types'; +import { Group, SummaryData, ColumnsChildren } from '../types/interfaces'; +import { ColumnProps } from '../types/column.interfaces'; +import { AggregateRowProps, AggregateColumnProps, CustomSummaryType } from '../types/aggregate.interfaces'; +import { + useGridMutableProvider, + useGridComputedProvider +} from '../contexts'; +import { RowBase } from '../components'; +import { getUid } from '../utils'; +import { DateFormatOptions, extend, isNullOrUndefined, NumberFormatOptions } from '@syncfusion/react-base'; +import { DataUtil } from '@syncfusion/react-data'; + +// CSS class constants following enterprise naming convention +const CSS_SUMMARY_ROW: string = 'sf-summaryrow'; + +/** + * FooterRowsBase component renders the footer rows within the table footer section + * + * @component + * @private + * @param {Partial} props - Component properties + * @param {RefObject} ref - Forwarded ref to expose internal elements and methods + * @returns {JSX.Element} The rendered tfoot element with footer rows + */ +const FooterRowsBase: ForwardRefExoticComponent & RefAttributes> = + memo(forwardRef>( + (props: Partial, ref: RefObject) => { + const { tableScrollerPadding, ...rest } = props; + + const { columnsDirective, responseData } = useGridMutableProvider(); + const { aggregates, rowHeight, serviceLocator } = useGridComputedProvider(); + + // Refs for DOM elements and child components + const footerSectionRef: RefObject = useRef(null); + const rowsObjectRef: RefObject[]> = useRef[]>([]); + + /** + * Returns the collection of footer row elements + * + * @returns {HTMLCollectionOf | undefined} Collection of footer row elements + */ + const getFooterRows: () => HTMLCollectionOf | undefined = useCallback(() => { + return footerSectionRef.current?.children as HTMLCollectionOf; + }, [footerSectionRef.current?.children]); + + /** + * Returns the row options objects with DOM element references + * + * @returns {IRow[]} Array of row options objects with element references + */ + const getFooterRowsObject: () => IRow[] = useCallback(() => rowsObjectRef.current, [rowsObjectRef.current]); + + /** + * Expose internal elements and methods through the forwarded ref + */ + useImperativeHandle(ref, () => ({ + footerSectionRef: footerSectionRef.current, + getFooterRows, + getFooterRowsObject + }), [getFooterRows, getFooterRowsObject]); + + /** + * Callback to store row element references directly in the row object + * + * @param {number} index - Row index + * @param {HTMLTableRowElement} element - Row DOM element + */ + const storeRowRef: (index: number, element: HTMLTableRowElement, cellRef: ICell[]) => void = + useCallback((index: number, element: HTMLTableRowElement, cellRef: ICell[]) => { + // Directly update the element reference in the row object + rowsObjectRef.current[index as number].element = element; + rowsObjectRef.current[index as number].cells = cellRef; + }, []); + + const getData: () => AggregateRowProps[] = (): AggregateRowProps[] => { + const rows: AggregateRowProps[] = []; + const row: AggregateRowProps[] = aggregates.slice(); + for (let i: number = 0; i < row.length; i++) { + const columns: AggregateColumnProps[] = row[parseInt(i.toString(), 10)].columns; + if (columns && columns.length) { + rows.push({ columns: columns }); + } + } + return rows; + }; + + const getFormatter: (column: AggregateColumnProps) => Function = (column: AggregateColumnProps): Function => { + const valueFormatter: IValueFormatter = serviceLocator?.getService('valueFormatter'); + if (typeof (column.format) === 'object') { + return valueFormatter.getFormatFunction(extend({}, column.format as DateFormatOptions)); + } else if (typeof (column.format) === 'string') { + return valueFormatter.getFormatFunction({ format: column.format } as NumberFormatOptions); + } + return (a: Object) => a; + }; + + const calculateAggregate: (type: AggregateType | string, data: Object, column?: AggregateColumnProps) => Object = + (type: AggregateType | string, data: Object, column?: AggregateColumnProps): Object => { + if (type === 'Custom') { + const temp: CustomSummaryType = column.customAggregate as CustomSummaryType; + if (typeof temp === 'string') { + return temp; + } + return temp ? temp(data, column) : ''; + } + return 'result' in data ? DataUtil.aggregates[type.toLowerCase()](data.result, column.field) : null; + }; + + const setTemplate: (column: AggregateColumnProps, data: Object, single: Object) => Object = + (column: AggregateColumnProps, data: Object, single: Object): Object => { + let types: AggregateType[] = column.type as AggregateType[]; + const formatFn: Function = getFormatter(column); + const group: Group = data; + if (!(types instanceof Array)) { + types = [column.type as AggregateType]; + } + for (let i: number = 0; i < types.length; i++) { + const key: string = column.field + ' - ' + types[parseInt(i.toString(), 10)].toLowerCase(); + const disp: string = column.columnName; + const val: Object = types[parseInt(i.toString(), 10)] !== 'Custom' && group.aggregates + && key in group.aggregates ? group.aggregates[`${key}`] : + calculateAggregate(types[parseInt(i.toString(), 10)], group, column); + single[`${disp}`] = single[`${disp}`] || {}; + single[`${disp}`][`${key}`] = val; + single[`${disp}`][types[parseInt(i.toString(), 10)]] = !isNullOrUndefined(val) ? formatFn(val) : ''; + } + return single; + }; + + const buildSummaryData: (args: SummaryData) => Object[] = (args: SummaryData): Object[] => { + const dummy: Object[] = []; + const summaryRows: AggregateRowProps[] = getData(); + for (let i: number = 0; i < summaryRows.length; i++) { + let single: Object = {}; + const column: AggregateColumnProps[] = summaryRows[parseInt(i.toString(), 10)].columns; + for (let j: number = 0; j < column.length; j++) { + single = setTemplate(column[parseInt(j.toString(), 10)], args, single); + } + dummy.push(single); + } + return dummy; + }; + + /** + * Memoized footer row content to prevent unnecessary re-renders + */ + const footerRowContent: JSX.Element[] | null = useMemo(() => { + const rows: JSX.Element[] = []; + const rowOptions: IRow[] = []; + const summaries: AggregateRowProps[] = getData(); + const data: Object[] = buildSummaryData(responseData); + // Generate footer rows based on aggregates + for (let rowIndex: number = 0; rowIndex < summaries.length; rowIndex++) { + const options: IRow = {}; + options.uid = getUid('grid-row'); + options.data = data[parseInt(rowIndex.toString(), 10)]; + options.index = rowIndex; + options.isAggregateRow = true; + + const rowId: string = `grid-summary-row-${rowIndex}-${Math.random().toString(36).substr(2, 5)}`; + // Store the options object for getRowsObject + rowOptions.push({ ...options }); + rows.push( + { + if (element?.rowRef?.current) { + storeRowRef(rowIndex, element.rowRef.current, element.getCells()); + } + }} + role='row' + row={options} + key={rowId} + rowType={RenderType.Summary} + className={`${CSS_SUMMARY_ROW}`.trim()} + data-uid={options.uid} + style={{ height: `${rowHeight}px` }} + tableScrollerPadding={tableScrollerPadding} + aggregateRow={summaries[parseInt(rowIndex.toString(), 10)]} + > + {(columnsDirective.props as ColumnsChildren).children} + + ); + } + + // Store the row options in the ref for access via getRowsObject + rowsObjectRef.current = rowOptions; + return rows; + }, [columnsDirective, rowHeight, responseData, tableScrollerPadding]); + + useEffect(() => { + return () => { + rowsObjectRef.current = []; + }; + }, []); + + return ( + + {footerRowContent} + + ); + } + )); + +/** + * Set display name for debugging purposes + */ +FooterRowsBase.displayName = 'FooterRowsBase'; + +/** + * Export the FooterRowsBase component for use in other components + * + * @private + */ +export { FooterRowsBase }; diff --git a/components/grids/src/grid/views/FooterTable.tsx b/components/grids/src/grid/views/FooterTable.tsx new file mode 100644 index 0000000..fb1b4b2 --- /dev/null +++ b/components/grids/src/grid/views/FooterTable.tsx @@ -0,0 +1,102 @@ +import { + forwardRef, + ForwardRefExoticComponent, + RefAttributes, + useImperativeHandle, + useRef, + useMemo, + memo, + JSX, + RefObject +} from 'react'; +import { FooterRowsBase } from './FooterRows'; +import { + useGridComputedProvider, + useGridMutableProvider +} from '../contexts'; +import { + FooterRowsRef, + FooterTableRef, + IFooterTableBase +} from '../types'; + +/** + * FooterTableBase component renders the table structure for grid footer + * + * @component + * @private + * @param {Partial} props - Component properties + * @param {string} [props.className] - Additional CSS class names + * @param {string} [props.role] - ARIA role attribute + * @param {Object} [props.style] - Inline styles for the table + * @param {RefObject} ref - Forwarded ref to expose internal elements and methods + * @returns {JSX.Element} The rendered footer table component + */ +const FooterTableBase: ForwardRefExoticComponent & RefAttributes> = + memo(forwardRef>( + (props: Partial, ref: RefObject) => { + const { tableScrollerPadding, ...rest } = props; + // Access grid context providers + const { colElements: ColElements } = useGridMutableProvider(); + const { id } = useGridComputedProvider(); + + // Refs for DOM elements and child components + const footerTableRef: RefObject = useRef(null); + const rowSectionRef: RefObject = useRef(null); + + /** + * Memoized colgroup element to prevent unnecessary re-renders + * Contains column definitions for the table + */ + const colGroupContent: JSX.Element = useMemo(() => ( + + {ColElements} + + ), [ColElements, id]); + + /** + * Expose internal elements and methods through the forwarded ref + */ + useImperativeHandle(ref, () => ({ + footerTableRef: footerTableRef.current, + getFooterTable: () => footerTableRef.current, + ...(rowSectionRef.current) + }), [footerTableRef.current, rowSectionRef.current]); + + /** + * Memoized footer rows component to prevent unnecessary re-renders + */ + const footerRows: JSX.Element = useMemo(() => ( + + ), [tableScrollerPadding]); + + return ( + + {colGroupContent} + {footerRows} +
+ ); + } + )); + +/** + * Set display name for debugging purposes + */ +FooterTableBase.displayName = 'FooterTableBase'; + +/** + * Export the FooterTableBase component for use in other components + * + * @private + */ +export { FooterTableBase }; diff --git a/components/grids/src/grid/views/HeaderPanel.tsx b/components/grids/src/grid/views/HeaderPanel.tsx new file mode 100644 index 0000000..e3f7fc6 --- /dev/null +++ b/components/grids/src/grid/views/HeaderPanel.tsx @@ -0,0 +1,112 @@ +import { forwardRef, ForwardRefExoticComponent, RefAttributes, useImperativeHandle, useRef, useMemo, memo, CSSProperties, RefObject, JSX } from 'react'; +import { HeaderTableBase } from './index'; +import { HeaderPanelRef, HeaderTableRef, IHeaderPanelBase } from '../types'; +import { useGridComputedProvider } from '../contexts'; + +// CSS class constants following enterprise naming convention +const CSS_HEADER_TABLE: string = 'sf-table'; + +/** + * Default styles for header table to ensure consistent rendering + * + * @type {CSSProperties} + */ +const DEFAULT_TABLE_STYLE: CSSProperties = { + borderCollapse: 'separate', + borderSpacing: '0.25px' +}; + +/** + * HeaderPanelBase component renders the static area for the grid header. + * This component wraps the HeaderTableBase in a scrollable container and + * is responsible for organizing the header rows and synchronizing scrolling behavior. + * + * @component + * @private + * @param {Partial} props - Component properties + * @param {object} props.panelAttributes - Attributes to apply to the header panel container + * @param {object} props.scrollContentAttributes - Attributes to apply to the scrollable content container + * @param {RefObject} ref - Forwarded ref to expose internal elements + * @returns {JSX.Element} The rendered header container with scrollable table + */ +const HeaderPanelBase: ForwardRefExoticComponent & RefAttributes> = + memo(forwardRef>( + (props: Partial, ref: RefObject) => { + const { panelAttributes, scrollContentAttributes } = props; + const { sortSettings, filterSettings, gridLines } = useGridComputedProvider(); + + // Refs for DOM elements and child components + const headerPanelRef: RefObject = useRef(null); + const headerScrollRef: RefObject = useRef(null); + const headerTableRef: RefObject = useRef(null); + + /** + * Expose internal elements and methods through the forwarded ref + * Only define properties specific to HeaderPanel and forward HeaderTable properties + */ + useImperativeHandle(ref, () => ({ + // HeaderPanel specific properties + headerPanelRef: headerPanelRef.current, + headerScrollRef: headerScrollRef.current, + + // Forward all properties from HeaderTable + ...(headerTableRef.current) + }), [headerPanelRef.current, headerScrollRef.current, headerTableRef.current]); + + const headerTableSort: string = sortSettings?.enabled || filterSettings?.enabled ? 'sf-sortfilter' : ''; + const headerTableFilter: string = filterSettings?.enabled && gridLines === 'Default' ? 'sf-filterbartable' : ''; + const headerRightBorder: string = !filterSettings?.enabled || (filterSettings.enabled && (gridLines === 'Vertical' || gridLines === 'None')) ? ' sf-headerborder' : ''; + /** + * Memoized header table component to prevent unnecessary re-renders + */ + const headerTable: JSX.Element = useMemo(() => ( + + ), [headerTableFilter]); + + return ( +
+
+ {headerTable} +
+
+ ); + } + ), (prevProps: Partial, nextProps: Partial) => { + // Custom comparison function for memo to prevent unnecessary re-renders + // Only re-render if styles have changed + const prevStyle: CSSProperties = prevProps.panelAttributes?.style; + const nextStyle: CSSProperties = nextProps.panelAttributes?.style; + const prevScrollStyle: CSSProperties = prevProps.scrollContentAttributes?.style; + const nextScrollStyle: CSSProperties = nextProps.scrollContentAttributes?.style; + + // Deep comparison of style objects + const stylesEqual: boolean = + JSON.stringify(prevStyle) === JSON.stringify(nextStyle) && + JSON.stringify(prevScrollStyle) === JSON.stringify(nextScrollStyle); + + return stylesEqual; + }); + +/** + * Set display name for debugging purposes + */ +HeaderPanelBase.displayName = 'HeaderPanelBase'; + +/** + * Export the HeaderPanelBase component for direct usage if needed + * + * @private + */ +export { HeaderPanelBase }; diff --git a/components/grids/src/grid/views/HeaderRows.tsx b/components/grids/src/grid/views/HeaderRows.tsx new file mode 100644 index 0000000..67bfc6e --- /dev/null +++ b/components/grids/src/grid/views/HeaderRows.tsx @@ -0,0 +1,173 @@ +import { + forwardRef, + ForwardRefExoticComponent, + RefAttributes, + useImperativeHandle, + useRef, + useMemo, + useCallback, + memo, + RefObject, + JSX, + useEffect +} from 'react'; +import { + HeaderRowsRef, + IHeaderRowsBase, + RowRef, + IRow, + ICell, + RenderType +} from '../types'; +import { ColumnProps } from '../types/column.interfaces'; +import { + useGridMutableProvider, + useGridComputedProvider } from '../contexts'; +import { RowBase } from '../components'; +import { ColumnsChildren } from '../types/interfaces'; +import { isNullOrUndefined } from '@syncfusion/react-base'; + +// CSS class constants following enterprise naming convention +const CSS_COLUMN_HEADER: string = 'sf-columnheader'; +const CSS_FILTER_HEADER: string = 'sf-filterbar'; + +/** + * HeaderRowsBase component renders the header rows within the table header section + * + * @component + * @private + * @param {Partial} props - Component properties + * @param {RefObject} ref - Forwarded ref to expose internal elements and methods + * @returns {JSX.Element} The rendered thead element with header rows + */ +const HeaderRowsBase: ForwardRefExoticComponent & RefAttributes> = + memo(forwardRef>( + (props: Partial, ref: RefObject) => { + const { columnsDirective, headerRowDepth } = useGridMutableProvider(); + const { filterSettings, rowClass } = useGridComputedProvider(); + const { rowHeight, textWrapSettings } = useGridComputedProvider(); + + // Refs for DOM elements and child components + const headerSectionRef: RefObject = useRef(null); + const rowsObjectRef: RefObject[]> = useRef[]>([]); + + /** + * Returns the collection of header row elements + * + * @returns {HTMLCollectionOf | undefined} Collection of header row elements + */ + const getHeaderRows: () => HTMLCollectionOf | undefined = useCallback(() => { + return headerSectionRef.current?.children as HTMLCollectionOf; + }, [headerSectionRef.current?.children]); + + /** + * Returns the row options objects with DOM element references + * + * @returns {IRow[]} Array of row options objects with element references + */ + const getHeaderRowsObject: () => IRow[] = useCallback(() => rowsObjectRef.current, [rowsObjectRef.current]); + + /** + * Expose internal elements and methods through the forwarded ref + */ + useImperativeHandle(ref, () => ({ + headerSectionRef: headerSectionRef.current, + getHeaderRows, + getHeaderRowsObject + }), [getHeaderRows, getHeaderRowsObject]); + + /** + * Callback to store row element references directly in the row object + * + * @param {number} index - Row index + * @param {HTMLTableRowElement} element - Row DOM element + */ + const storeRowRef: (index: number, element: HTMLTableRowElement, cellRef: ICell[]) => void = + useCallback((index: number, element: HTMLTableRowElement, cellRef: ICell[]) => { + // Directly update the element reference in the row object + if (rowsObjectRef.current[index as number]) { // StrictMode purpose type gaurd condition added. + rowsObjectRef.current[index as number].element = element; + rowsObjectRef.current[index as number].cells = cellRef; + } + }, []); + + /** + * Memoized header row content to prevent unnecessary re-renders + */ + const headerRowContent: JSX.Element[] | null = useMemo(() => { + const rows: JSX.Element[] = []; + const rowOptions: IRow[] = []; + // Generate header rows based on headerRowDepth + for (let rowIndex: number = 0; rowIndex < headerRowDepth; rowIndex++) { + const options: IRow = {}; + options.index = rowIndex; + const rowId: string = `grid-header-row-${rowIndex}-${Math.random().toString(36).substr(2, 5)}`; + // Store the options object for getRowsObject + rowOptions.push({ ...options }); + const rowCustomClass: string = !isNullOrUndefined(rowClass) ? (typeof rowClass === 'function' ? + rowClass({rowType: 'header', rowIndex: options.index}) : rowClass) : ''; + rows.push( + { + if (element?.rowRef?.current) { + storeRowRef(rowIndex, element.rowRef.current, element.getCells()); + } + }} + role='row' + row={options} + key={rowId} + rowType={RenderType.Header} + className={`${CSS_COLUMN_HEADER} ${textWrapSettings?.enabled && textWrapSettings?.wrapMode === 'Header' ? 'sf-wrap' : ''}`.trim() + + (rowCustomClass.length ? `${' ' + rowCustomClass}` : '')} + style={{ height : `${rowHeight}px`}} + > + {(columnsDirective.props as ColumnsChildren).children} + + ); + if (rowIndex === headerRowDepth - 1 && filterSettings?.enabled) { + rows.push( + + {(columnsDirective.props as ColumnsChildren).children} + + ); + } + } + + // Store the row options in the ref for access via getRowsObject + rowsObjectRef.current = rowOptions; + return rows; + }, [columnsDirective, textWrapSettings?.enabled, textWrapSettings, rowHeight, filterSettings?.enabled, rowClass]); + + useEffect(() => { + return () => { + rowsObjectRef.current = []; + }; + }, []); + + return ( + + {headerRowContent} + + ); + } + )); + +/** + * Set display name for debugging purposes + */ +HeaderRowsBase.displayName = 'HeaderRowsBase'; + +/** + * Export the HeaderRowsBase component for use in other components + * + * @private + */ +export { HeaderRowsBase }; diff --git a/components/grids/src/grid/views/HeaderTable.tsx b/components/grids/src/grid/views/HeaderTable.tsx new file mode 100644 index 0000000..5646401 --- /dev/null +++ b/components/grids/src/grid/views/HeaderTable.tsx @@ -0,0 +1,100 @@ +import { + forwardRef, + ForwardRefExoticComponent, + RefAttributes, + useImperativeHandle, + useRef, + useMemo, + memo, + JSX, + RefObject +} from 'react'; +import { HeaderRowsBase } from './index'; +import { + HeaderRowsRef, + HeaderTableRef, + IHeaderTableBase +} from '../types'; +import { + useGridComputedProvider, + useGridMutableProvider +} from '../contexts'; + +/** + * HeaderTableBase component renders the table structure for grid headers + * + * @component + * @private + * @param {Partial} props - Component properties + * @param {string} [props.className] - Additional CSS class names + * @param {string} [props.role] - ARIA role attribute + * @param {Object} [props.style] - Inline styles for the table + * @param {RefObject} ref - Forwarded ref to expose internal elements and methods + * @returns {JSX.Element} The rendered header table component + */ +const HeaderTableBase: ForwardRefExoticComponent & RefAttributes> = + memo(forwardRef>( + (props: Partial, ref: RefObject) => { + // Access grid context providers + const { colElements: ColElements } = useGridMutableProvider(); + const { id } = useGridComputedProvider(); + + // Refs for DOM elements and child components + const headerTableRef: RefObject = useRef(null); + const rowSectionRef: RefObject = useRef(null); + + /** + * Memoized colgroup element to prevent unnecessary re-renders + * Contains column definitions for the table + */ + const colGroupContent: JSX.Element = useMemo(() => ( + + {ColElements.length ? ColElements : null} + + ), [ColElements, id]); + + /** + * Expose internal elements and methods through the forwarded ref + */ + useImperativeHandle(ref, () => ({ + headerTableRef: headerTableRef.current, + getHeaderTable: () => headerTableRef.current, + ...(rowSectionRef.current) + }), [headerTableRef.current, rowSectionRef.current]); + + /** + * Memoized header rows component to prevent unnecessary re-renders + */ + const headerRows: JSX.Element = useMemo(() => ( + + ), []); + + return ( + + {colGroupContent} + {headerRows} +
+ ); + } + )); + +/** + * Set display name for debugging purposes + */ +HeaderTableBase.displayName = 'HeaderTableBase'; + +/** + * Export the HeaderTableBase component for use in other components + * + * @private + */ +export { HeaderTableBase }; diff --git a/components/grids/src/grid/views/PagerPanel.tsx b/components/grids/src/grid/views/PagerPanel.tsx new file mode 100644 index 0000000..cb3a3e8 --- /dev/null +++ b/components/grids/src/grid/views/PagerPanel.tsx @@ -0,0 +1,85 @@ +import { PageProps, Pager, PagerRef } from '@syncfusion/react-pager'; +import { forwardRef, ForwardRefExoticComponent, RefAttributes, Ref, memo, RefObject, JSX, useState } from 'react'; +import { IGrid, GridRef } from '../types/grid.interfaces'; +import { useGridComputedProvider, useGridMutableProvider } from '../contexts'; +import { isNullOrUndefined } from '@syncfusion/react-base'; +import { PagerArgsInfo } from '../types/page.interfaces'; +import { MutableGridSetter } from '../types/interfaces'; + +/** + * PagerPanelBase component renders the pagination controls for the grid. + * This component encapsulates pagination functionality by wrapping the Pager component, + * handling page navigation events, and maintaining synchronization between the grid state and pagination UI. + * It supports features like page size selection, navigation buttons, and custom templates for pagination rendering. + * + * @param {Partial} props - Configuration for the pager including current page, total pages, and page count state + * @param {RefObject} ref - Forwarded ref that exposes imperative methods for parent components + * @returns {JSX.Element} The rendered pager element. + */ + +const PagerPanelBase: ForwardRefExoticComponent> = + memo(forwardRef>((props: Partial, ref: Ref): JSX.Element => { + const grid: Partial & Partial = useGridComputedProvider(); + const gridObj: Partial & Partial = useGridComputedProvider(); + const { setCurrentPage, setGridAction, allowKeyboard, pageSettings } = grid; + const { totalRecordsCount, cssClass, editModule } = useGridMutableProvider(); + const [_, setPagerCurrentPage] = useState(props.currentPage); + const clickHander: (e: PagerArgsInfo) => void = async(e: PagerArgsInfo) => { + const args: PagerArgsInfo = { + cancel: false, currentPage: e.currentPage, previousPage: e.oldPage, requestType: 'paging' + }; + args.type = 'pageChanging'; + const confirmResult: boolean = await editModule?.checkUnsavedChanges?.(); + if (!isNullOrUndefined(confirmResult) && !confirmResult) { + setPagerCurrentPage(pageSettings.currentPage); // force re-render as well not change pager currentPage state. + return; + } + grid.onPageChangeStart?.(args); + args.isPageLoading = false; + if (args.cancel) { + return; + } + setCurrentPage(args.currentPage as number); + setGridAction(args); + }; + + const addAriaAttribute: () => void = () => { + requestAnimationFrame(() => { + if (!(props.template) && (ref as RefObject).current && (ref as RefObject).current.element) { + if (gridObj?.getContentTable?.()) { + (ref as RefObject).current.element.setAttribute('aria-controls', gridObj?.getContentTable?.().id); + } + } + }); + }; + + return ( + + ); + } + )); + +/** + * Set display name for debugging purposes + */ +PagerPanelBase.displayName = 'PagerPanelBase'; + +/** + * Export the PagerPanelBase component for direct usage if needed + * + * @private + */ +export { PagerPanelBase }; diff --git a/components/grids/src/grid/views/Render.tsx b/components/grids/src/grid/views/Render.tsx new file mode 100644 index 0000000..9a176ae --- /dev/null +++ b/components/grids/src/grid/views/Render.tsx @@ -0,0 +1,241 @@ +import { + forwardRef, + ForwardRefExoticComponent, + RefAttributes, + useImperativeHandle, + useRef, + useLayoutEffect, + useMemo, + memo, + JSX, + RefObject, + ReactNode, + useEffect +} from 'react'; +import { HeaderPanelBase, ContentPanelBase, PagerPanelBase, GridToolbar } from './index'; +import { RenderRef, IRenderBase, HeaderPanelRef, ContentPanelRef, FooterPanelRef } from '../types'; +import { useGridComputedProvider, useGridMutableProvider } from '../contexts'; +import { useRender, useScroll } from '../hooks'; +import { ToolbarItemConfig, ToolbarAPI } from '../types/toolbar.interfaces'; +import { PagerRef } from '@syncfusion/react-pager'; +import { FooterPanelBase } from './FooterPanel'; +import { Spinner } from '@syncfusion/react-popups'; + +/** + * CSS class names used in the Render component + */ +const CSS_CLASS_NAMES: Record = { + GRID_HEADER: 'sf-gridheader sf-lib sf-droppable', + HEADER_CONTENT: 'sf-headercontent', + GRID_CONTENT: 'sf-gridcontent', + CONTENT: 'sf-content', + GRID_FOOTER: 'sf-gridfooter', + GRID_FOOTER_PADDING: 'sf-footerpadding', + FOOTER_CONTENT: 'sf-summarycontent' +}; + +/** + * Custom hook to synchronize scroll elements between header and content panels + * + * @private + * @param {Object} headerRef - Reference to the header panel + * @param {Object} contentRef - Reference to the content panel + * @param {Object} footerRef - Reference to the footer panel + * @param {Function} setHeaderElement - Function to set the header scroll element + * @param {Function} setContentElement - Function to set the content scroll element + * @param {Function} setFooterElement - Function to set the footer scroll element + * @param {Function} setPadding - Function to set padding for scroll elements + * @returns {void} + */ +const useSyncScrollElements: ( + headerRef: RefObject, + contentRef: RefObject, + footerRef: RefObject, + setHeaderElement: (el: HTMLElement | null) => void, + setContentElement: (el: HTMLElement | null) => void, + setFooterElement: (el: HTMLElement | null) => void, + setPadding: () => void +) => void = ( + headerRef: RefObject, + contentRef: RefObject, + footerRef: RefObject, + setHeaderElement: (el: HTMLElement | null) => void, + setContentElement: (el: HTMLElement | null) => void, + setFooterElement: (el: HTMLElement | null) => void, + setPadding: () => void +): void => { + useLayoutEffect(() => { + const headerElement: HTMLDivElement = headerRef.current?.headerScrollRef; + setHeaderElement(headerElement); + + const contentElement: HTMLDivElement = contentRef.current?.contentScrollRef; + setContentElement(contentElement); + setPadding(); + + const footerElement: HTMLDivElement = footerRef.current?.footerScrollRef; + setFooterElement(footerElement); + + return () => { + setHeaderElement(null); + setContentElement(null); + setFooterElement(null); + }; + }, [headerRef, contentRef, footerRef.current, setHeaderElement, setContentElement, setFooterElement, setPadding]); +}; + +/** + * Base component for rendering the grid structure with header and content panels + * + * @component + */ +const RenderBase: ForwardRefExoticComponent & RefAttributes> = memo( + forwardRef>((_props: Partial, ref: RefObject) => { + const headerPanelRef: RefObject = useRef(null); + const contentPanelRef: RefObject = useRef(null); + const footerPanelRef: RefObject = useRef(null); + const pagerObjectRef: RefObject = useRef(null); + + const { privateRenderAPI, protectedRenderAPI } = useRender(); + const { privateScrollAPI, protectedScrollAPI, setHeaderScrollElement, setContentScrollElement, setFooterScrollElement } = + useScroll(); + const { setPadding } = protectedScrollAPI; + const { headerContentBorder, headerPadding, onContentScroll, onHeaderScroll, onFooterScroll, getCssProperties } = privateScrollAPI; + const { textWrapSettings, pageSettings, aggregates, toolbar, id } = useGridComputedProvider(); + const { columnsDirective, currentViewData, totalRecordsCount, cssClass, toolbarModule, editModule } = useGridMutableProvider(); + + // Synchronize scroll elements between header and content panels + useSyncScrollElements( + headerPanelRef, + contentPanelRef, + footerPanelRef, + setHeaderScrollElement, + setContentScrollElement, + setFooterScrollElement, + setPadding + ); + + // Expose methods and properties through ref + useImperativeHandle(ref, () => ({ + // Render specific methods + refresh: protectedRenderAPI.refresh, + showSpinner: protectedRenderAPI.showSpinner, + hideSpinner: protectedRenderAPI.hideSpinner, + scrollModule: protectedScrollAPI, + // Forward all properties from header and content panels + ...(headerPanelRef.current as HeaderPanelRef), + ...(contentPanelRef.current as ContentPanelRef), + ...(footerPanelRef.current as FooterPanelRef), + pagerModule: pagerObjectRef.current + }), [ + protectedRenderAPI.refresh, + headerPanelRef.current, + contentPanelRef.current, + footerPanelRef.current, + pagerObjectRef.current + ]); + + const pagerPanel: JSX.Element = useMemo(() => ( + + + ), [totalRecordsCount, pageSettings]); + + + // Memoize header panel to prevent unnecessary re-renders + const headerPanel: JSX.Element = useMemo(() => ( + + ), [headerPadding, headerContentBorder, onHeaderScroll]); + + // Memoize content panel to prevent unnecessary re-renders + const contentPanel: JSX.Element = useMemo(() => ( + + ), [setPadding, privateRenderAPI.contentStyles, privateRenderAPI.isContentBusy, onContentScroll]); + + const footerPanel: JSX.Element = useMemo(() => { + if (!columnsDirective || !currentViewData || currentViewData.length === 0) { + return null; + } + const tableScrollerPadding: boolean = headerPadding[`${getCssProperties.padding}`] && headerPadding[`${getCssProperties.padding}`] !== '0px' ? true : false; + const cssClass: string = `${CSS_CLASS_NAMES.GRID_FOOTER} ${tableScrollerPadding ? CSS_CLASS_NAMES.GRID_FOOTER_PADDING : ''}`; + return (); + }, [headerPadding, getCssProperties, columnsDirective, currentViewData, onFooterScroll]); + + useEffect(() => { + if (!privateRenderAPI.isContentBusy && editModule?.isShowAddNewRowActive && !editModule?.isShowAddNewRowDisabled) { + contentPanelRef?.current?.addInlineRowFormRef?.current?.focusFirstField(); + } + }, [privateRenderAPI.isContentBusy]); + + return ( + <> + + {toolbarModule && toolbar?.length > 0 && ( + + )} + {headerPanel} + {contentPanel} + {aggregates?.length ? footerPanel : null} + {pageSettings?.enabled && pagerPanel} + + ); + }) +); + +/** + * Columns component that wraps RenderBase for external usage + * + * @returns {JSX.Element} Rendered component + */ +export const Columns: React.FC<{ children?: ReactNode }> = (): JSX.Element => { + return null; +}; + +export { + RenderBase +}; + +RenderBase.displayName = 'RenderBase'; diff --git a/components/grids/src/grid/views/editing/ConfirmDialog.tsx b/components/grids/src/grid/views/editing/ConfirmDialog.tsx new file mode 100644 index 0000000..1e19249 --- /dev/null +++ b/components/grids/src/grid/views/editing/ConfirmDialog.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { useState, useCallback, useEffect } from 'react'; +import { Dialog } from '@syncfusion/react-popups'; +import { Button, Color, Variant } from '@syncfusion/react-buttons'; +import { useGridComputedProvider, useGridMutableProvider } from '../../contexts'; +import { ConfirmDialogProps } from '../../types/edit.interfaces'; + +/** + * ConfirmDialog component for handling confirmation dialogs in grid editing + * + * This component provides a React-based dialog system to replace window.confirm + * and window.alert usage in the grid editing functionality, following the original + * grid component's dialog patterns and using sf- classes as per React implementation standards. + * + * @param {ConfirmDialogProps} props - ConfirmDialog component props + * @param {boolean} props.isOpen - Whether the dialog is open + * @param {Object} props.config - Dialog configuration + * @param {Function} [props.onConfirm] - Callback when confirm button is clicked + * @param {Function} [props.onCancel] - Callback when cancel button is clicked + * @returns {React.ReactElement} ConfirmDialog component + */ +export const ConfirmDialog: React.FC = ({ + isOpen, + config, + onConfirm, + onCancel +}: ConfirmDialogProps): React.ReactElement | null => { + const { getParentElement, cssClass } = useGridMutableProvider(); + const { id } = useGridComputedProvider(); + const [internalOpen, setInternalOpen] = useState(false); + + // Sync internal state with props + useEffect((): void => { + setInternalOpen(isOpen); + }, [isOpen]); + + /** + * Handle confirm button click + */ + const handleConfirm: () => void = useCallback((): void => { + onConfirm?.(); + setInternalOpen(false); + }, [onConfirm]); + + /** + * Handle cancel button click + * + * @returns {void} + */ + const handleCancel: () => void = useCallback((): void => { + onCancel?.(); + setInternalOpen(false); + }, [onCancel]); + + return ( + + + {/* Only show cancel button if cancelText is provided and not empty */} + {config.cancelText && config?.cancelText.trim() !== '' && ( + + )} + + } + > + {config.message} + + ); +}; + +ConfirmDialog.displayName = 'ConfirmDialog'; diff --git a/components/grids/src/grid/views/editing/EditCell.tsx b/components/grids/src/grid/views/editing/EditCell.tsx new file mode 100644 index 0000000..fd4a02e --- /dev/null +++ b/components/grids/src/grid/views/editing/EditCell.tsx @@ -0,0 +1,365 @@ +import { forwardRef, useImperativeHandle, useRef, RefObject, useCallback, JSX, isValidElement, memo } from 'react'; +import { EditType, ValueType } from '../../types'; +import { MutableGridSetter } from '../../types/interfaces'; +import { GridRef } from '../../types/grid.interfaces'; +import { EditParams, EditCellProps, EditCellRef, EditCellInputRef } from '../../types/edit.interfaces'; +import { TextBox, ITextBox, TextBoxProps } from '@syncfusion/react-inputs'; +import { NumericTextBox, INumericTextBox, NumericTextBoxProps, TextBoxChangeEvent, NumericChangeEvent } from '@syncfusion/react-inputs'; +import { Checkbox, ICheckbox, CheckboxProps, CheckboxChangeEvent } from '@syncfusion/react-buttons'; +import { DatePicker, IDatePicker, DatePickerProps, ChangeEvent } from '@syncfusion/react-calendars'; +import { DropDownList, IDropDownList, DropDownListProps, ChangeEventArgs } from '@syncfusion/react-dropdowns'; +import { useGridComputedProvider, useGridMutableProvider } from '../../contexts'; +import { DataManager, DataResult, DataUtil, Predicate, Query } from '@syncfusion/react-data'; +import { getCustomDateFormat } from '../../utils'; +import { getNumberPattern } from '@syncfusion/react-base'; + +/** + * EditCell component for rendering different types of edit inputs + * + * @param props - EditCell component props + * @param ref - Forward ref for imperative methods + * @returns EditCell component + */ +export const EditCell: React.ForwardRefExoticComponent> = + memo(forwardRef(({ + column, + value, + rowData, + error, + onChange, + onBlur, + onFocus, + isAdd, + disabled, + formState + }: EditCellProps, ref: React.ForwardedRef) => { + // Type for component refs that can be either native HTML elements or Syncfusion components + const inputRef: RefObject = useRef(null); + + // Access grid context to get complete dataSource for dropdown + const gridContext: Partial & Partial = useGridComputedProvider(); + const { cssClass } = useGridMutableProvider(); + const dataSource: Object[] | DataManager | DataResult = gridContext.dataSource; + + /** + * Focuses the input element (works with both native inputs and Syncfusion components) + * Enhanced focus handling with proper element checking + */ + const focus: () => void = useCallback(() => { + // Don't focus disabled elements + // Primary key fields should be enabled during add operations + // Added disabled prop to fully support showAddNewRow functionality + const isDisabled: boolean = disabled || column.allowEdit === false || (column.isPrimaryKey === true && !isAdd); + if (isDisabled || !inputRef.current) { + return; + } + + // Use requestAnimationFrame to ensure DOM is ready + requestAnimationFrame(() => { + const currentInput: HTMLInputElement | HTMLSelectElement | ITextBox | INumericTextBox | ICheckbox | IDatePicker + | IDropDownList = inputRef.current; + + // Check if currentInput is a valid DOM element before using DOM methods + if (!currentInput) { + return; + } + + if ('element' in currentInput && currentInput.element) { + // For Syncfusion components, focus the underlying element + const element: HTMLElement = currentInput.element as HTMLElement; + if (element && typeof element.querySelector === 'function') { + if (element && !element.hasAttribute('disabled') && !(element.hasAttribute('readonly') && !element.classList.contains('sf-dropdownlist')) && typeof element.focus === 'function') { + (element.classList.contains('sf-date-wrapper') ? element.querySelector('input') : element).focus(); + } + } + else if (element && !element.hasAttribute('disabled') && !element.hasAttribute('readonly') && typeof element.focus === 'function') { + element.focus(); + } + } + }); + }, [column.allowEdit, column.isPrimaryKey, isAdd]); + + /** + * Gets the current value + * + * @returns {ValueType | null} The current value of the edit cell + */ + const getValue: () => ValueType | null = (): ValueType | null => { + return value; + }; + + /** + * Sets the value + */ + const setValue: (newValue: ValueType | null) => void = useCallback((newValue: ValueType | null) => { + onChange?.(newValue); + }, [onChange, value]); + + // Expose imperative methods via ref + useImperativeHandle(ref, () => ({ + focus, + getValue, + setValue + }), [focus, getValue, setValue, onChange, isAdd]); + + /** + * Handle Syncfusion component change events + * Prevents input values from clearing during typing + */ + const handleSyncfusionChange: (newValue: string | number | boolean | Date | null | Object) => void = + useCallback((newValue: string | number | boolean | Date | null) => { + // Call onChange immediately for form validator state real-time updates + onChange?.(newValue); + }, [onChange]); + + /** + * Handle Syncfusion component blur events + * Enhanced blur handling with proper element checking and validation trigger + */ + const handleSyncfusionBlur: () => void = useCallback(() => { + setTimeout(() => { + onBlur?.(value); + }, 0); // Datepicker blur validation remove fix + }, [onBlur, value, formState]); + + /** + * Handle Syncfusion component focus events + * Enhanced focus handling to maintain focus state + */ + const handleSyncfusionFocus: () => void = useCallback(() => { + onFocus?.(); + }, [onFocus, formState]); + + /** + * Render editor component with fallback to HTML input + * This structure allows easy replacement when Syncfusion components become available + * Added disabled state support for non-editable columns + * Primary key fields should be enabled during add operations + * + * @param {string} editorType - The type of editor to render (textbox, numeric, checkbox, etc.) + * @param {Object} editorProps - Additional properties to pass to the editor component + * @returns {JSX.Element} The rendered editor component as JSX element + */ + const renderEditor: (editorType: string, editorProps?: EditParams) => JSX.Element = + (editorType: string, editorProps: EditParams = {}): JSX.Element => { + // Use isAdd parameter to determine if this is an add operation + // This replaces the complex isAddOperation calculation with the passed parameter + // Added disabled prop to fully support showAddNewRow functionality + const isDisabled: boolean = disabled || column.allowEdit === false || (column.isPrimaryKey === true && !isAdd); + const baseProps: {[key: string]: string | number | boolean} = { + 'data-mappinguid': column.uid, + 'id': `grid-edit-${column.field}` + }; + + switch (editorType) { + case 'textbox': + return ( + } + className={`sf-field${formState?.errors[column.field] ? + ' sf-error' : ''}` + (cssClass !== '' ? (' ' + cssClass) : '')} + value={value?.toString() || ''} + onChange={isDisabled ? undefined : (event: TextBoxChangeEvent) => + handleSyncfusionChange(event.value)} + onBlur={isDisabled ? undefined : handleSyncfusionBlur} + onFocus={isDisabled ? undefined : handleSyncfusionFocus} + labelMode={'Never'} + disabled={isDisabled} + readOnly={isDisabled} + {...baseProps} + {...editorProps as TextBoxProps} + /> + ); + + case 'numeric': + return ( + } + className={`sf-field${formState?.errors[column.field] ? + ' sf-error' : ''}` + (cssClass !== '' ? (' ' + cssClass) : '')} + format={(typeof (column.format) === 'object' ? getNumberPattern(column.format, false)?.toLowerCase() : + (column.format as string)?.toLowerCase()) ?? 'n2'} // only provided string format support. + value={value as number || null} + onChange={isDisabled ? undefined : (event: NumericChangeEvent) => + handleSyncfusionChange(event.value)} + onBlur={isDisabled ? undefined : handleSyncfusionBlur} + onFocus={isDisabled ? undefined : handleSyncfusionFocus} + labelMode={'Never'} + disabled={isDisabled} + readOnly={isDisabled} + role="spinbutton" + autoComplete="off" + {...baseProps} + {...editorProps as NumericTextBoxProps} + /> + ); + + case 'checkbox': + return ( + } + className={`sf-field${formState?.errors[column.field] ? + ' sf-error' : ''}` + (cssClass !== '' ? (' ' + cssClass) : '')} + checked={Boolean(value)} + onChange={isDisabled ? undefined : (event: CheckboxChangeEvent) => + handleSyncfusionChange(event.value)} + onBlur={isDisabled ? undefined : (args: React.FocusEventHandler & + React.FocusEvent) => { + args.target.closest('.sf-checkbox-wrapper.sf-field')?.classList.remove('sf-focus'); + handleSyncfusionBlur(); + }} + onFocus={isDisabled ? undefined : (args: React.FocusEventHandler & + React.FocusEvent) => { + args.target.closest('.sf-checkbox-wrapper.sf-field')?.classList.add('sf-focus'); + handleSyncfusionFocus(); + }} + label={' '} + disabled={isDisabled} + {...baseProps} + {...editorProps as Omit} + /> + ); + + case 'datepicker': + return ( + } + className={`sf-field${formState?.errors[column.field] ? + ' sf-error' : ''}` + (cssClass !== '' ? (' ' + cssClass) : '')} + value={value ? new Date(value as Date) : null} + onChange={isDisabled ? undefined : ((args: ChangeEvent) => { + handleSyncfusionChange(args.value as Date); + }) as any} + format={column.format ? getCustomDateFormat(column.format, column.type) : 'M/d/yyyy'} // only provided string format support + onClose={isDisabled ? undefined : handleSyncfusionBlur} + onOpen={isDisabled ? undefined : handleSyncfusionFocus} + labelMode={'Never'} + disabled={isDisabled} + onBlur={isDisabled ? undefined : handleSyncfusionBlur} + readOnly={isDisabled} + {...baseProps} + {...editorProps as DatePickerProps} + /> + ); + + case 'dropdown': + return ( + } + className={`sf-field${formState?.errors[column.field] ? + ' sf-error' : ''}` + (cssClass !== '' ? (' ' + cssClass) : '')} + value={value} + dataSource={(dataSource instanceof DataManager ? dataSource : + 'result' in dataSource ? dataSource.result : new DataManager(dataSource as Object[])) as + DataManager | string[] | { [key: string]: object; }[] | number[] | boolean[]} + fields={{ value: column.field }} + query={new Query().where(new Predicate(column.field, 'notequal', null, true, false)).select([column.field])} + actionComplete={(e: { result: Object[] }) => { + e.result = DataUtil.distinct(e.result, column.field, true); + }} + onChange={isDisabled ? undefined : (args: ChangeEventArgs) => { + handleSyncfusionChange(args.value); + }} + onClose={isDisabled ? undefined : handleSyncfusionBlur} + onOpen={isDisabled ? undefined : handleSyncfusionFocus} + disabled={isDisabled} + onBlur={isDisabled ? undefined : handleSyncfusionBlur} + popupHeight={'200px'} + sortOrder={(editorProps as DropDownListProps).sortOrder} + labelMode={'Never'} + readOnly={isDisabled} + allowObjectBinding={editorProps.allowObjectBinding || false} + popupWidth={editorProps.popupWidth || '100%'} + filterable={editorProps.filterable || false} + filterType={editorProps.filterType || 'StartsWith'} + ignoreAccent={editorProps.ignoreAccent || false} + ignoreCase={true} + {...baseProps} + {...editorProps as DropDownListProps} + /> + ); + + default: + return ( + } + className={`sf-field${formState?.errors[column.field] ? + ' sf-error' : ''}` + (cssClass !== '' ? (' ' + cssClass) : '')} + value={value?.toString() || ''} + onChange={isDisabled ? undefined : (event: TextBoxChangeEvent) => + handleSyncfusionChange(event.value)} + onBlur={isDisabled ? undefined : handleSyncfusionBlur} + onFocus={isDisabled ? undefined : handleSyncfusionFocus} + labelMode={'Never'} + disabled={isDisabled} + readOnly={isDisabled} + {...baseProps} + {...editorProps as TextBoxProps} + /> + ); + } + }; + + // Render custom edit template if provided + if (column.editTemplate) { + if (typeof column.editTemplate === 'string' || isValidElement(column.editTemplate)) { + return column.editTemplate; + } else { + return column.editTemplate({ + defaultValue: value, + column, + rowData, + error, + action: isAdd ? 'Add' : 'Edit', + onChange + }); + } + } + + // Support edit.type property patterns + const editType: string | EditType = column.edit?.type; + const editParams: EditParams = column.edit?.params || {}; + + // Render based on edit type configuration + if (editType) { + let editorComponent: JSX.Element; + switch (editType) { + + case EditType.NumericTextBox: + editorComponent = renderEditor('numeric', editParams); + break; + + case EditType.TextBox: + editorComponent = renderEditor('textbox', { + ...editParams + }); + break; + + case EditType.CheckBox: + editorComponent = renderEditor('checkbox', editParams); + break; + + case EditType.DatePicker: + // Use DatePicker component for datePickerEdit + editorComponent = renderEditor('datepicker', editParams); + break; + + case EditType.DropDownList: + // Use DropDownList component for dropDownEdit + editorComponent = renderEditor('dropdown', editParams); + break; + } + + return editorComponent; + } + + // Render default type + const editorComponent: JSX.Element = renderEditor('', {...editParams}); + return ( + <> + {editorComponent} + + ); + })); + +EditCell.displayName = 'EditCell'; diff --git a/components/grids/src/grid/views/editing/InlineEditForm.tsx b/components/grids/src/grid/views/editing/InlineEditForm.tsx new file mode 100644 index 0000000..4d9f7e2 --- /dev/null +++ b/components/grids/src/grid/views/editing/InlineEditForm.tsx @@ -0,0 +1,769 @@ +import { memo, useRef, useEffect, useCallback, forwardRef, useMemo, useState, useImperativeHandle, CSSProperties, JSX } from 'react'; +import { FieldValidationRules, Form, FormField, FormState, FormValueType, IFormValidator, ValidationRules } from '@syncfusion/react-inputs'; +import { EditCell, ValidationTooltips } from '../index'; +import { EditCellRef, InlineEditFormProps, InlineEditFormRef } from '../../types/edit.interfaces'; +import { useGridComputedProvider, useGridMutableProvider } from '../../contexts'; +import { IValueFormatter, ValueType } from '../../types'; +import { ColumnProps, ColumnValidationConfig, IColumnBase } from '../../types/column.interfaces'; +import { getObject } from '../../utils'; +import { DataUtil } from '@syncfusion/react-data'; +import { isNullOrUndefined, isUndefined } from '@syncfusion/react-base'; + +/** + * InlineEditForm component that prevents unnecessary re-renders during typing + * + * @param props - InlineEditForm component props + * @param ref - Forward ref for imperative methods + * @returns Memoized EditForm component + */ +export const InlineEditForm: React.ForwardRefExoticComponent> = + memo(forwardRef(({ + editData, + validationErrors, + onFieldChange, + onSave, + onCancel, + columns, + editRowIndex, + rowUid, + template: CustomTemplate, + disabled = false, + isAddOperation + }: InlineEditFormProps, ref: React.ForwardedRef) => { + // Use refs to store stable callback references + const onFieldChangeRef: React.RefObject<((field: string, value: ValueType | null) => void) | + undefined> = useRef(onFieldChange); + + // Only update refs when callbacks actually change + useEffect(() => { + onFieldChangeRef.current = onFieldChange; + }, [onFieldChange]); + + // Create stable callback wrappers that don't change on every render + const stableOnFieldChange: (field: string, value: ValueType | null) => void = + useCallback((field: string, value: ValueType | null): void => { + // Use the current ref value to ensure we always have the latest callback + onFieldChangeRef.current?.(field, value); + }, []); + + const formRef: React.RefObject = useRef(null); + const editCellRefs: React.RefObject<{ [field: string]: EditCellRef }> = useRef<{ [field: string]: EditCellRef }>({}); + const { rowHeight, id, getVisibleColumns, serviceLocator, editModule, contentPanelRef, contentTableRef, + height } = useGridComputedProvider(); + const { colElements: ColElements, cssClass } = useGridMutableProvider(); + const formatter: IValueFormatter = serviceLocator?.getService('valueFormatter'); + + /** + * Internal data state that's isolated from grid until save + */ + const [internalData, setInternalData] = useState(() => { + // For add operations, start with truly empty data + if (isAddOperation) { + const addData: Record = {}; + columns.forEach((column: ColumnProps) => { + if (column.field && column.defaultValue !== undefined) { + // Apply defaultValue only when explicitly set + if (column.type === 'string') { + addData[column.field] = typeof column.defaultValue === 'string' + ? column.defaultValue + : String(column.defaultValue); + } else { + addData[column.field] = column.defaultValue; + } + } + // Don't set any value if no defaultValue is specified + }); + return addData; + } else { + // For edit operations, use all existing data + return editData ? { ...editData } : {}; + } + }); + + const isLastRow: boolean = useMemo(() => { + return height !== 'auto' && ((editModule?.editSettings?.newRowPosition === 'Bottom' && isAddOperation) || + (!isAddOperation && rowUid === contentTableRef?.rows?.[contentTableRef?.rows?.length - 1].getAttribute('data-uid'))) && + (contentPanelRef?.firstElementChild as HTMLElement)?.offsetHeight > contentTableRef?.scrollHeight; + }, [contentPanelRef, contentTableRef?.rows?.length, editModule?.editSettings, isAddOperation]); + + /** + * Enhanced FormValidator integration with comprehensive validation rule mapping + * This creates a proper ValidationRules object for the FormValidator component + */ + const formValidationRules: ValidationRules = useMemo(() => { + const rules: ValidationRules = {}; + + columns.forEach((column: ColumnProps) => { + if (column.field && column.visible && (column.validationRules || column.type || column.edit?.type)) { + const columnRules: FieldValidationRules = {}; + const validationRules: ColumnValidationConfig = column.validationRules || {}; + + // Convert column validation rules to FormValidator format + if (validationRules.required !== undefined && validationRules.required !== false) { + columnRules.required = [validationRules.required, 'This field is required.']; + } + + if (validationRules.minLength !== undefined) { + columnRules.minLength = [validationRules.minLength, `Please enter at least ${validationRules.minLength} characters.`]; + } + + if (validationRules.maxLength !== undefined) { + columnRules.maxLength = [validationRules.maxLength, `Please enter no more than ${validationRules.maxLength} characters.`]; + } + + if (validationRules.min !== undefined) { + columnRules.min = [validationRules.min, `Please enter a value greater than or equal to ${validationRules.min}.`]; + } + + if (validationRules.max !== undefined) { + columnRules.max = [validationRules.max, `Please enter a value less than or equal to ${validationRules.max}.`]; + } + + // Enhanced range validation support + if (validationRules.range && Array.isArray(validationRules.range) && validationRules.range.length === 2) { + columnRules.range = [validationRules.range, `Please enter a value in between ${validationRules.range[0]} and ${validationRules.range[1]}.`]; + } + + // Enhanced range length validation support + if (validationRules.rangeLength && Array.isArray(validationRules.rangeLength) && + validationRules.rangeLength.length === 2) { + columnRules.rangeLength = [validationRules.rangeLength, + `Please enter between ${ + validationRules.rangeLength[0] + } and ${validationRules.rangeLength[1]} characters.`]; + } + + // Enhanced regex validation support + if (validationRules.regex) { + columnRules.regex = [validationRules.regex, 'This field format is invalid.']; + } + + if (validationRules.email) { + columnRules.email = [validationRules.email, 'Please enter a valid email.']; + } + + if (validationRules.url) { + columnRules.url = [validationRules.url, 'Please enter a valid url.']; + } + + if (validationRules.digits) { + columnRules.digits = [validationRules.digits, 'Please enter digits(0-9) only.']; + } + + if (validationRules.creditCard) { + columnRules.creditCard = [validationRules.creditCard, 'Please enter a valid creditcard number.']; + } + + if (validationRules.tel) { + columnRules.tel = [validationRules.tel, 'Please enter a valid telephone number.']; + } + + if (validationRules.equalTo) { + columnRules.equalTo = [validationRules.equalTo, + `This field value not matches with ${validationRules.equalTo} field value.`]; + } + + // Enhanced custom validation with proper error handling + if (validationRules.customValidator && typeof validationRules.customValidator === 'function') { + columnRules.customValidator = (value: FormValueType) => { + try { + const result: string | null = validationRules.customValidator(value); + return result || null; + } catch (error) { + return `Validation error: ${(error as Error).message}`; + } + }; + } + + if (column.type === 'number') { + columnRules.number = [validationRules.number, 'Please enter a valid number.']; + } + + if (column.type === 'date') { + columnRules.date = [validationRules.date, 'Please enter a valid date.']; + } + + // Only add rules if there are actual validation rules defined + if (Object.keys(columnRules).length > 0) { + rules[column.field] = columnRules; + } + } + }); + + return rules; + }, [columns]); + + const isNewSessionRef: React.RefObject = useRef(false); + + // Track Tab direction for proper focus management after save + const lastTabDirectionRef: React.RefObject = useRef(true); // true = forward (Tab), false = backward (Shift+Tab) + + // Reset the new session flag after initialization + useEffect(() => { + if (isNewSessionRef.current) { + isNewSessionRef.current = false; + } + }); + + /** + * Store edit cell ref + */ + const storeEditCellRef: (field: string, cellRef: EditCellRef | null) => void = + useCallback((field: string, cellRef: EditCellRef | null) => { + if (cellRef) { + editCellRefs.current[field as string] = cellRef; + } else { + delete editCellRefs.current[field as string]; + } + }, []); + + /** + * Focus the first editable field + * For add operations, primary key fields should be focused first + * For edit operations, skip primary key fields (they're disabled) + */ + const focusFirstField: () => void = useCallback(() => { + let firstEditableColumn: ColumnProps | undefined; + + if (isAddOperation) { + // For add operations, primary key fields are enabled and should be focused first + firstEditableColumn = columns.find((col: ColumnProps) => + col.allowEdit !== false && col.visible && + col.field && + col.isPrimaryKey === true + ); + + // If no primary key field found, find the first non-primary key editable field + if (!firstEditableColumn) { + firstEditableColumn = columns.find((col: ColumnProps) => + col.allowEdit !== false && col.visible && + !col.isPrimaryKey && + col.field + ); + } + } else { + // For edit operations, skip primary key fields (they're disabled) + // Focus the first non-primary key editable field + if (editModule.focusLastField.current) { + firstEditableColumn = [...columns].reverse().find((col: ColumnProps) => + col.allowEdit !== false && col.visible && + !col.isPrimaryKey && + col.field + ); + } else { + firstEditableColumn = columns.find((col: ColumnProps) => + col.allowEdit !== false && col.visible && + !col.isPrimaryKey && + col.field + ); + } + editModule.focusLastField.current = false; + } + + if (firstEditableColumn && editCellRefs.current[firstEditableColumn.field]) { + requestAnimationFrame(() => { + setTimeout(() => { + editCellRefs?.current?.[firstEditableColumn?.field]?.focus?.(); + }, 0); + }); + } + }, [columns, isAddOperation]); + + /** + * Enhanced FormValidator validation state management + * This tracks the FormValidator's internal validation state properly + */ + const [formState, setFormState] = useState(null); + + /** + * Validate the form using FormValidator component + * This ensures validation works correctly without unnecessary re-rendering + */ + const validateForm: () => boolean = useCallback((): boolean => { + if (formRef.current) { + // Trigger FormValidator validation + const isFormValid: boolean = formRef.current.validate(); + return isFormValid; + } + // Fallback to existing validation errors check when FormValidator is not available + return Object.keys(validationErrors).length === 0; + }, [validationErrors]); + + /** + * Get all edit cell refs + */ + const getEditCells: () => EditCellRef[] = useCallback((): EditCellRef[] => { + return Object.values(editCellRefs.current); + }, []); + + /** + * Get the form element + */ + const getFormElement: () => HTMLFormElement | null = useCallback((): HTMLFormElement | null => { + return formRef.current.element; + }, []); + + /** + * Get current form data + */ + const getCurrentData: () => string | number | boolean | Record | Date = useCallback(() => { + return formState?.values; + }, [formState]); + + /** + * Handle field value change with proper data isolation + * This prevents re-renders while maintaining data consistency + */ + const handleFieldChange: (column: ColumnProps, value: ValueType) => + void = useCallback((column: ColumnProps, value: ValueType) => { + let formattedValue: ValueType = (column?.type === 'date' || column?.type === 'datetime' || column?.type === 'number') && typeof value === 'string' ? + (formatter.fromView(value, (column as IColumnBase)?.parseFn, column?.type)) : value; + if ((column?.type === 'number' && isNaN(formattedValue as number)) || + ((column?.type === 'date' || column?.type === 'datetime') && isUndefined(formattedValue as string))) { + formattedValue = ''; + } + // Update internal data immediately for UI responsiveness + const topLevelKey: string = column.field.split('.')[0]; + const copiedComplexData: Object = column.field.includes('.') && typeof internalData[topLevelKey as string] === 'object' + ? { + ...internalData, + [topLevelKey]: JSON.parse(JSON.stringify(internalData[topLevelKey as string])) + } + : { ...internalData }; + + const editedData: Object = DataUtil.setValue(column.field, formattedValue, copiedComplexData); + setInternalData({...editedData}); + + // Update FormValidator state + if (!isNullOrUndefined(internalData[topLevelKey as string]) && typeof internalData[topLevelKey as string] === 'object' + && !(internalData[topLevelKey as string] instanceof Date)) { + formState?.onChange?.(topLevelKey, { value: { + ...editedData[topLevelKey as string], + [column.field.split('.')[1]]: value + } as FormValueType }); + } else { + formState?.onChange?.(column.field, { value: value as FormValueType }); + } + // Notify parent for validation but don't expose data until save + stableOnFieldChange?.(column.field, formattedValue); + }, [stableOnFieldChange, formState]); + + /** + * Handle field blur + * Enhanced blur handling to properly trigger FormValidator validation + * This ensures validation happens on every field blur event + */ + const handleFieldBlur: (column: ColumnProps, value: string | number | boolean | Record | Date) => + void = useCallback((column: ColumnProps, value: string | number | boolean | Record | Date) => { + value = (column?.type === 'date' || column?.type === 'number') && typeof value === 'string' ? + (formatter.fromView(value, (column as IColumnBase)?.parseFn, column?.type)) : value; + // Update internal data on blur (consistency check) + const topLevelKey: string = column.field.split('.')[0]; + const copiedComplexData: Object = column.field.includes('.') && typeof internalData[topLevelKey as string] === 'object' + ? { + ...internalData, + [topLevelKey]: JSON.parse(JSON.stringify(internalData[topLevelKey as string])) + } + : { ...internalData }; + + const editedData: Object = DataUtil.setValue(column.field, value, copiedComplexData); + setInternalData({...editedData}); + + if (!(isAddOperation && editModule.isShowAddNewRowActive) || + (isAddOperation && formState && Object.keys(formState.errors).length > 0)) { + // Always trigger FormValidator blur validation + // This is essential for proper validation behavior + formState?.onBlur?.(column.field); + + // Also trigger manual validation for immediate feedback + // This ensures validation errors are displayed immediately on blur + formRef.current?.validateField?.(column.field); + } + + }, [formState]); + + /** + * Handle Enter key (save) + */ + const handleEnter: () => void = useCallback(() => { + onSave?.(); + }, [onSave]); + + /** + * Handle Escape key (cancel) + */ + const handleEscape: () => void = useCallback(() => { + onCancel?.(); + }, [onCancel]); + + /** + * Expose imperative methods via ref + */ + useImperativeHandle(ref, () => ({ + focusFirstField, + validateForm, + getEditCells, + getFormElement, + getCurrentData, + editCellRefs, + formState, + formRef + }), [focusFirstField, validateForm, getEditCells, getFormElement, getCurrentData, formState, + editCellRefs.current, formRef.current]); + + /** + * Enhanced Tab boundary detection and auto-save behavior + * This function detects when Tab/Shift+Tab navigation reaches the boundaries of the edit form + * and automatically saves the form while maintaining proper focus management + * For add operations, include primary key fields in boundary detection + */ + const handleTabBoundaryNavigation: (event: KeyboardEvent, currentField: string) => boolean = + useCallback((event: KeyboardEvent, currentField: string) => { + // Get editable columns based on operation type + let editableColumns: ColumnProps[]; + + if (isAddOperation) { + // For add operations, include primary key fields (they're enabled) + editableColumns = columns.filter((col: ColumnProps) => + col.allowEdit !== false && + col.field + ); + } else { + // For edit operations, exclude primary key fields (they're disabled) + editableColumns = columns.filter((col: ColumnProps) => + col.allowEdit !== false && col.visible && + !col.isPrimaryKey && + col.field + ); + } + + const currentIndex: number = editableColumns.findIndex((col: ColumnProps) => col.field === currentField); + + if (currentIndex === -1) { + return false; + } + + const isTabForward: boolean = event.key === 'Tab' && !event.shiftKey; + const isTabBackward: boolean = event.key === 'Tab' && event.shiftKey; + const isLastField: boolean = currentIndex === editableColumns.length - 1; + const isFirstField: boolean = currentIndex === 0; + + // Detect boundary conditions for auto-save + if ((isTabForward && isLastField) || (isTabBackward && isFirstField)) { + // Track Tab direction for proper focus management + lastTabDirectionRef.current = isTabForward; + + // Auto-save when reaching edit form boundaries + // Based on the information in your clipboard, this implements the expected behavior where + // continuously pressing Tab or Shift+Tab to exit focus from edit form should + // automatically save the changes and move focus to the appropriate cell of the saved content row + // Prevent the default Tab behavior to avoid focus jumping + event.preventDefault(); + event.stopPropagation(); + + // Use setTimeout to ensure proper timing for auto-save + setTimeout(() => { + // Pass Tab direction to save function for proper focus management + // This triggers the enhanced endEdit function in useEdit with proper direction info + onSave?.(lastTabDirectionRef.current); + }, 0); + return true; // Indicate that boundary navigation was handled + } + + return false; // Indicate that normal Tab navigation should continue + }, [columns, isAddOperation, onSave]); + + /** + * Render edit cells with proper data binding + * For add operations, primary key fields should be focused first + * For edit operations, skip primary key fields (they're disabled) + */ + const renderEditCells: React.JSX.Element[] = useMemo(() => { + if (!formState) { return null; } + + return columns.map((column: ColumnProps, index: number) => { + // For add operations, primary key fields should be editable + // For edit operations, primary key fields should be disabled + // Also check column/field visiibility and the disabled prop for showAddNewRow functionality + const isEditable: boolean = column.allowEdit !== false && column.field && column.visible && + (isAddOperation || !column.isPrimaryKey) && !disabled; + + // Handle undefined values properly for truly empty edit forms + // Let EditCell handle undefined values appropriately for each input type + const fieldError: string = validationErrors[column.field]; + + if (!isEditable) { + return column.visible ? ( + + + storeEditCellRef(column.field, cellRef)} + column={{ ...column, allowEdit: false }} + value={getObject(column.field, formState?.values) ?? formState?.values?.[column.field]} + rowData={internalData} + error={formState?.errors[column.field]} + onChange={handleFieldChange.bind(null, column)} + onBlur={handleFieldBlur.bind(null, column)} + disabled={disabled} + onFocus={() => { + formState?.onFocus?.(column.field); + }} + isAdd={isAddOperation} + formState={formState} + /> + + + ) : ( + + ); + } + + return column.visible ? ( + + + storeEditCellRef(column.field, cellRef)} + column={column} + value={getObject(column.field, formState?.values) ?? formState?.values?.[column.field]} + rowData={internalData} + error={formState?.errors[column.field]} + onChange={handleFieldChange.bind(null, column)} + onBlur={handleFieldBlur.bind(null, column)} + disabled={disabled} + onFocus={() => { + formState?.onFocus?.(column.field); + }} + isAdd={isAddOperation} + formState={formState} + /> + + + ) : ( + + ); + }); + }, [columns, internalData, validationErrors, isAddOperation, disabled, handleFieldChange, + handleFieldBlur, handleEnter, handleEscape, storeEditCellRef + ]); + + // Render custom edit template if provided + if (CustomTemplate) { + return ( + + +
} + validateOnChange={!(isAddOperation && editModule?.isShowAddNewRowActive) || (isAddOperation && + formState && Object.keys(formState.errors).length > 0)} + onFormStateChange={setFormState} + className={'sf-gridform' + (cssClass !== '' ? ' ' + cssClass : '')} + id={`grid-edit-form-${editRowIndex}`} + aria-label={`${isAddOperation ? 'Add' : 'Edit'} Record Form`} + role='form' + > +
+ +
+ {formState && Object.keys(formState.errors).length > 0 && ( + + )} + + + + ); + } + + useEffect(() => { + const resetShowAddNewRowForm: (event: CustomEvent) => void = (event: CustomEvent) => { + const { editData } = event.detail; + setInternalData(editData); + requestAnimationFrame(() => { + formRef.current?.reset?.(); + }); + }; + if (isAddOperation && editModule?.isShowAddNewRowActive && !editModule?.isShowAddNewRowDisabled) { + formRef.current?.element?.addEventListener('resetShowAddNewRowForm', resetShowAddNewRowForm as EventListener); + } + return () => { + formRef.current?.element?.removeEventListener?.('resetShowAddNewRowForm', resetShowAddNewRowForm as EventListener); + }; + }, [formState, internalData, formRef]); + + /** + * Handle custom Tab navigation events from EditCell components + * Enhanced to handle boundary navigation for auto-save behavior + */ + useEffect(() => { + const handleTabEvent: (event: CustomEvent) => void = (event: CustomEvent) => { + const { field, originalEvent } = event.detail; + + // First check if this is a boundary navigation that should trigger auto-save + if (originalEvent && handleTabBoundaryNavigation(originalEvent, field)) { + // Prevent the original Tab event from continuing + originalEvent.preventDefault(); + originalEvent.stopPropagation(); + editModule.nextPrevEditRowInfo.current = originalEvent; + // Boundary navigation was handled (auto-save triggered), don't continue with normal navigation + return; + } + if (isAddOperation && editModule.isShowAddNewRowActive && !formState?.errors) { + formState?.onBlur?.(field); + formRef.current.validateField(field); + } + }; + + formRef.current?.element?.addEventListener('editCellTab', handleTabEvent as EventListener); + + return () => { + formRef.current?.element?.removeEventListener('editCellTab', handleTabEvent as EventListener); + }; + }, [handleTabBoundaryNavigation]); + + /** + * Enhanced focus management for proper auto-focus behavior + * Only focus on initial mount and when starting a new edit session + */ + const hasInitialFocusAttempted: React.RefObject = useRef(false); + const focusTimeoutRef: React.RefObject = useRef(null); + + useEffect(() => { + // Reset focus attempt flag when starting a new edit session + if (isNewSessionRef.current) { + hasInitialFocusAttempted.current = false; + } + + // Only attempt focus once per edit session + if (hasInitialFocusAttempted.current) { + return undefined; + } + + hasInitialFocusAttempted.current = true; + + // Clear any existing focus timeout + if (focusTimeoutRef.current) { + clearTimeout(focusTimeoutRef.current); + } + + // Use timeout instead of requestAnimationFrame for better reliability + focusTimeoutRef.current = setTimeout(() => { + const activeElement: HTMLElement | null = document.activeElement as HTMLElement; + const isAlreadyFocusedInEdit: boolean | Element = activeElement && ( + activeElement.closest('.sf-editedrow') || + activeElement.closest('.sf-addedrow') + ); + + // Always auto-focus for new edit sessions, regardless of current focus + // This ensures proper focus behavior when clicking on different rows or starting edit + if ((isNewSessionRef.current || !isAlreadyFocusedInEdit) && !editModule?.isShowAddNewRowActive) { + focusFirstField(); + } + }, 0); // Reduced delay for better responsiveness + + return () => { + clearTimeout(focusTimeoutRef.current); + }; + }, [editRowIndex, rowUid]); // Re-run when edit session changes + + /** + * Memoized colgroup element to prevent unnecessary re-renders + * Contains column definitions for the table + */ + const colGroupContent: JSX.Element = useMemo(() => ( + + {ColElements.length ? ColElements : null} + + ), [ColElements, id, isAddOperation]); + + return rowUid ? ( + + +
} + validateOnChange={!(isAddOperation && editModule?.isShowAddNewRowActive) || (isAddOperation && + formState && Object.keys(formState.errors).length > 0)} + onFormStateChange={(args: FormState) => { + setFormState(args); + }} + className={'sf-gridform' + (cssClass !== '' ? ' ' + cssClass : '')} + id={`grid-edit-form-${editRowIndex}`} + aria-label={`${isAddOperation ? 'Add' : 'Edit'} Record Form`} + role='form' + > + + {colGroupContent} + + + {renderEditCells} + + +
+ + {formState && Object.keys(formState.errors).length > 0 && ( + + )} + + + + ) : ( + <> + ); + })); + +InlineEditForm.displayName = 'InlineEditForm'; diff --git a/components/grids/src/grid/views/editing/ToolBar.tsx b/components/grids/src/grid/views/editing/ToolBar.tsx new file mode 100644 index 0000000..f00b196 --- /dev/null +++ b/components/grids/src/grid/views/editing/ToolBar.tsx @@ -0,0 +1,381 @@ +import { Button, Color, Variant } from '@syncfusion/react-buttons'; +import { Toolbar, ToolbarItem, ToolbarSpacer } from '@syncfusion/react-navigations'; +import { JSX, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ToolbarAPI, ToolbarClickEvent, ToolbarItemConfig, ToolbarConfig } from '../../types/toolbar.interfaces'; +import { MutableGridBase } from '../../types'; +import { SelectionModel } from '../../types/selection.interfaces'; +import { editModule } from '../../types/edit.interfaces'; +import { searchModule } from '../../types/search.interfaces'; +import { useGridComputedProvider, useGridMutableProvider } from '../../contexts'; +import { IL10n } from '@syncfusion/react-base'; +import { CloseIcon, EditIcon, PlusIcon, SaveIcon, SearchIcon, TrashIcon } from '@syncfusion/react-icons'; +import { InputBase, renderClearButton, renderFloatLabelElement } from '@syncfusion/react-inputs'; + +/** + * Search Input Wrapper Component using InputBase similar to FilterBar pattern + * + * @param {Object} props - The component props + * @param {string} props.gridId - The ID of the grid + * @param {IL10n} props.localization - The localization object + * @param {searchModule} [props.searchModule] - The search module + * @param {Function} [props.handleClick] - The click handler + * @param {boolean} [props.allowKeyboard] - To allow keyboard + * @param {boolean} [props.disabled] - To allow disabled + * @returns {JSX.Element} - The rendered SearchInputWrapper component + */ +const SearchInputWrapper: React.FC<{ + gridId: string; + localization: IL10n; + searchModule?: searchModule; + handleClick?: (args: React.MouseEvent) => void; + allowKeyboard?: boolean; + disabled?: boolean; +}> = ({ gridId, localization, searchModule, handleClick, allowKeyboard, disabled }: { + gridId: string; + localization: IL10n; + searchModule?: searchModule; + handleClick?: (args: React.MouseEvent) => void; + allowKeyboard?: boolean; + disabled?: boolean; +}): JSX.Element => { + const [searchValue, setSearchValue] = useState(disabled ? '' : searchModule.searchSettings?.value || ''); + const [isFocused, setIsFocused] = useState(false); + const searchInputRef: RefObject = useRef(null); + + useEffect(() => { + setSearchValue(searchModule.searchSettings?.value); + }, [searchModule.searchSettings?.value]); + + const clearInput: () => void = useCallback((e?: React.MouseEvent) => { + // Prevent default and stop propagation if event is provided + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + + setSearchValue(''); + searchModule.search(''); + + // Ensure input gets focus but after a short delay to let events settle + setTimeout(() => { + searchInputRef.current?.focus(); + }, 0); + }, [searchModule]); + + const handleSearch: (value: string) => void = useCallback((value: string) => { + searchModule?.search(value); + }, [searchModule]); + + const handleKeyDown: (e: React.KeyboardEvent) => void = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === 'Tab' || (e.shiftKey && e.key === 'Tab')) { + if (e.key === 'Enter') { + e.preventDefault(); // Prevent default to avoid focus movement + e.stopPropagation(); // Stop propagation to avoid grid focus handlers + } + handleSearch(searchValue); + } else if (e.key === 'Escape') { + clearInput(); + } + }, [searchValue, handleSearch]); + + const handleChange: (e: React.ChangeEvent) => void = useCallback((e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }, []); + + const handleFocus: () => void = useCallback(() => { + setIsFocused(true); + }, []); + + const handleBlur: () => void = useCallback(() => { + setIsFocused(false); + }, []); + + const searchId: string = `${gridId}_searchbar`; + + return ( +
+ ) => handleClick(e)} + onChange={handleChange} + onKeyDown={allowKeyboard ? handleKeyDown : undefined} + onFocus={handleFocus} + onBlur={handleBlur} + disabled={disabled} + /> + {renderFloatLabelElement('Never', isFocused, searchValue, localization?.getConstant('searchButtonLabel'), searchId)} + {renderClearButton(searchValue, clearInput)} + ) => { + // Prevent default and stop propagation to avoid focus issues + e.preventDefault(); + e.stopPropagation(); + handleSearch(searchValue); + // Move focus back to the search input to ensure consistent UX + searchInputRef.current?.focus(); + }} + > + + +
+ ); +}; + +const rearrangeToolbar: (toolbar: (string | ToolbarItemConfig)[]) => (string | ToolbarItemConfig)[] = + (toolbar: (string | ToolbarItemConfig)[]): (string | ToolbarItemConfig)[] => { + const withoutSearch: (string | ToolbarItemConfig)[] = toolbar.filter((item: string | ToolbarItemConfig) => { + if (typeof item === 'string') { + return item !== 'Search'; + } + return item.id !== 'Search' && item.text !== 'Search'; + }); + + const hasSearch: boolean = toolbar.some((item: string | ToolbarItemConfig) => { + if (typeof item === 'string') { + return item === 'Search'; + } + return item.id === 'Search' || item.text === 'Search'; + }); + + return hasSearch ? [...withoutSearch, toolbar.find((item: string | ToolbarItemConfig) => { + if (typeof item === 'string') { + return item === 'Search'; + } + return item.id === 'Search' || item.text === 'Search'; + })!] : toolbar; + }; + +/** + * Toolbar component for the grid + * + * @param {Object} props - Toolbar props + * @param {Array} props.toolbar - Toolbar props + * @param {string} props.gridId - Toolbar props + * @param {string} props.className - Toolbar props + * @param {ToolbarAPI} props.toolbarAPI - Toolbar props + * @returns {Element} Toolbar component + */ +export const GridToolbar: React.FC = ({ + toolbar, + gridId, + className, + toolbarAPI +}: { + toolbar?: (string | ToolbarItemConfig)[], + gridId?: string, + className?: string, + toolbarAPI?: ToolbarAPI +}) => { + toolbar = rearrangeToolbar(toolbar || []); + const { serviceLocator, allowKeyboard } = useGridComputedProvider(); + const localization: IL10n = serviceLocator?.getService('localization'); + const modulesRef: React.RefObject<{ + editModule?: editModule, + selectionModule?: SelectionModel, + searchModule?: searchModule, + currentViewData?: Object[] + }> = useRef<{ + editModule?: editModule, + selectionModule?: SelectionModel, + searchModule?: searchModule, + currentViewData?: Object[] + }>({}); + + const gridContext: MutableGridBase = useGridMutableProvider(); + const { + editModule, + selectionModule, + searchModule, + currentViewData + } = gridContext; + + // Update refs without causing re-renders + modulesRef.current = { editModule, selectionModule, searchModule, currentViewData }; + + // Stable button click handler that never changes + const handleButtonClick: (itemId: string, originalEvent?: React.MouseEvent) => void = + useCallback((itemId: string, originalEvent?: React.MouseEvent) => { + const args: ToolbarClickEvent = { + item: { id: itemId }, + event: originalEvent?.nativeEvent, + cancel: false + }; + toolbarAPI.handleToolbarClick(args); + }, [toolbarAPI.handleToolbarClick]); // Only depend on the stable function + + // Use disabledItems from the toolbar API for rendering + const disabledItems: Set = toolbarAPI.disabledItems; + + // Create toolbar items only once based on configuration + const renderToolbarItems: React.ReactElement>[] = useMemo(() => { + const items: React.ReactElement[] = []; + + toolbar.forEach((item: string | ToolbarItemConfig, index: number) => { + let itemConfig: ToolbarItemConfig; + + if (typeof item === 'string') { + // Predefined items + switch (item) { + case 'Add': + itemConfig = { + id: `${gridId}_add`, + title: localization?.getConstant('addButtonLabel'), + text: localization?.getConstant('addButtonLabel'), + icon: , + disabled: disabledItems.has(`${gridId}_add`) + }; + break; + case 'Edit': + itemConfig = { + id: `${gridId}_edit`, + title: localization?.getConstant('editButtonLabel'), + text: localization?.getConstant('editButtonLabel'), + icon: , + disabled: disabledItems.has(`${gridId}_edit`) + }; + break; + case 'Update': + itemConfig = { + id: `${gridId}_update`, + title: localization?.getConstant('updateButtonLabel'), + text: localization?.getConstant('updateButtonLabel'), + icon: , + disabled: disabledItems.has(`${gridId}_update`) + }; + break; + case 'Delete': + itemConfig = { + id: `${gridId}_delete`, + title: localization?.getConstant('deleteButtonLabel'), + text: localization?.getConstant('deleteButtonLabel'), + icon: , + disabled: disabledItems.has(`${gridId}_delete`) + }; + break; + case 'Cancel': + itemConfig = { + id: `${gridId}_cancel`, + title: localization?.getConstant('cancelButtonLabel'), + text: localization?.getConstant('cancelButtonLabel'), + icon: , + disabled: disabledItems.has(`${gridId}_cancel`) + }; + break; + case 'Search': + items.push(); // condition based need to handle adding spacer. + // Search functionality using InputBase component similar to FilterBar pattern + items.push( + + ) => handleButtonClick(`${gridId}_search`, args)} + localization={localization} + searchModule={modulesRef.current.searchModule} + allowKeyboard={allowKeyboard} + disabled={disabledItems.has(`${gridId}_search`)} + /> + + ); + return; + } + } else { + // For custom items, merge the existing config with the disabled state + itemConfig = { + ...item, + disabled: disabledItems.has(item.id) || item.disabled + }; + } + + // Create toolbar button + items.push( + + + + ); + }); + + return items; + }, [toolbar, gridId, handleButtonClick, disabledItems]); // Include disabledItems in dependencies + + // Only do initial refresh once when toolbar is ready + useEffect(() => { + if (toolbarAPI.isRendered) { + // Single initial refresh after a short delay + toolbarAPI.refreshToolbarItems(); + } + return undefined; + }, [toolbarAPI.isRendered]); // Only depend on isRendered + + // Add event-based toolbar refresh to prevent reactive dependencies + useEffect(() => { + if (!toolbarAPI.isRendered) { + return undefined; + } + + // Create a custom event listener for toolbar refresh + const handleToolbarRefresh: () => void = () => { + toolbarAPI.refreshToolbarItems(); + }; + + // Listen for selection changes via custom events instead of reactive dependencies + const handleSelectionChange: () => void = () => { + setTimeout(handleToolbarRefresh, 0); + }; + + const handleEditStateChange: () => void = () => { + setTimeout(handleToolbarRefresh, 0); + }; + + // Add event listeners to the grid element for state changes + const toolbarElement: HTMLElement | null = toolbarAPI.getToolbar(); + const gridElement: HTMLElement | null = toolbarElement?.closest('.sf-grid'); + gridElement?.addEventListener('selectionChanged', handleSelectionChange); + gridElement?.addEventListener('editStateChanged', handleEditStateChange); + gridElement?.addEventListener('toolbarRefresh', handleToolbarRefresh); + + return () => { + gridElement?.removeEventListener('selectionChanged', handleSelectionChange); + gridElement?.removeEventListener('editStateChanged', handleEditStateChange); + gridElement?.removeEventListener('toolbarRefresh', handleToolbarRefresh); + }; + }, [toolbarAPI.isRendered, toolbarAPI.refreshToolbarItems, gridId]); + + return ( + ) => { + if ((args.target as HTMLElement)?.closest('.sf-toolbar-item button')?.id) { + handleButtonClick((args.target as HTMLElement)?.closest('.sf-toolbar-item button')?.id, args); + } + }} + > + {renderToolbarItems} + + ); +}; diff --git a/components/grids/src/grid/views/editing/ValidationTooltips.tsx b/components/grids/src/grid/views/editing/ValidationTooltips.tsx new file mode 100644 index 0000000..c345ee4 --- /dev/null +++ b/components/grids/src/grid/views/editing/ValidationTooltips.tsx @@ -0,0 +1,104 @@ +import { FormState } from '@syncfusion/react-inputs'; +import { EditCellRef, ValidationTooltipsProps } from '../../types/edit.interfaces'; +import { RefObject, useEffect, useState } from 'react'; +import { Tooltip } from '@syncfusion/react-popups'; +import { useGridComputedProvider, useGridMutableProvider } from '../../contexts'; +import { GridRef } from '../../types/grid.interfaces'; +import { MutableGridSetter } from '../../types/interfaces'; +import { formatUnit } from '@syncfusion/react-base'; +import { parseUnit } from '../../utils'; + +export const ValidationTooltips: React.FC = ({ formState, editCellRefs }: { + formState: FormState | null, + editCellRefs?: React.RefObject<{ [field: string]: EditCellRef }> +}) => { + const grid: Partial & Partial = useGridComputedProvider(); + const { editModule } = useGridMutableProvider(); + const [tooltipTargets, setTooltipTargets] = useState>>({}); + const [activeTooltips, setActiveTooltips] = useState>(new Set()); + + // Create container refs for all error fields - moved outside the render loop + const [containerRefs, setContainerRefs] = useState>>({}); + + // Create proper React refs for tooltip targets and manage tooltip visibility + // Target the table cell (td) instead of the input element for proper arrow positioning + useEffect(() => { + const newTargets: Record> = {}; + const newActiveTooltips: Set = new Set(); + const newContainerRefs: Record> = {}; + + Object.keys(formState.errors).forEach((field: string) => { + // Target the table cell (td) containing the input for proper tooltip positioning + let targetElement: HTMLElement | null = null; + + // First, try to find the input element + const inputElement: HTMLElement = editModule?.isShowAddNewRowActive && editModule?.isShowAddNewRowDisabled ? + grid.element?.querySelector(`.sf-editedrow [id="grid-edit-${field}"]`) as HTMLElement : + grid.element?.querySelector(`[id="grid-edit-${field}"]`) as HTMLElement; + + // Once we have the input element, find its containing table cell (td) + // This ensures the tooltip arrow targets the cell, not the input itself + // Find the closest table cell (td) that contains this input + targetElement = inputElement?.closest('td.sf-rowcell, td.sf-edit-cell') as HTMLElement; + + const targetElementRef: React.RefObject = { current: targetElement } as React.RefObject; + newTargets[field as string] = targetElementRef; + newActiveTooltips.add(field); + + // Create or reuse container ref for this field + newContainerRefs[field as string] = containerRefs[field as string] || { current: null }; + }); + + setTooltipTargets(newTargets); + setActiveTooltips(newActiveTooltips); + setContainerRefs(newContainerRefs); + }, [formState?.errors, editCellRefs]); + + // Cleanup effect for container refs + useEffect(() => { + return () => { + Object.values(containerRefs).forEach((ref: RefObject) => { + if (ref.current) { + ref.current = null; + } + }); + }; + }, [containerRefs]); + + return ( + <> + {Object.entries(formState.errors).map(([field, error]: [string, string]) => { + const containerRef: RefObject = containerRefs[field as string]; + const targetRef: RefObject = tooltipTargets[field as string]; + const isActive: boolean = activeTooltips.has(field); + + if (!targetRef || !targetRef.current || !isActive || !containerRef) { + return null; + } + + return ( + <> +
+ {error}} + target={targetRef} + container={containerRef} + style={{ + maxWidth: formatUnit(targetRef.current?.getBoundingClientRect?.()?.width - + (parseUnit(getComputedStyle(targetRef.current)?.paddingRight) * 2)) + }} + position="BottomCenter" + opensOn="Custom" + open={true} + className={`sf-griderror sf-validation-error-${field}`} + windowCollision={true} + /> + + ); + })} + + ); +}; diff --git a/components/grids/src/grid/views/index.ts b/components/grids/src/grid/views/index.ts new file mode 100644 index 0000000..bdaae72 --- /dev/null +++ b/components/grids/src/grid/views/index.ts @@ -0,0 +1,15 @@ +export * from './Render'; +export * from './editing/ToolBar'; +export * from './HeaderPanel'; +export * from './HeaderTable'; +export * from './HeaderRows'; +export * from './FilterBar'; +export * from './editing/ConfirmDialog'; +export * from './editing/InlineEditForm'; +export * from './editing/EditCell'; +export * from './editing/ValidationTooltips'; +export * from './ContentPanel'; +export * from './ContentTable'; +export * from './ContentRows'; +export * from './Aggregate'; +export * from './PagerPanel'; diff --git a/components/grids/src/index.ts b/components/grids/src/index.ts new file mode 100644 index 0000000..d24d1bd --- /dev/null +++ b/components/grids/src/index.ts @@ -0,0 +1 @@ +export * from './grid'; diff --git a/components/popups/styles/tooltip/_all.scss b/components/grids/styles/grid/_all.scss similarity index 100% rename from components/popups/styles/tooltip/_all.scss rename to components/grids/styles/grid/_all.scss diff --git a/components/grids/styles/grid/_layout.scss b/components/grids/styles/grid/_layout.scss new file mode 100644 index 0000000..a0fd9c2 --- /dev/null +++ b/components/grids/styles/grid/_layout.scss @@ -0,0 +1,727 @@ +@mixin wrap-styles { + height: Auto; + line-height: $grid-rowcell-wrap-height; + overflow-wrap: break-word; + text-overflow: clip; + white-space: normal; + word-wrap: break-word; +} + +@mixin grid-top-bottom-padding($bottom, $top) { + padding-bottom: $bottom; + padding-top: $top; +} + +@mixin grid-margin-padding($margin, $padding) { + margin: $margin; + padding: $padding; +} + +@mixin float-with-margin($float, $margin) { + float: $float; + margin: $margin; +} + +@mixin grid-padding-left-right($left, $right) { + padding-left: $left; + padding-right: $right; +} + +@mixin grid-line-height-padding-styles($lheight, $padding) { + line-height: $lheight; + padding: $padding; +} + +@mixin grid-font-size-weight-styles($size, $weight) { + font-size: $size; + font-weight: $weight; +} + +@mixin grid-border-style-weight($style, $width) { + border-style: $style; + border-width: $width; +} + +@mixin border-and-font($style, $width, $size, $weight) { + @include grid-border-style-weight($style, $width); + @include grid-font-size-weight-styles($size, $weight); +} + +@mixin grid-border-style-width-font-size-weight($style, $width, $size, $weight) { + @include grid-border-style-weight($style, $width); + @include grid-font-size-weight-styles($size, $weight); +} + +@include export-module('grid-layout') { + #{&}.sf-grid { + @include grid-border-style-weight(none $grid-border-style $grid-border-style, $grid-border-width); + border-radius: $grid-border-radius; + display: block; + font-family: $grid-content-font-family; + font-size: $grid-content-text-size; + height: auto; + position: relative; + + .sf-toolbar.sf-sticky, + .sf-gridheader.sf-sticky { + position: sticky; + z-index: 10; + } + + .sf-gridheader { + user-select: none; + border-bottom-style: $grid-border-style; + border-bottom-width: $grid-border-width; + border-top-style: $grid-border-style; + border-top-width: $grid-border-width; + + .sf-headercell { + user-select: none; + } + + thead .sf-icons:not(.sf-check, .sf-stop) { + font-size: $grid-icon-font-size; + } + + tr:first-child th { + border-top: 0 none; + } + + .sf-rightalign { + .sf-sortfilterdiv { + @include float-with-margin(left, $grid-sort-right-align-margin); + } + + .sf-sortnumber { + @include float-with-margin(left, $grid-sort-number-right-align-margin); + } + } + + .sf-centeralign.sf-headercell[aria-sort = 'none'] .sf-headercelldiv, + .sf-centeralign.sf-headercell:not([aria-sort]) .sf-headercelldiv { + padding-right: $grid-headercell-sort-padding-right; + } + + .sf-sortfilter { + .sf-rightalign .sf-headercelldiv { + padding: $grid-headercell-right-align-padding; + margin-left: 8px; + } + + .sf-headercelldiv { + padding: $grid-headercell-padding; + } + } + } + + .sf-gridcontent .sf-normaledit .sf-rowcell.sf-lastrowadded { + border-bottom: $grid-border-width $grid-border-style $grid-border-color; + border-top: 0 none $grid-border-color; + } + + .sf-gridcontent table tbody .sf-normaledit .sf-rowcell { + border-top: $grid-border-width $grid-border-style; + } + + .sf-toolbar { + border-bottom: 0; + border-left: 0; + border-right: 0; + border-top: $grid-toolbar-border $grid-border-color; + border-radius: 0; + } + + .sf-toolbar-items { + .sf-toolbar-item.sf-search-wrapper { + @include grid-top-bottom-padding($grid-toolbar-search-wrapper-padding-bottom, $grid-toolbar-search-wrapper-padding-top); + .sf-search:focus { + opacity: $grid-toolbar-search-bar-text-opacity; + } + + .sf-search::placeholder { + color: $grid-toolbar-searchwrapper-text-color; + } + + .sf-search { + margin-bottom: $grid-toolbar-search-margin-bottom; + opacity: 1; + width: $grid-toolbar-search-width; + &.sf-input-focus { + opacity: 1; + } + .sf-search-icon { + min-width: $grid-toolbar-search-icon-min-width; + svg { + margin-bottom: 3px; + } + } + } + } + } + + .sf-headercell.sf-defaultcursor { + cursor: default; + } + + .sf-table { + border-collapse: separate; + table-layout: fixed; + width: 100%; + } + + .sf-headercelldiv { + border: 0 none; + display: block; + @include grid-font-size-weight-styles($grid-header-font-size, $grid-header-font-weight); + height: $grid-header-height; + @include grid-line-height-padding-styles($grid-headercell-line-height, $grid-headercell-padding); + margin: $grid-headercell-margin; + overflow: hidden; + text-align: left; + text-transform: $grid-header-text-transform; + user-select: none; + } + + .sf-rightalign, + .sf-leftalign, + .sf-centeralign { + .sf-headercelldiv { + padding: 0 .4em; + } + } + + .sf-headercell.sf-templatecell .sf-headercelldiv { + height: auto; + min-height: $grid-header-height; + } + + .sf-headercell.sf-mousepointer { + cursor: pointer; + } + + & .sf-gridcontent { + tr:first-child td { + border-top: 0 none; + } + } + + .sf-headercell { + @include border-and-font($grid-border-style, $grid-header-border-width, $grid-header-font-size, $grid-header-font-weight); + height: $grid-headercell-height; + overflow: hidden; + padding: $grid-header-padding-top $grid-header-padding $grid-header-padding-bottom; + position: relative; + text-align: left; + letter-spacing: 0.24px; + } + + .sf-rowcell { + @include grid-border-style-weight($grid-border-style, $grid-rowcell-border-width); + display: table-cell; + font-size: $grid-content-text-size; + @include grid-line-height-padding-styles($grid-rowcell-line-height, $grid-content-padding $grid-content-right-padding); + overflow: hidden; + vertical-align: middle; + white-space: nowrap; + width: auto; + letter-spacing: 0.24px; + } + + .sf-rightalign { + &.sf-rowcell, + &.sf-summarycell, + &.sf-headercell .sf-headercelldiv { + text-align: right; + } + } + + .sf-leftalign { + &.sf-rowcell, + &.sf-summarycell, + &.sf-headercell .sf-headercelldiv { + text-align: left; + } + } + + .sf-centeralign { + &.sf-rowcell, + &.sf-summarycell, + &.sf-headercell .sf-headercelldiv { + text-align: center; + } + } + + .sf-justifyalign { + &.sf-rowcell, + &.sf-summarycell, + &.sf-headercell .sf-headercelldiv { + text-align: justify; + } + } + + &:not(.sf-grid-min-height) tr.sf-insertedrow .sf-rowcell:empty, + .sf-row.sf-emptyrow { + height: $grid-rowcell-line-height + $grid-content-padding + $grid-content-padding + 1; + } + + .sf-editedrow, + .sf-addedrow { + .sf-input-group input.sf-input, + .sf-input-group.sf-control-wrapper input.sf-input { + min-height: unset; + } + } + + &:not(.sf-grid-min-height) .sf-gridcontent { + tr td:first-child:empty, + tr.sf-row .sf-rowcell:empty { + height: $grid-rowcell-line-height + $grid-content-padding + $grid-content-padding; + } + } + + .sf-summarycell { + @include border-and-font(solid, 1px 0 0, $grid-summary-cell-font-size, $grid-header-font-weight); + height: auto; + @include grid-line-height-padding-styles($grid-summary-cell-line-height, $grid-content-padding $grid-content-right-padding); + white-space: normal; + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; + letter-spacing: 0.24px; + &.sf-lastrowcell { + border-bottom-width: 1px; + } + } + + .sf-rowcell.sf-lastrowcell { + border-bottom-width: 1px; + } + + &.sf-bothlines { + .sf-rowcell.sf-lastrowcell { + border-bottom-width: 1px; + } + .sf-rowcell { + border-width: $grid-rowcell-both-border-width; + } + .sf-filterbarcell { + border-width: $grid-filter-cell-both-border-width; + border-top-width: 1px; + } + } + + &.sf-gridheader th[rowspan] { + padding-bottom: 13px; + vertical-align: bottom; + } + + .sf-emptyrow td { + letter-spacing: 0.24px; + font-size: $grid-content-text-size; + @include grid-line-height-padding-styles($grid-rowcell-line-height, .7em); + } + + &.sf-bothlines, + .sf-filterbartable { + .sf-headercell { + border-width: $grid-headercell-both-border-width; + } + } + + .sf-filterbartable .sf-filterbarcell { + border-width: $grid-filter-cell-both-border-width; + border-top-width: 1px; + } + + &:not(.sf-rtl) tr { + & td:first-child, + & th.sf-headercell:first-child, + & th.sf-filterbarcell:first-child { + border-left-width: 0; + } + } + + .sf-hide, + th.sf-headercell.sf-hide { + display: none; + } + + .sf-rowcell, + .sf-gridcontent, + .sf-gridheader, + .sf-headercontent, + .sf-gridfooter, + .sf-summarycontent { + overflow: hidden; + vertical-align: middle; + } + + .sf-sortfilterdiv { + float: right; + height: $grid-sort-height; + @include grid-margin-padding($grid-sort-mg, $grid-sort-padding); + width: $grid-sort-width; + } + + .sf-sortnumber { + border-radius: $grid-sort-number-border-radius; + display: inline-block; + float: right; + text-align: center; + font-size: $grid-sort-number-font-size; + height: $grid-sort-number-size; + line-height: $grid-sort-number-line-height; + margin: $grid-sort-number-margin; + width: $grid-sort-number-size; + } + + &.sf-verticallines { + .sf-rowcell, + .sf-headercell, + .sf-filterbarcell { + border-width: 0 0 0 $grid-border-width; + } + } + + &.sf-hidelines { + .sf-rowcell, + .sf-headercell, + .sf-filterbarcell { + border-width: 0; + } + } + + &.sf-horizontallines { + .sf-headercell { + border-width: 0; + } + + .sf-rowcell, + .sf-filterbarcell { + border-width: $grid-border-width 0 0; + } + } + + &.sf-horizontallines, + &.sf-verticallines, + &.sf-hidelines { + .sf-rowcell.sf-lastrowcell { + border-bottom-width: $grid-border-width; + } + } + + .sf-filterbarcell { + border-collapse: collapse; + @include grid-border-style-weight($grid-border-style, $grid-filterbar-cell-border-width); + cursor: default; + height: $grid-filterbar-cell-height; + overflow: hidden; + padding: $grid-filterbar-cell-padding; + vertical-align: bottom; + } + + .sf-searchclear { + position: relative; + float: right; + } + + .sf-filterdiv, + .sf-fltrtempdiv { + padding: $grid-filter-padding; + position: relative; + text-align: center; + width: 100%; + } + + .sf-pager { + border-bottom: transparent; + border-left: transparent; + border-right: transparent; + } + + .sf-footerpadding { + @include grid-padding-left-right(0, 14px); + + .sf-lastsummarycell { + border-left: none; + border-right: 1px solid $grid-border-color; + } + } + + .sf-content:not(.sf-tooltip-content) { + -webkit-overflow-scrolling: touch; + overflow-x: auto; + overflow-y: scroll; + position: relative; + } + + .sf-headercontent { + @include grid-border-style-weight(solid, 0); + } + + .sf-griderror label { + display: inline !important; + } + + .sf-tooltip-wrap.sf-griderror { + z-index: 1000; + } + + .sf-normaledit { + border-top: 0; + padding: 0; + .sf-rowcell { + @include grid-top-bottom-padding($grid-edit-cell-padding, $grid-edit-top-cell-padding); + } + } + + &:not(.sf-row-responsive) .sf-gridcontent tr.sf-row:first-child .sf-rowcell { + border-top: 0; + } + + .sf-row { + .sf-input-group .sf-input.sf-field, + .sf-input-focus .sf-input.sf-field { + font-size: $grid-content-text-size; + @include grid-top-bottom-padding($grid-edit-input-padding-bottom, $grid-edit-input-padding-top); + } + + .sf-input-group { + margin-bottom: $grid-edit-input-margin; + margin-top: $grid-edit-input-margin-top; + vertical-align: middle; + line-height: 28.5px; + } + } + + .sf-content.sf-mac-safari::-webkit-scrollbar { + width: 7px; + } + + .sf-content.sf-mac-safari::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, .5); + border-radius: 4px; + } + + .sf-rowcell, + .sf-headercelldiv { + text-overflow: ellipsis; + white-space: nowrap; + } + + &.sf-wrap { + .sf-gridheader { + .sf-rightalign .sf-sortnumber { + margin: $grid-wrap-sort-number-margin; + } + + .sf-sortnumber { + margin: 0 16px 0 2px; + } + + .sf-sortfilterdiv { + margin: $grid-wrap-sort-icon-margin; + } + } + + .sf-rowcell, + .sf-headercelldiv { + @include wrap-styles; + } + + .sf-columnheader { + .sf-icon-group::before { + display: inline-block; + } + } + } + + + &.sf-rtl { + .sf-headercell { + border-width: $grid-rtl-headercell-border-width; + + .sf-headercelldiv { + padding: $grid-rtl-headercell-padding; + + .sf-sortnumber { + @include float-with-margin(left, $grid-rtl-sort-number-margin); + } + } + + .sf-filterbarcell input { + border-width: $grid-filterbar-input-border-width; + } + + .sf-sortfilterdiv { + @include float-with-margin(left, $grid-rtl-sort-cell-margin); + } + + &.sf-leftalign { + .sf-sortfilterdiv { + @include float-with-margin(right, $grid-sort-margin); + } + + .sf-headercelldiv { + padding: 0 25px 0 .3em; + + .sf-sortnumber { + @include float-with-margin(right, $grid-sort-right-margin); + } + } + } + + &.sf-rightalign { + .sf-sortnumber { + @include float-with-margin(left, $grid-rtl-sort-number-right-align-margin); + } + } + } + + .sf-gridheader { + .sf-rightalign .sf-sortfilterdiv { + margin: $grid-rtl-sort-cell-right-align-margin; + } + + .sf-centeralign.sf-headercell[aria-sort = 'none'] .sf-headercelldiv, + .sf-centeralign.sf-headercell:not([aria-sort]) .sf-headercelldiv { + padding-left: $grid-headercell-sort-padding-right; + } + } + + .sf-rowcell, + .sf-filterbarcell { + border-width: $grid-rowcell-border-width; + } + + .sf-lastrowcell { + border-width: $grid-border-width $grid-border-width $grid-border-width 0; + } + + tr td:first-child, + tr th:first-child { + border-left-width: $grid-rtl-header-first-cell-border-left; + } + + &.sf-bothlines, + .sf-filterbartable { + .sf-headercell { + border-width: $grid-headercell-both-border-width; + } + } + + .sf-filterbartable .sf-filterbarcell { + border-width: $grid-filter-cell-both-border-width; + } + + &.sf-bothlines { + .sf-filterbarcell, + .sf-rowcell, + .sf-rowcell.sf-lastrowcell { + border-width: $grid-filter-cell-both-border-width; + } + } + + &.sf-verticallines { + .sf-rowcell, + .sf-filterbarcell { + border-width: 1px 0 0 $grid-border-width; + } + } + + &.sf-hidelines { + .sf-rowcell, + .sf-headercell, + .sf-filterbarcell { + border-width: 0; + } + } + + &.sf-horizontallines { + .sf-rowcell, + .sf-filterbarcell { + border-width: $grid-border-width 0 0; + } + } + + &.sf-horizontallines, + &.sf-verticallines, + &.sf-hidelines { + .sf-rowcell.sf-lastrowcell { + border-bottom-width: $grid-border-width; + } + } + + .sf-searchclear, + .sf-cc-searchdiv span.sf-ccsearch-icon.sf-icons { + float: left; + } + + .sf-footerpadding { + @include grid-padding-left-right(14px, 0); + + tr.sf-summaryrow td.sf-lastsummarycell:last-child { + border-right: none; + border-left: 1px solid $grid-border-color; + } + } + + &.sf-wrap .sf-rightalign .sf-sortnumber { + margin: $grid-rtl-wrap-sort-number-margin; + } + &.sf-wrap .sf-sortnumber { + margin: 0 5px 0 7px; + } + } + + &.sf-wrap .sf-columnheader, + .sf-columnheader.sf-wrap { + .sf-headercelldiv { + @include wrap-styles; + margin-bottom: $grid-text-wrap-margin-bottom; + margin-top: $grid-text-wrap-margin-top; + } + + .sf-sortfilterdiv { + margin: $grid-header-wrap-sort-filter-margin; + } + + .sf-rightalign, + .sf-leftalign { + .sf-sortfilterdiv { + margin: $grid-header-wrap-right-align-sort-margin; + } + } + } + + .sf-gridcontent.sf-wrap .sf-rowcell { + @include wrap-styles; + } + } +} + +#{&}.sf-grid-min-height { + .sf-rowcell { + line-height: 0; + @include grid-top-bottom-padding(0, 0); + } + + .sf-gridheader { + .sf-headercell, + .sf-headercell .sf-headercelldiv:not(.sf-sort-icon) { + height: auto; + } + } + + .sf-summarycell { + @include grid-line-height-padding-styles(normal, 0 8px); + } +} + +@-moz-document url-prefix() { + #{&}.sf-grid-min-height { + .sf-rowcell { + line-height: normal; + } + } +} \ No newline at end of file diff --git a/components/grids/styles/grid/_material3-definition.scss b/components/grids/styles/grid/_material3-definition.scss new file mode 100644 index 0000000..1b89fad --- /dev/null +++ b/components/grids/styles/grid/_material3-definition.scss @@ -0,0 +1,119 @@ +$grid-content-padding: 13px !default; +$grid-icon-color: $icon-color !default; +$grid-border-style: solid !default; +$grid-content-font-family: $font-family !default; +$grid-header-font-size: 14px !default; +$grid-header-text-color: $content-text-color-alt1 !default; +$grid-text-color: $content-text-color !default; +$grid-sort-number-bg: $content-bg-color-alt2 !default; +$grid-sort-number-text-color: $content-text-color-alt1 !default; +$grid-table-bg-color: rgba($content-bg-color) !default; +$grid-row-selection-color: $table-bg-color-selected !default; +$grid-row-selection-hover-bg-color: $table-bg-color-selected-hover !default; +$grid-text-wrap-margin-bottom: 2px !default; +$grid-text-wrap-margin-top: 0 !default; +$grid-header-padding-bottom: 12px !default; +$grid-header-padding-top: 12px !default; +$grid-headercell-line-height: 20px !default; +$grid-filterbar-cell-text-indent: 1px !default; +$grid-rtl-header-first-cell-border-left: 0 !default; +$grid-toolbar-border: 1px solid !default; +$grid-filter-padding: 0 !default; +$grid-sort-number-border-radius: 65% !default; +$grid-wrap-sort-number-margin: 0 2px 0 0 !default; +$grid-wrap-sort-icon-margin: -9px 10px !default; +$grid-rtl-headercell-border-width: 0 !default; +$grid-rtl-wrap-sort-number-margin: 0 5px 0 0 !default; +$grid-device-headercell-padding: 6px 12px 6px !default; +$grid-device-header-first-cell-padding: 6px 12px 6px 16px !default; +$grid-device-header-last-cell-padding: 6px 16px 6px 12px !default; +$grid-device-rowcell-padding: 8px 12px !default; +$grid-device-row-first-cell-padding: 8px 12px 8px 16px !default; +$grid-device-row-last-cell-padding: 8px 16px 8px 12px !default; +$grid-device-filterbar-cell-padding: 8px 12px !default; +$grid-device-filterbar-first-cell-padding: 8px 12px 8px 16px !default; +$grid-device-filterbar-last-cell-padding: 8px 16px 8px 12px !default; +$grid-filter-padding: 2px !default; +$grid-rtl-filter-padding: 2px !default; +$grid-rtl-filter-float: left !default; +$grid-rtl-headercell-filter-icon-padding: 0 1.6em 0 10px !default; +$grid-row-bg-color: rgba($content-bg-color) !default; +$grid-header-bg-color: $content-bg-color-alt1 !default; +$grid-headercell-both-border-width: 0 0 0 1px !default; +$grid-headercell-margin: -7px -7px -7px -5px !default; +$grid-icon-font-size: 16px !default; +$grid-border-width: 1px !default; +$grid-header-padding: 12px !default; +$grid-header-wrap-sort-filter-margin: -29px 10px !default; +$grid-headercell-right-align-padding: 0 8px !default; +$grid-headercell-height: 48px !default; +$grid-filterbar-cell-height: 58px !default; +$grid-header-font-weight: 500 !default; +$grid-filter-cell-both-border-width: 1px 0 1px 1px !default; +$grid-border-color: rgba($border-light) !default; +$grid-rowcell-line-height: 21px !default; +$grid-hover-content-text-color: rgba($content-text-color-hover) !default; +$grid-content-bg-color: rgba($content-bg-color) !default; +$grid-row-selection-bg-color: $table-bg-color-selected !default; +$grid-hover-bg-color: $content-bg-color-hover !default; +$grid-content-text-size: 14px !default; +$grid-header-height: 20px !default; +$grid-rowcell-border-width: 1px 0 0 !default; +$grid-content-right-padding: 12px !default; +$grid-toolbar-search-margin-bottom: 0 !default; +$grid-toolbar-search-width: 160px !default; +$grid-toolbar-search-icon-min-width: 32px !default; +$grid-toolbar-search-clear-icon-min-width: 32px !default; +$grid-toolbar-search-clear-icon-font-size: 16px !default; +$grid-toolbar-search-clear-icon-padding: 0 !default; +$grid-toolbar-search-clear-icon-margin-right: 0 !default; +$grid-rtl-toolbar-search-clear-icon-padding: 0 !default; +$grid-rtl-toolbar-search-clear-icon-margin: 0 !default; +$grid-toolbar-search-wrapper-padding-bottom: 3px !default; +$grid-toolbar-search-wrapper-padding-top: 3px !default; +$grid-summary-cell-font-size: 14px !default; +$grid-filterbar-cell-border-width: 1px 0 0 !default; +$grid-filterbar-cell-padding: 6px 12px !default; +$grid-filterbar-input-border-width: 0 !default; +$grid-border-radius: 1px !default; +$grid-summary-row-bg: $content-bg-color-alt1 !default; +$grid-sort-width: 20px !default; +$grid-sort-height: 20px !default; +$grid-sort-number-margin: 0 4px 0 12px !default; +$grid-sort-number-right-align-margin: 0 2px 0 -2px !default; +$grid-sort-number-line-height: 20px !default; +$grid-sort-right-align-margin: -19px 8px -12px -6px !default; +$grid-sort-mg: -19px -4px -12px 8px !default; +$grid-sort-padding: 2px !default; +$grid-rowcell-both-border-width: 1px 0 0 1px !default; +$grid-summary-cell-line-height: 21px !default; +$grid-validation-error-text-color: rgba($error) !default; +$grid-validation-error-bg-color: rgba($error-container) !default; +$grid-edit-input-padding-bottom: 1px !default; +$grid-edit-input-padding-top: 2px !default; +$grid-edit-input-margin: 3px !default; +$grid-edit-top-cell-padding: 0 !default; +$grid-edit-cell-padding: 0 !default; +$grid-rtl-sort-number-margin: 0 8px 0 4px !default; +$grid-rtl-sort-number-right-align-margin: 0 8px 0 2px !default; +$grid-rtl-sort-cell-margin: -19px 8px -12px -2px !default; +$grid-rtl-sort-cell-right-align-margin: -19px 8px -12px -2px !default; +$grid-rtl-headercell-border-width: 0 !default; +$grid-rtl-headercell-padding: 0 .4em 0 2.2em !default; +$grid-headercell-sort-padding-right: 8px !default; +$grid-headercell-padding: 0 1.4em 0 .4em !default; +$grid-filterbar-border-radius: 4px !default; +$grid-header-border-width: 0 !default; +$grid-edit-input-margin-top: 0 !default; +$grid-filterbar-cell-text-input: 32px !default; +$grid-cell-focus-shadow: 0 0 0 1px rgba($primary) inset !default; +$grid-header-wrap-right-align-sort-margin: -29px -5px !default; +$grid-rowcell-wrap-height: 21px !default; +$grid-header-text-transform: none !default; +$grid-sort-number-size: 20px !default; +$grid-sort-number-font-size: 14px !default; +$grid-toolbar-searchwrapper-text-color: rgba($placeholder-text-color) !default; +$grid-toolbar-search-bar-text-opacity: 1 !default; +$grid-row-boolean-cell-color: rgba($primary-text-color) !default; +$grid-sort-margin: -19px 2px !default; +$grid-sort-right-margin: 0 4px 0 4px !default; diff --git a/components/grids/styles/grid/_theme.scss b/components/grids/styles/grid/_theme.scss new file mode 100644 index 0000000..8249b11 --- /dev/null +++ b/components/grids/styles/grid/_theme.scss @@ -0,0 +1,92 @@ +@mixin bgcolor-color-styles($bgcolor, $color) { + background: $bgcolor; + color: $color; +} + +@include export-module('grid-theme') { + + #{&}.sf-grid{ + border-color: $grid-border-color; + + .sf-content:not(.sf-tooltip-content) { + background-color: $grid-row-bg-color; + } + + .sf-table { + background-color: $grid-header-bg-color; + } + + .sf-focused { + box-shadow: $grid-cell-focus-shadow; + } + + .sf-gridheader { + @include bgcolor-color-styles($grid-header-bg-color, $grid-header-text-color); + border-bottom-color: $grid-border-color; + border-top-color: $grid-border-color; + } + + .sf-gridcontent tr:first-child td { + border-top-color: transparent; + } + + .sf-verticallines th, + .sf-filterbarcell, + .sf-headercell, + .sf-summarycell, + .sf-headercontent, + .sf-rowcell, + &.sf-rtl .sf-default.sf-verticallines th:last-child, + .sf-emptyrow.sf-show-added-row .sf-lastrowcell { + border-color: $grid-border-color; + } + + .sf-headercontent.sf-headerborder { + border-right-color: transparent; + } + + &.sf-rtl .sf-headercontent.sf-headerborder { + border-left-color: transparent; + } + + .sf-gridfooter { + background: $grid-summary-row-bg; + } + + .sf-rowcell, + .sf-summarycell, + .sf-emptyrow { + color: $grid-text-color; + } + + &.sf-gridhover .sf-row:not(.sf-editedrow):hover { + .sf-rowcell:not(.sf-active) { + @include bgcolor-color-styles($grid-hover-bg-color, $grid-hover-content-text-color); + } + + .sf-rowcell { + @include bgcolor-color-styles($grid-row-selection-hover-bg-color, $grid-text-color); + } + } + + .sf-sortnumber { + @include bgcolor-color-styles($grid-sort-number-bg, $grid-sort-number-text-color); + } + + td.sf-active { + @include bgcolor-color-styles($grid-row-selection-bg-color, $grid-text-color); + } + + .sf-gridcontent table tbody .sf-normaledit .sf-rowcell { + border-top-color: $grid-border-color; + } + } + + .sf-tooltip-wrap.sf-griderror { + background-color: $grid-validation-error-bg-color; + border-color: $grid-validation-error-bg-color; + .sf-tooltip-arrow-outer-top, .sf-tooltip-arrow-outer-left, .sf-tooltip-arrow-outer-bottom, .sf-tooltip-arrow-outer-right { + border-bottom-color: $grid-validation-error-bg-color; + } + } +} diff --git a/components/grids/styles/grid/material3.scss b/components/grids/styles/grid/material3.scss new file mode 100644 index 0000000..5e1a144 --- /dev/null +++ b/components/grids/styles/grid/material3.scss @@ -0,0 +1,15 @@ +@import '../../base/themes/material3.scss'; +@import '../../inputs/input/material3-definition.scss'; +@import '../../inputs/numerictextbox/material3-definition.scss'; +@import '../../buttons/button/material3-definition.scss'; +@import '../../buttons/radio-button/material3-definition.scss'; +@import '../../buttons/check-box/material3-definition.scss'; +@import '../../dropdowns/drop-down-list/material3-definition.scss'; +@import '../../popups/spinner/material3-definition.scss'; +@import '../../popups/tooltip/material3-definition.scss'; +@import '../../navigations/toolbar/material3-definition.scss'; +@import '../../navigations/context-menu/material3-definition.scss'; +@import '../../notifications/skeleton/material3-definition.scss'; +@import '../../pager/pager/material3-definition.scss'; +@import 'material3-definition.scss'; +@import 'all.scss'; diff --git a/components/grids/styles/material3.scss b/components/grids/styles/material3.scss new file mode 100644 index 0000000..a7d84b9 --- /dev/null +++ b/components/grids/styles/material3.scss @@ -0,0 +1,15 @@ +@import '../base/themes/material3.scss'; +@import '../inputs/input/material3-definition.scss'; +@import '../inputs/numerictextbox/material3-definition.scss'; +@import '../buttons/button/material3-definition.scss'; +@import '../buttons/radio-button/material3-definition.scss'; +@import '../buttons/check-box/material3-definition.scss'; +@import '../dropdowns/drop-down-list/material3-definition.scss'; +@import '../popups/spinner/material3-definition.scss'; +@import '../popups/tooltip/material3-definition.scss'; +@import '../navigations/toolbar/material3-definition.scss'; +@import '../navigations/context-menu/material3-definition.scss'; +@import '../notifications/skeleton/material3-definition.scss'; +@import '../pager/pager/material3-definition.scss'; +@import 'grid/material3-definition.scss'; +@import 'grid/all.scss'; diff --git a/components/navigations/tsconfig.json b/components/grids/tsconfig.json similarity index 99% rename from components/navigations/tsconfig.json rename to components/grids/tsconfig.json index e580aa1..3ab8c23 100644 --- a/components/navigations/tsconfig.json +++ b/components/grids/tsconfig.json @@ -39,4 +39,4 @@ "test-report" ], // Exclude these directories from compilation. "compileOnSave": false // Disable Compile-on-Save. -} \ No newline at end of file +} diff --git a/components/icons/CHANGELOG.MD b/components/icons/CHANGELOG.MD deleted file mode 100644 index d1c7df9..0000000 --- a/components/icons/CHANGELOG.MD +++ /dev/null @@ -1,13 +0,0 @@ -# Changelog - -## [Unreleased] - -## 30.1.37 (2025-06-25) - -### Icons -The React Icon Library is a centralized and scalable collection of SVG-based icon components built specifically for modern React applications. Designed to offer a consistent and customizable icon experience across projects. -**Key Features** -* Includes a comprehensive set of over 500+ ready-to-use SVG icon components covering essential categories such as arrows, indicators, file types, and more. -* Icon components support dynamic props like color, size, width, height, and viewBox, enabling easy visual customization directly through JSX. -* Icons are exported as lightweight, reusable React functional components. They can be seamlessly embedded in buttons, inputs, navigation items, cards, and other custom components. -* With modular exports, only the icons used in your application are included in the final bundle, ensuring optimal performance with minimal overhead. diff --git a/components/icons/CHANGELOG.md b/components/icons/CHANGELOG.md new file mode 100644 index 0000000..30d5cee --- /dev/null +++ b/components/icons/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## [Unreleased] \ No newline at end of file diff --git a/components/icons/src/icon.tsx b/components/icons/src/icon.tsx deleted file mode 100644 index 92be67f..0000000 --- a/components/icons/src/icon.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { HTMLAttributes, memo, SVGProps } from 'react'; -import { SvgIcon } from './svg-icon'; - -/** - * Type for the icon component - */ -export type IconComponent = React.FC & SVGProps>; - -/** - * Type definition for the icon creator function - * Represents a function that creates a memoized icon component from an SVG path - * - * @param path - The SVG path data string - * @returns A memoized React component that renders the icon - */ -type IconGenerator = (svgElements: React.ReactNode -) => React.NamedExoticComponent & SVGProps>; - -/** - * Base icon component creator function creates a reusable icon component from an SVG path. - * - * @param {string} svgElements - The SVG path data string - * @returns {IconComponent} A memoized React functional component - */ -export const createIcon: IconGenerator = (svgElements: React.ReactNode) => { - const IconComponent: React.FC & SVGProps> = ({ - width = 24, - height = 24, - viewBox = '0 0 24 24', - className = '', - ...otherProps - }: HTMLAttributes & SVGProps) => { - return ( - - {svgElements} - - - ); - }; - return memo(IconComponent); -}; diff --git a/components/icons/src/icons/above-average.tsx b/components/icons/src/icons/above-average.tsx deleted file mode 100644 index 820223a..0000000 --- a/components/icons/src/icons/above-average.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AboveAverage: IconComponent = createIcon(path); - diff --git a/components/icons/src/icons/activities.tsx b/components/icons/src/icons/activities.tsx deleted file mode 100644 index 64f1f3b..0000000 --- a/components/icons/src/icons/activities.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Activities: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/add-chart-element.tsx b/components/icons/src/icons/add-chart-element.tsx deleted file mode 100644 index 7359f79..0000000 --- a/components/icons/src/icons/add-chart-element.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AddChartElement: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/add-notes.tsx b/components/icons/src/icons/add-notes.tsx deleted file mode 100644 index 3255941..0000000 --- a/components/icons/src/icons/add-notes.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AddNotes: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/adjustment.tsx b/components/icons/src/icons/adjustment.tsx deleted file mode 100644 index a39b52a..0000000 --- a/components/icons/src/icons/adjustment.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Adjustment: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/agenda-date-range.tsx b/components/icons/src/icons/agenda-date-range.tsx deleted file mode 100644 index 4ba3b82..0000000 --- a/components/icons/src/icons/agenda-date-range.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AgendaDateRange: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/ai-chat.tsx b/components/icons/src/icons/ai-chat.tsx deleted file mode 100644 index ef3bf58..0000000 --- a/components/icons/src/icons/ai-chat.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AiChat: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/align-bottom.tsx b/components/icons/src/icons/align-bottom.tsx deleted file mode 100644 index dfbb19c..0000000 --- a/components/icons/src/icons/align-bottom.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AlignBottom: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/align-center.tsx b/components/icons/src/icons/align-center.tsx deleted file mode 100644 index 9575a19..0000000 --- a/components/icons/src/icons/align-center.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AlignCenter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/align-left.tsx b/components/icons/src/icons/align-left.tsx deleted file mode 100644 index f5e2d38..0000000 --- a/components/icons/src/icons/align-left.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AlignLeft: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/align-middle.tsx b/components/icons/src/icons/align-middle.tsx deleted file mode 100644 index 0163e8d..0000000 --- a/components/icons/src/icons/align-middle.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AlignMiddle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/align-right.tsx b/components/icons/src/icons/align-right.tsx deleted file mode 100644 index 61beadf..0000000 --- a/components/icons/src/icons/align-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AlignRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/align-top.tsx b/components/icons/src/icons/align-top.tsx deleted file mode 100644 index 78461f1..0000000 --- a/components/icons/src/icons/align-top.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AlignTop: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/all.tsx b/components/icons/src/icons/all.tsx deleted file mode 100644 index 7784882..0000000 --- a/components/icons/src/icons/all.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const All: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/animals.tsx b/components/icons/src/icons/animals.tsx deleted file mode 100644 index 87d3bc4..0000000 --- a/components/icons/src/icons/animals.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Animals: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/annotation-edit.tsx b/components/icons/src/icons/annotation-edit.tsx deleted file mode 100644 index 12eb378..0000000 --- a/components/icons/src/icons/annotation-edit.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AnnotationEdit: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/area.tsx b/components/icons/src/icons/area.tsx deleted file mode 100644 index 2333d24..0000000 --- a/components/icons/src/icons/area.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Area: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/arrow-down.tsx b/components/icons/src/icons/arrow-down.tsx deleted file mode 100644 index 0a738d4..0000000 --- a/components/icons/src/icons/arrow-down.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ArrowDown: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/arrow-head-fill.tsx b/components/icons/src/icons/arrow-head-fill.tsx deleted file mode 100644 index 9d8b4d5..0000000 --- a/components/icons/src/icons/arrow-head-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ArrowHeadFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/arrow-head.tsx b/components/icons/src/icons/arrow-head.tsx deleted file mode 100644 index 228d9e4..0000000 --- a/components/icons/src/icons/arrow-head.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ArrowHead: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/arrow-left.tsx b/components/icons/src/icons/arrow-left.tsx deleted file mode 100644 index a6397c5..0000000 --- a/components/icons/src/icons/arrow-left.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ArrowLeft: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/arrow-right-up.tsx b/components/icons/src/icons/arrow-right-up.tsx deleted file mode 100644 index 497aceb..0000000 --- a/components/icons/src/icons/arrow-right-up.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ArrowRightUp: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/arrow-right.tsx b/components/icons/src/icons/arrow-right.tsx deleted file mode 100644 index 7091678..0000000 --- a/components/icons/src/icons/arrow-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ArrowRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/arrow-tail-fill.tsx b/components/icons/src/icons/arrow-tail-fill.tsx deleted file mode 100644 index 6d652ca..0000000 --- a/components/icons/src/icons/arrow-tail-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ArrowTailFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/arrow-tail.tsx b/components/icons/src/icons/arrow-tail.tsx deleted file mode 100644 index 9e23b45..0000000 --- a/components/icons/src/icons/arrow-tail.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ArrowTail: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/arrow-up.tsx b/components/icons/src/icons/arrow-up.tsx deleted file mode 100644 index ea28064..0000000 --- a/components/icons/src/icons/arrow-up.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ArrowUp: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/audio.tsx b/components/icons/src/icons/audio.tsx deleted file mode 100644 index e86df01..0000000 --- a/components/icons/src/icons/audio.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Audio: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/auto-fit-all-column.tsx b/components/icons/src/icons/auto-fit-all-column.tsx deleted file mode 100644 index e6f049b..0000000 --- a/components/icons/src/icons/auto-fit-all-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AutoFitAllColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/auto-fit-column.tsx b/components/icons/src/icons/auto-fit-column.tsx deleted file mode 100644 index d71a614..0000000 --- a/components/icons/src/icons/auto-fit-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AutoFitColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/auto-fit-content.tsx b/components/icons/src/icons/auto-fit-content.tsx deleted file mode 100644 index ffd870c..0000000 --- a/components/icons/src/icons/auto-fit-content.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AutoFitContent: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/auto-fit-window.tsx b/components/icons/src/icons/auto-fit-window.tsx deleted file mode 100644 index 53d66d8..0000000 --- a/components/icons/src/icons/auto-fit-window.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const AutoFitWindow: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bar-head.tsx b/components/icons/src/icons/bar-head.tsx deleted file mode 100644 index 3a4e99a..0000000 --- a/components/icons/src/icons/bar-head.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BarHead: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bar-tail.tsx b/components/icons/src/icons/bar-tail.tsx deleted file mode 100644 index 16bc97a..0000000 --- a/components/icons/src/icons/bar-tail.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BarTail: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/below-average.tsx b/components/icons/src/icons/below-average.tsx deleted file mode 100644 index 2091966..0000000 --- a/components/icons/src/icons/below-average.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BelowAverage: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/between.tsx b/components/icons/src/icons/between.tsx deleted file mode 100644 index c2d27d8..0000000 --- a/components/icons/src/icons/between.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Between: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/blockquote.tsx b/components/icons/src/icons/blockquote.tsx deleted file mode 100644 index cdb5901..0000000 --- a/components/icons/src/icons/blockquote.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Blockquote: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bold.tsx b/components/icons/src/icons/bold.tsx deleted file mode 100644 index 9e1e32f..0000000 --- a/components/icons/src/icons/bold.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Bold: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bookmark.tsx b/components/icons/src/icons/bookmark.tsx deleted file mode 100644 index d6f2b84..0000000 --- a/components/icons/src/icons/bookmark.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Bookmark: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-all.tsx b/components/icons/src/icons/border-all.tsx deleted file mode 100644 index c759107..0000000 --- a/components/icons/src/icons/border-all.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderAll: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-bottom.tsx b/components/icons/src/icons/border-bottom.tsx deleted file mode 100644 index 452b73a..0000000 --- a/components/icons/src/icons/border-bottom.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderBottom: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-box.tsx b/components/icons/src/icons/border-box.tsx deleted file mode 100644 index 6f93e60..0000000 --- a/components/icons/src/icons/border-box.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderBox: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-center.tsx b/components/icons/src/icons/border-center.tsx deleted file mode 100644 index 4a35d00..0000000 --- a/components/icons/src/icons/border-center.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderCenter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-custom.tsx b/components/icons/src/icons/border-custom.tsx deleted file mode 100644 index e9432ec..0000000 --- a/components/icons/src/icons/border-custom.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderCustom: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-diagonal-1.tsx b/components/icons/src/icons/border-diagonal-backslash.tsx similarity index 91% rename from components/icons/src/icons/border-diagonal-1.tsx rename to components/icons/src/icons/border-diagonal-backslash.tsx index 5e80fba..a5dd3b4 100644 --- a/components/icons/src/icons/border-diagonal-1.tsx +++ b/components/icons/src/icons/border-diagonal-backslash.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const BorderDiagonal1: IconComponent = createIcon(path); +export const BorderDiagonalBackslashIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-diagonal-down.tsx b/components/icons/src/icons/border-diagonal-down.tsx deleted file mode 100644 index ebbc502..0000000 --- a/components/icons/src/icons/border-diagonal-down.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderDiagonalDown: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-diagonal-2.tsx b/components/icons/src/icons/border-diagonal-slash.tsx similarity index 92% rename from components/icons/src/icons/border-diagonal-2.tsx rename to components/icons/src/icons/border-diagonal-slash.tsx index 4bbbd34..a11dd6d 100644 --- a/components/icons/src/icons/border-diagonal-2.tsx +++ b/components/icons/src/icons/border-diagonal-slash.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const BorderDiagonal2: IconComponent = createIcon(path); +export const BorderDiagonalSlashIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-diagonal-up.tsx b/components/icons/src/icons/border-diagonal-up.tsx deleted file mode 100644 index 3b5dcf3..0000000 --- a/components/icons/src/icons/border-diagonal-up.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderDiagonalUp: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-frame.tsx b/components/icons/src/icons/border-frame.tsx deleted file mode 100644 index a649f0c..0000000 --- a/components/icons/src/icons/border-frame.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderFrame: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-shadow-2.tsx b/components/icons/src/icons/border-inner-shadow.tsx similarity index 87% rename from components/icons/src/icons/border-shadow-2.tsx rename to components/icons/src/icons/border-inner-shadow.tsx index bd5e7e0..b0e5e62 100644 --- a/components/icons/src/icons/border-shadow-2.tsx +++ b/components/icons/src/icons/border-inner-shadow.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const BorderShadow2: IconComponent = createIcon(path); +export const BorderInnerShadowIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-inner.tsx b/components/icons/src/icons/border-inner.tsx deleted file mode 100644 index f57d8c0..0000000 --- a/components/icons/src/icons/border-inner.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderInner: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-left.tsx b/components/icons/src/icons/border-left.tsx deleted file mode 100644 index 8034883..0000000 --- a/components/icons/src/icons/border-left.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderLeft: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-middle.tsx b/components/icons/src/icons/border-middle.tsx deleted file mode 100644 index d174d8d..0000000 --- a/components/icons/src/icons/border-middle.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderMiddle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-none-1.tsx b/components/icons/src/icons/border-none-content.tsx similarity index 97% rename from components/icons/src/icons/border-none-1.tsx rename to components/icons/src/icons/border-none-content.tsx index 8a9d7cc..ac53047 100644 --- a/components/icons/src/icons/border-none-1.tsx +++ b/components/icons/src/icons/border-none-content.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const BorderNone1: IconComponent = createIcon(path); +export const BorderNoneContentIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-none.tsx b/components/icons/src/icons/border-none.tsx deleted file mode 100644 index e7c991d..0000000 --- a/components/icons/src/icons/border-none.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderNone: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-shadow-1.tsx b/components/icons/src/icons/border-outer-shadow.tsx similarity index 98% rename from components/icons/src/icons/border-shadow-1.tsx rename to components/icons/src/icons/border-outer-shadow.tsx index 1afca14..dc005a5 100644 --- a/components/icons/src/icons/border-shadow-1.tsx +++ b/components/icons/src/icons/border-outer-shadow.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const BorderShadow1: IconComponent = createIcon(path); +export const BorderOuterShadowIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-outer.tsx b/components/icons/src/icons/border-outer.tsx deleted file mode 100644 index 4ca57b7..0000000 --- a/components/icons/src/icons/border-outer.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderOuter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-right.tsx b/components/icons/src/icons/border-right.tsx deleted file mode 100644 index 6652eaa..0000000 --- a/components/icons/src/icons/border-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/border-top.tsx b/components/icons/src/icons/border-top.tsx deleted file mode 100644 index f1ec6fb..0000000 --- a/components/icons/src/icons/border-top.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BorderTop: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bottom-10-items.tsx b/components/icons/src/icons/bottom-10-items.tsx deleted file mode 100644 index 37be4d7..0000000 --- a/components/icons/src/icons/bottom-10-items.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Bottom10Items: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bottom-10.tsx b/components/icons/src/icons/bottom-10.tsx deleted file mode 100644 index 865bf2a..0000000 --- a/components/icons/src/icons/bottom-10.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Bottom10: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/box.tsx b/components/icons/src/icons/box.tsx deleted file mode 100644 index 4dfacdd..0000000 --- a/components/icons/src/icons/box.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Box: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/break-page.tsx b/components/icons/src/icons/break-page.tsx deleted file mode 100644 index 69f8d8b..0000000 --- a/components/icons/src/icons/break-page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BreakPage: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/break-section.tsx b/components/icons/src/icons/break-section.tsx deleted file mode 100644 index 1b42477..0000000 --- a/components/icons/src/icons/break-section.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BreakSection: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/break.tsx b/components/icons/src/icons/break.tsx deleted file mode 100644 index f41b8ab..0000000 --- a/components/icons/src/icons/break.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Break: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/brightness.tsx b/components/icons/src/icons/brightness.tsx deleted file mode 100644 index b16ad07..0000000 --- a/components/icons/src/icons/brightness.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Brightness: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bring-forward.tsx b/components/icons/src/icons/bring-forward.tsx deleted file mode 100644 index 1758b91..0000000 --- a/components/icons/src/icons/bring-forward.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BringForward: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bring-to-center.tsx b/components/icons/src/icons/bring-to-center.tsx deleted file mode 100644 index cd408fc..0000000 --- a/components/icons/src/icons/bring-to-center.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BringToCenter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bring-to-front.tsx b/components/icons/src/icons/bring-to-front.tsx deleted file mode 100644 index 4653f42..0000000 --- a/components/icons/src/icons/bring-to-front.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BringToFront: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bring-to-view.tsx b/components/icons/src/icons/bring-to-view.tsx deleted file mode 100644 index 24461ec..0000000 --- a/components/icons/src/icons/bring-to-view.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BringToView: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/building-block.tsx b/components/icons/src/icons/building-block.tsx deleted file mode 100644 index 2ea4dc1..0000000 --- a/components/icons/src/icons/building-block.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const BuildingBlock: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bullet-6.tsx b/components/icons/src/icons/bullet-arrow.tsx similarity index 81% rename from components/icons/src/icons/bullet-6.tsx rename to components/icons/src/icons/bullet-arrow.tsx index 530772c..f154533 100644 --- a/components/icons/src/icons/bullet-6.tsx +++ b/components/icons/src/icons/bullet-arrow.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Bullet6: IconComponent = createIcon(path); +export const BulletArrowIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bullet-5.tsx b/components/icons/src/icons/bullet-check.tsx similarity index 80% rename from components/icons/src/icons/bullet-5.tsx rename to components/icons/src/icons/bullet-check.tsx index e1a194e..aafea18 100644 --- a/components/icons/src/icons/bullet-5.tsx +++ b/components/icons/src/icons/bullet-check.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Bullet5: IconComponent = createIcon(path); +export const BulletCheckIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bullet-1.tsx b/components/icons/src/icons/bullet-circle-outline.tsx similarity index 86% rename from components/icons/src/icons/bullet-1.tsx rename to components/icons/src/icons/bullet-circle-outline.tsx index c481be0..24eea16 100644 --- a/components/icons/src/icons/bullet-1.tsx +++ b/components/icons/src/icons/bullet-circle-outline.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Bullet1: IconComponent = createIcon(path); +export const BulletCircleOutlineIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bullet-2.tsx b/components/icons/src/icons/bullet-circle.tsx similarity index 81% rename from components/icons/src/icons/bullet-2.tsx rename to components/icons/src/icons/bullet-circle.tsx index 9d1bb23..ca04dee 100644 --- a/components/icons/src/icons/bullet-2.tsx +++ b/components/icons/src/icons/bullet-circle.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Bullet2: IconComponent = createIcon(path); +export const BulletCircleIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bullet-4.tsx b/components/icons/src/icons/bullet-diamond.tsx similarity index 94% rename from components/icons/src/icons/bullet-4.tsx rename to components/icons/src/icons/bullet-diamond.tsx index 00aa27e..0e1bc2b 100644 --- a/components/icons/src/icons/bullet-4.tsx +++ b/components/icons/src/icons/bullet-diamond.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Bullet4: IconComponent = createIcon(path); +export const BulletDiamondIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bullet-7.tsx b/components/icons/src/icons/bullet-point.tsx similarity index 81% rename from components/icons/src/icons/bullet-7.tsx rename to components/icons/src/icons/bullet-point.tsx index 04c8c6b..83b6395 100644 --- a/components/icons/src/icons/bullet-7.tsx +++ b/components/icons/src/icons/bullet-point.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Bullet7: IconComponent = createIcon(path); +export const BulletPointIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/bullet-3.tsx b/components/icons/src/icons/bullet-square.tsx similarity index 74% rename from components/icons/src/icons/bullet-3.tsx rename to components/icons/src/icons/bullet-square.tsx index a2efd6d..0f87648 100644 --- a/components/icons/src/icons/bullet-3.tsx +++ b/components/icons/src/icons/bullet-square.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Bullet3: IconComponent = createIcon(path); +export const BulletSquareIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/button-field.tsx b/components/icons/src/icons/button-field.tsx deleted file mode 100644 index efe4d90..0000000 --- a/components/icons/src/icons/button-field.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ButtonField: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/calculate-sheet.tsx b/components/icons/src/icons/calculate-sheet.tsx deleted file mode 100644 index a4bca96..0000000 --- a/components/icons/src/icons/calculate-sheet.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CalculateSheet: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/calculated-member.tsx b/components/icons/src/icons/calculated-member.tsx deleted file mode 100644 index 4ea7ac1..0000000 --- a/components/icons/src/icons/calculated-member.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CalculatedMember: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/calculation.tsx b/components/icons/src/icons/calculation.tsx deleted file mode 100644 index e86bbfb..0000000 --- a/components/icons/src/icons/calculation.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Calculation: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/caption.tsx b/components/icons/src/icons/caption.tsx deleted file mode 100644 index 1254df7..0000000 --- a/components/icons/src/icons/caption.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Caption: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/cell.tsx b/components/icons/src/icons/cell.tsx deleted file mode 100644 index 0ff7e7a..0000000 --- a/components/icons/src/icons/cell.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Cell: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/change-case.tsx b/components/icons/src/icons/change-case.tsx deleted file mode 100644 index 30c8769..0000000 --- a/components/icons/src/icons/change-case.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChangeCase: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/change-scale-ratio.tsx b/components/icons/src/icons/change-scale-ratio.tsx deleted file mode 100644 index 26b5e59..0000000 --- a/components/icons/src/icons/change-scale-ratio.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChangeScaleRatio: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/changes-accept.tsx b/components/icons/src/icons/changes-accept.tsx deleted file mode 100644 index cf35730..0000000 --- a/components/icons/src/icons/changes-accept.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChangesAccept: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/changes-reject.tsx b/components/icons/src/icons/changes-reject.tsx deleted file mode 100644 index e17da44..0000000 --- a/components/icons/src/icons/changes-reject.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChangesReject: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/changes-track.tsx b/components/icons/src/icons/changes-track.tsx deleted file mode 100644 index 46a8118..0000000 --- a/components/icons/src/icons/changes-track.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChangesTrack: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/character-style.tsx b/components/icons/src/icons/character-style.tsx deleted file mode 100644 index 1574bbf..0000000 --- a/components/icons/src/icons/character-style.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CharacterStyle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-100-percent-stacked-area.tsx b/components/icons/src/icons/chart-2d-100-percent-stacked-area.tsx deleted file mode 100644 index bac268f..0000000 --- a/components/icons/src/icons/chart-2d-100-percent-stacked-area.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2d100PercentStackedArea: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-100-percent-stacked-bar.tsx b/components/icons/src/icons/chart-2d-100-percent-stacked-bar.tsx deleted file mode 100644 index 778b0f8..0000000 --- a/components/icons/src/icons/chart-2d-100-percent-stacked-bar.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2d100PercentStackedBar: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-100-percent-stacked-column.tsx b/components/icons/src/icons/chart-2d-100-percent-stacked-column.tsx deleted file mode 100644 index e56991d..0000000 --- a/components/icons/src/icons/chart-2d-100-percent-stacked-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2d100PercentStackedColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-100-percent-stacked-line-marked.tsx b/components/icons/src/icons/chart-2d-100-percent-stacked-line-marked.tsx deleted file mode 100644 index a3d6841..0000000 --- a/components/icons/src/icons/chart-2d-100-percent-stacked-line-marked.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2d100PercentStackedLineMarked: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-100-percent-stacked-line.tsx b/components/icons/src/icons/chart-2d-100-percent-stacked-line.tsx deleted file mode 100644 index 6b6283d..0000000 --- a/components/icons/src/icons/chart-2d-100-percent-stacked-line.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2d100PercentStackedLine: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-area.tsx b/components/icons/src/icons/chart-2d-area.tsx deleted file mode 100644 index bd6bcf6..0000000 --- a/components/icons/src/icons/chart-2d-area.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dArea: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-clustered-bar.tsx b/components/icons/src/icons/chart-2d-clustered-bar.tsx deleted file mode 100644 index 3eff984..0000000 --- a/components/icons/src/icons/chart-2d-clustered-bar.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dClusteredBar: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-clustered-column.tsx b/components/icons/src/icons/chart-2d-clustered-column.tsx deleted file mode 100644 index 3fa4c5b..0000000 --- a/components/icons/src/icons/chart-2d-clustered-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dClusteredColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-line-marked.tsx b/components/icons/src/icons/chart-2d-line-marked.tsx deleted file mode 100644 index 6d37cce..0000000 --- a/components/icons/src/icons/chart-2d-line-marked.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dLineMarked: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-line.tsx b/components/icons/src/icons/chart-2d-line.tsx deleted file mode 100644 index 2edde4e..0000000 --- a/components/icons/src/icons/chart-2d-line.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dLine: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-pie-2.tsx b/components/icons/src/icons/chart-2d-pie-2.tsx deleted file mode 100644 index eb44ede..0000000 --- a/components/icons/src/icons/chart-2d-pie-2.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dPie2: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-stacked-area.tsx b/components/icons/src/icons/chart-2d-stacked-area.tsx deleted file mode 100644 index 1b47161..0000000 --- a/components/icons/src/icons/chart-2d-stacked-area.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dStackedArea: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-stacked-bar.tsx b/components/icons/src/icons/chart-2d-stacked-bar.tsx deleted file mode 100644 index cdbed0c..0000000 --- a/components/icons/src/icons/chart-2d-stacked-bar.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dStackedBar: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-stacked-column.tsx b/components/icons/src/icons/chart-2d-stacked-column.tsx deleted file mode 100644 index 325b7bf..0000000 --- a/components/icons/src/icons/chart-2d-stacked-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dStackedColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-stacked-line-marked.tsx b/components/icons/src/icons/chart-2d-stacked-line-marked.tsx deleted file mode 100644 index f4082ba..0000000 --- a/components/icons/src/icons/chart-2d-stacked-line-marked.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dStackedLineMarked: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-2d-stacked-line.tsx b/components/icons/src/icons/chart-2d-stacked-line.tsx deleted file mode 100644 index b1a7b03..0000000 --- a/components/icons/src/icons/chart-2d-stacked-line.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart2dStackedLine: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-axes-primary-horizontal.tsx b/components/icons/src/icons/chart-axes-primary-horizontal.tsx deleted file mode 100644 index ee6672e..0000000 --- a/components/icons/src/icons/chart-axes-primary-horizontal.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartAxesPrimaryHorizontal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-axes-primary-vertical.tsx b/components/icons/src/icons/chart-axes-primary-vertical.tsx deleted file mode 100644 index 8446f55..0000000 --- a/components/icons/src/icons/chart-axes-primary-vertical.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartAxesPrimaryVertical: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-axes.tsx b/components/icons/src/icons/chart-axes.tsx deleted file mode 100644 index 51b7a68..0000000 --- a/components/icons/src/icons/chart-axes.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartAxes: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-axis-titles-primary-horizontal.tsx b/components/icons/src/icons/chart-axis-titles-primary-horizontal.tsx deleted file mode 100644 index 8f5ccf6..0000000 --- a/components/icons/src/icons/chart-axis-titles-primary-horizontal.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartAxisTitlesPrimaryHorizontal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-axis-titles-primary-vertical.tsx b/components/icons/src/icons/chart-axis-titles-primary-vertical.tsx deleted file mode 100644 index 0805a3c..0000000 --- a/components/icons/src/icons/chart-axis-titles-primary-vertical.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartAxisTitlesPrimaryVertical: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-axis-titles.tsx b/components/icons/src/icons/chart-axis-titles.tsx deleted file mode 100644 index 68d2e94..0000000 --- a/components/icons/src/icons/chart-axis-titles.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartAxisTitles: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-data-labels-center.tsx b/components/icons/src/icons/chart-data-labels-center.tsx deleted file mode 100644 index bfedd29..0000000 --- a/components/icons/src/icons/chart-data-labels-center.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartDataLabelsCenter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-data-labels-inside-base.tsx b/components/icons/src/icons/chart-data-labels-inside-base.tsx deleted file mode 100644 index b7a7706..0000000 --- a/components/icons/src/icons/chart-data-labels-inside-base.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartDataLabelsInsideBase: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-data-labels-inside-end.tsx b/components/icons/src/icons/chart-data-labels-inside-end.tsx deleted file mode 100644 index 5551510..0000000 --- a/components/icons/src/icons/chart-data-labels-inside-end.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartDataLabelsInsideEnd: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-data-labels-none.tsx b/components/icons/src/icons/chart-data-labels-none.tsx deleted file mode 100644 index 3ad3faf..0000000 --- a/components/icons/src/icons/chart-data-labels-none.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartDataLabelsNone: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-data-labels-outside-end.tsx b/components/icons/src/icons/chart-data-labels-outside-end.tsx deleted file mode 100644 index b8ef7b0..0000000 --- a/components/icons/src/icons/chart-data-labels-outside-end.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartDataLabelsOutsideEnd: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-data-labels.tsx b/components/icons/src/icons/chart-data-labels.tsx deleted file mode 100644 index e0bbb69..0000000 --- a/components/icons/src/icons/chart-data-labels.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartDataLabels: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-donut.tsx b/components/icons/src/icons/chart-donut.tsx deleted file mode 100644 index 49ee8b2..0000000 --- a/components/icons/src/icons/chart-donut.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartDonut: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-gridlines.tsx b/components/icons/src/icons/chart-gridlines.tsx deleted file mode 100644 index d4c8b3f..0000000 --- a/components/icons/src/icons/chart-gridlines.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartGridlines: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-insert-bar.tsx b/components/icons/src/icons/chart-insert-bar.tsx deleted file mode 100644 index deb93f0..0000000 --- a/components/icons/src/icons/chart-insert-bar.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartInsertBar: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-insert-column.tsx b/components/icons/src/icons/chart-insert-column.tsx deleted file mode 100644 index 83fbd4a..0000000 --- a/components/icons/src/icons/chart-insert-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartInsertColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-insert-line.tsx b/components/icons/src/icons/chart-insert-line.tsx deleted file mode 100644 index cc689ee..0000000 --- a/components/icons/src/icons/chart-insert-line.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartInsertLine: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-insert-pie.tsx b/components/icons/src/icons/chart-insert-pie.tsx deleted file mode 100644 index 0847a5c..0000000 --- a/components/icons/src/icons/chart-insert-pie.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartInsertPie: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-insert-x-y-scatter.tsx b/components/icons/src/icons/chart-insert-x-y-scatter.tsx deleted file mode 100644 index 275d87c..0000000 --- a/components/icons/src/icons/chart-insert-x-y-scatter.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartInsertXYScatter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-legend-bottom.tsx b/components/icons/src/icons/chart-legend-bottom.tsx deleted file mode 100644 index affb2d3..0000000 --- a/components/icons/src/icons/chart-legend-bottom.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartLegendBottom: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-legend-left.tsx b/components/icons/src/icons/chart-legend-left.tsx deleted file mode 100644 index ae539b1..0000000 --- a/components/icons/src/icons/chart-legend-left.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartLegendLeft: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-legend-none.tsx b/components/icons/src/icons/chart-legend-none.tsx deleted file mode 100644 index f402360..0000000 --- a/components/icons/src/icons/chart-legend-none.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartLegendNone: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-legend-right.tsx b/components/icons/src/icons/chart-legend-right.tsx deleted file mode 100644 index cb5d306..0000000 --- a/components/icons/src/icons/chart-legend-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartLegendRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-legend-top.tsx b/components/icons/src/icons/chart-legend-top.tsx deleted file mode 100644 index 8f4024e..0000000 --- a/components/icons/src/icons/chart-legend-top.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartLegendTop: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-legend.tsx b/components/icons/src/icons/chart-legend.tsx deleted file mode 100644 index 538d98f..0000000 --- a/components/icons/src/icons/chart-legend.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartLegend: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-lines.tsx b/components/icons/src/icons/chart-lines.tsx deleted file mode 100644 index 72f11bc..0000000 --- a/components/icons/src/icons/chart-lines.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartLines: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-primary-major-horizontal.tsx b/components/icons/src/icons/chart-primary-major-horizontal.tsx deleted file mode 100644 index fb66d06..0000000 --- a/components/icons/src/icons/chart-primary-major-horizontal.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartPrimaryMajorHorizontal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-primary-major-vertical.tsx b/components/icons/src/icons/chart-primary-major-vertical.tsx deleted file mode 100644 index 548e6ba..0000000 --- a/components/icons/src/icons/chart-primary-major-vertical.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartPrimaryMajorVertical: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-primary-minor-horizontal.tsx b/components/icons/src/icons/chart-primary-minor-horizontal.tsx deleted file mode 100644 index dcd4db1..0000000 --- a/components/icons/src/icons/chart-primary-minor-horizontal.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartPrimaryMinorHorizontal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-primary-minor-vertical.tsx b/components/icons/src/icons/chart-primary-minor-vertical.tsx deleted file mode 100644 index 2d81756..0000000 --- a/components/icons/src/icons/chart-primary-minor-vertical.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartPrimaryMinorVertical: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-switch-row-column.tsx b/components/icons/src/icons/chart-switch-row-column.tsx deleted file mode 100644 index 7450911..0000000 --- a/components/icons/src/icons/chart-switch-row-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartSwitchRowColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-title-centered-overlay.tsx b/components/icons/src/icons/chart-title-centered-overlay.tsx deleted file mode 100644 index c423513..0000000 --- a/components/icons/src/icons/chart-title-centered-overlay.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartTitleCenteredOverlay: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-title-none.tsx b/components/icons/src/icons/chart-title-none.tsx deleted file mode 100644 index 6b61f7f..0000000 --- a/components/icons/src/icons/chart-title-none.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartTitleNone: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart-title.tsx b/components/icons/src/icons/chart-title.tsx deleted file mode 100644 index 5403ac8..0000000 --- a/components/icons/src/icons/chart-title.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChartTitle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chart.tsx b/components/icons/src/icons/chart.tsx deleted file mode 100644 index 6e643b9..0000000 --- a/components/icons/src/icons/chart.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Chart: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/check-box.tsx b/components/icons/src/icons/check-box.tsx deleted file mode 100644 index f85ad80..0000000 --- a/components/icons/src/icons/check-box.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CheckBox: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/check-large.tsx b/components/icons/src/icons/check-large.tsx deleted file mode 100644 index 3812171..0000000 --- a/components/icons/src/icons/check-large.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CheckLarge: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/check-small.tsx b/components/icons/src/icons/check-small.tsx deleted file mode 100644 index 1edd974..0000000 --- a/components/icons/src/icons/check-small.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CheckSmall: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/check-tick.tsx b/components/icons/src/icons/check-tick.tsx deleted file mode 100644 index 2b4b1b3..0000000 --- a/components/icons/src/icons/check-tick.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CheckTick: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/check.tsx b/components/icons/src/icons/check.tsx deleted file mode 100644 index 58ba4d0..0000000 --- a/components/icons/src/icons/check.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Check: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/intermediate-state.tsx b/components/icons/src/icons/checkbox-intermediate.tsx similarity index 81% rename from components/icons/src/icons/intermediate-state.tsx rename to components/icons/src/icons/checkbox-intermediate.tsx index d1d231c..c057df6 100644 --- a/components/icons/src/icons/intermediate-state.tsx +++ b/components/icons/src/icons/checkbox-intermediate.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const IntermediateState: IconComponent = createIcon(path); +export const CheckboxIntermediateIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-down-double.tsx b/components/icons/src/icons/chevron-down-double.tsx deleted file mode 100644 index bc4a05c..0000000 --- a/components/icons/src/icons/chevron-down-double.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronDownDouble: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-down-fill.tsx b/components/icons/src/icons/chevron-down-fill.tsx deleted file mode 100644 index beab068..0000000 --- a/components/icons/src/icons/chevron-down-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronDownFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-down-small.tsx b/components/icons/src/icons/chevron-down-small.tsx deleted file mode 100644 index 208fd58..0000000 --- a/components/icons/src/icons/chevron-down-small.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronDownSmall: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-down.tsx b/components/icons/src/icons/chevron-down.tsx deleted file mode 100644 index 93252bb..0000000 --- a/components/icons/src/icons/chevron-down.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronDown: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-left-double.tsx b/components/icons/src/icons/chevron-left-double.tsx deleted file mode 100644 index d1a686d..0000000 --- a/components/icons/src/icons/chevron-left-double.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronLeftDouble: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-left-fill.tsx b/components/icons/src/icons/chevron-left-fill.tsx deleted file mode 100644 index c97c631..0000000 --- a/components/icons/src/icons/chevron-left-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronLeftFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-left-small.tsx b/components/icons/src/icons/chevron-left-small.tsx deleted file mode 100644 index 607fffd..0000000 --- a/components/icons/src/icons/chevron-left-small.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronLeftSmall: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-left.tsx b/components/icons/src/icons/chevron-left.tsx deleted file mode 100644 index 42f8780..0000000 --- a/components/icons/src/icons/chevron-left.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronLeft: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-right-double.tsx b/components/icons/src/icons/chevron-right-double.tsx deleted file mode 100644 index 59ff0da..0000000 --- a/components/icons/src/icons/chevron-right-double.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronRightDouble: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-right-fill.tsx b/components/icons/src/icons/chevron-right-fill.tsx deleted file mode 100644 index 9f7aa86..0000000 --- a/components/icons/src/icons/chevron-right-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronRightFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-right-small.tsx b/components/icons/src/icons/chevron-right-small.tsx deleted file mode 100644 index 8e20f70..0000000 --- a/components/icons/src/icons/chevron-right-small.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronRightSmall: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-right.tsx b/components/icons/src/icons/chevron-right.tsx deleted file mode 100644 index d2edc12..0000000 --- a/components/icons/src/icons/chevron-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-up-double.tsx b/components/icons/src/icons/chevron-up-double.tsx deleted file mode 100644 index dcc7311..0000000 --- a/components/icons/src/icons/chevron-up-double.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronUpDouble: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-up-fill.tsx b/components/icons/src/icons/chevron-up-fill.tsx deleted file mode 100644 index 74d8561..0000000 --- a/components/icons/src/icons/chevron-up-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronUpFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-up-small.tsx b/components/icons/src/icons/chevron-up-small.tsx deleted file mode 100644 index 32de196..0000000 --- a/components/icons/src/icons/chevron-up-small.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronUpSmall: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/chevron-up.tsx b/components/icons/src/icons/chevron-up.tsx deleted file mode 100644 index 256e287..0000000 --- a/components/icons/src/icons/chevron-up.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ChevronUp: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle-add.tsx b/components/icons/src/icons/circle-add.tsx deleted file mode 100644 index 9a43bbd..0000000 --- a/components/icons/src/icons/circle-add.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CircleAdd: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle-check.tsx b/components/icons/src/icons/circle-check.tsx deleted file mode 100644 index 1d9ef39..0000000 --- a/components/icons/src/icons/circle-check.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CircleCheck: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle-close.tsx b/components/icons/src/icons/circle-close.tsx deleted file mode 100644 index 797c4ae..0000000 --- a/components/icons/src/icons/circle-close.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CircleClose: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle-head-fill.tsx b/components/icons/src/icons/circle-head-fill.tsx deleted file mode 100644 index 5d82954..0000000 --- a/components/icons/src/icons/circle-head-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CircleHeadFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle-head.tsx b/components/icons/src/icons/circle-head.tsx deleted file mode 100644 index 61e8244..0000000 --- a/components/icons/src/icons/circle-head.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CircleHead: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle-info.tsx b/components/icons/src/icons/circle-info.tsx deleted file mode 100644 index 9863655..0000000 --- a/components/icons/src/icons/circle-info.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CircleInfo: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle-remove.tsx b/components/icons/src/icons/circle-remove.tsx deleted file mode 100644 index f65dac8..0000000 --- a/components/icons/src/icons/circle-remove.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CircleRemove: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle-tail-fill.tsx b/components/icons/src/icons/circle-tail-fill.tsx deleted file mode 100644 index c776288..0000000 --- a/components/icons/src/icons/circle-tail-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CircleTailFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle-tail.tsx b/components/icons/src/icons/circle-tail.tsx deleted file mode 100644 index 0a588b9..0000000 --- a/components/icons/src/icons/circle-tail.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CircleTail: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/circle.tsx b/components/icons/src/icons/circle.tsx deleted file mode 100644 index 6755ff7..0000000 --- a/components/icons/src/icons/circle.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Circle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/clear-form.tsx b/components/icons/src/icons/clear-form.tsx deleted file mode 100644 index 6161022..0000000 --- a/components/icons/src/icons/clear-form.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ClearForm: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/clear-format.tsx b/components/icons/src/icons/clear-format.tsx deleted file mode 100644 index c5a02a8..0000000 --- a/components/icons/src/icons/clear-format.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ClearFormat: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/clear-rules.tsx b/components/icons/src/icons/clear-rules.tsx deleted file mode 100644 index b0443e6..0000000 --- a/components/icons/src/icons/clear-rules.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ClearRules: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/clock.tsx b/components/icons/src/icons/clock.tsx deleted file mode 100644 index b5d3e90..0000000 --- a/components/icons/src/icons/clock.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Clock: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/close.tsx b/components/icons/src/icons/close.tsx deleted file mode 100644 index 24f2098..0000000 --- a/components/icons/src/icons/close.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Close: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/code-view.tsx b/components/icons/src/icons/code-view.tsx deleted file mode 100644 index cc55dd7..0000000 --- a/components/icons/src/icons/code-view.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CodeView: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/collapse-2.tsx b/components/icons/src/icons/collapse.tsx similarity index 88% rename from components/icons/src/icons/collapse-2.tsx rename to components/icons/src/icons/collapse.tsx index b023ba6..13bdc55 100644 --- a/components/icons/src/icons/collapse-2.tsx +++ b/components/icons/src/icons/collapse.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Collapse2: IconComponent = createIcon(path); +export const CollapseIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/color-scales.tsx b/components/icons/src/icons/color-scales.tsx deleted file mode 100644 index 34ae659..0000000 --- a/components/icons/src/icons/color-scales.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ColorScales: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/columns.tsx b/components/icons/src/icons/columns.tsx deleted file mode 100644 index a0d75b1..0000000 --- a/components/icons/src/icons/columns.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Columns: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/combo-box.tsx b/components/icons/src/icons/combo-box.tsx deleted file mode 100644 index e6f2a44..0000000 --- a/components/icons/src/icons/combo-box.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ComboBox: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/comment-add.tsx b/components/icons/src/icons/comment-add.tsx deleted file mode 100644 index ab0bbe8..0000000 --- a/components/icons/src/icons/comment-add.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CommentAdd: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/comment-reopen.tsx b/components/icons/src/icons/comment-reopen.tsx deleted file mode 100644 index 73c211e..0000000 --- a/components/icons/src/icons/comment-reopen.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CommentReopen: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/comment-resolve.tsx b/components/icons/src/icons/comment-resolve.tsx deleted file mode 100644 index c4316a8..0000000 --- a/components/icons/src/icons/comment-resolve.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CommentResolve: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/comment-show.tsx b/components/icons/src/icons/comment-show.tsx deleted file mode 100644 index d4b43aa..0000000 --- a/components/icons/src/icons/comment-show.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CommentShow: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/conditional-formatting-large.tsx b/components/icons/src/icons/conditional-formatting-large.tsx deleted file mode 100644 index a205cba..0000000 --- a/components/icons/src/icons/conditional-formatting-large.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ConditionalFormattingLarge: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/conditional-formatting.tsx b/components/icons/src/icons/conditional-formatting.tsx deleted file mode 100644 index 0052b3b..0000000 --- a/components/icons/src/icons/conditional-formatting.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ConditionalFormatting: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/content-control.tsx b/components/icons/src/icons/content-control.tsx deleted file mode 100644 index d69d69d..0000000 --- a/components/icons/src/icons/content-control.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ContentControl: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/continue-numbering.tsx b/components/icons/src/icons/continue-numbering.tsx deleted file mode 100644 index 5f87bdc..0000000 --- a/components/icons/src/icons/continue-numbering.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ContinueNumbering: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/continuous-page-break.tsx b/components/icons/src/icons/continuous-page-break.tsx deleted file mode 100644 index ab35328..0000000 --- a/components/icons/src/icons/continuous-page-break.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ContinuousPageBreak: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/contrast.tsx b/components/icons/src/icons/contrast.tsx deleted file mode 100644 index e057d06..0000000 --- a/components/icons/src/icons/contrast.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Contrast: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/copy.tsx b/components/icons/src/icons/copy.tsx deleted file mode 100644 index 4f0b434..0000000 --- a/components/icons/src/icons/copy.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Copy: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/critical-path.tsx b/components/icons/src/icons/critical-path.tsx deleted file mode 100644 index f17dde2..0000000 --- a/components/icons/src/icons/critical-path.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const CriticalPath: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/crop.tsx b/components/icons/src/icons/crop.tsx deleted file mode 100644 index 9c3bc65..0000000 --- a/components/icons/src/icons/crop.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Crop: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/cut.tsx b/components/icons/src/icons/cut.tsx deleted file mode 100644 index ac4d0f3..0000000 --- a/components/icons/src/icons/cut.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Cut: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/data-bars.tsx b/components/icons/src/icons/data-bars.tsx deleted file mode 100644 index 3486e36..0000000 --- a/components/icons/src/icons/data-bars.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DataBars: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/data-validation.tsx b/components/icons/src/icons/data-validation.tsx deleted file mode 100644 index 6f99ce8..0000000 --- a/components/icons/src/icons/data-validation.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DataValidation: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/date-occurring.tsx b/components/icons/src/icons/date-occurring.tsx deleted file mode 100644 index a1014b2..0000000 --- a/components/icons/src/icons/date-occurring.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DateOccurring: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/day.tsx b/components/icons/src/icons/day.tsx deleted file mode 100644 index 40f4d2d..0000000 --- a/components/icons/src/icons/day.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Day: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/decrease-indent-rtl.tsx b/components/icons/src/icons/decrease-indent-rtl.tsx deleted file mode 100644 index 5ff33ea..0000000 --- a/components/icons/src/icons/decrease-indent-rtl.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DecreaseIndentRtl: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/decrease-indent.tsx b/components/icons/src/icons/decrease-indent.tsx deleted file mode 100644 index 7c6f79a..0000000 --- a/components/icons/src/icons/decrease-indent.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DecreaseIndent: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/delete-column.tsx b/components/icons/src/icons/delete-column.tsx deleted file mode 100644 index d7dd8a3..0000000 --- a/components/icons/src/icons/delete-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DeleteColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/delete-notes.tsx b/components/icons/src/icons/delete-notes.tsx deleted file mode 100644 index 86f176f..0000000 --- a/components/icons/src/icons/delete-notes.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DeleteNotes: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/delete-row.tsx b/components/icons/src/icons/delete-row.tsx deleted file mode 100644 index fa1e0f4..0000000 --- a/components/icons/src/icons/delete-row.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DeleteRow: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/description.tsx b/components/icons/src/icons/description.tsx deleted file mode 100644 index 03c71e2..0000000 --- a/components/icons/src/icons/description.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Description: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/dimension.tsx b/components/icons/src/icons/dimension.tsx deleted file mode 100644 index ace34ce..0000000 --- a/components/icons/src/icons/dimension.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Dimension: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/display.tsx b/components/icons/src/icons/display.tsx deleted file mode 100644 index abdfe88..0000000 --- a/components/icons/src/icons/display.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Display: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/double-check.tsx b/components/icons/src/icons/double-check.tsx deleted file mode 100644 index 4ae89ec..0000000 --- a/components/icons/src/icons/double-check.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DoubleCheck: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/download.tsx b/components/icons/src/icons/download.tsx deleted file mode 100644 index 59f6b9b..0000000 --- a/components/icons/src/icons/download.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Download: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/drag-and-drop-indicator.tsx b/components/icons/src/icons/drag-and-drop-indicator.tsx deleted file mode 100644 index ae2f53b..0000000 --- a/components/icons/src/icons/drag-and-drop-indicator.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DragAndDropIndicator: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/drag-and-drop.tsx b/components/icons/src/icons/drag-and-drop.tsx deleted file mode 100644 index bd57321..0000000 --- a/components/icons/src/icons/drag-and-drop.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DragAndDrop: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/drag-fill.tsx b/components/icons/src/icons/drag-fill.tsx deleted file mode 100644 index 89c28e7..0000000 --- a/components/icons/src/icons/drag-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DragFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/drop-down.tsx b/components/icons/src/icons/drop-down.tsx deleted file mode 100644 index 6c83fc2..0000000 --- a/components/icons/src/icons/drop-down.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DropDown: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/dropdown-list.tsx b/components/icons/src/icons/dropdown-list.tsx deleted file mode 100644 index 8205436..0000000 --- a/components/icons/src/icons/dropdown-list.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DropdownList: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/duplicate-cell.tsx b/components/icons/src/icons/duplicate-cell.tsx deleted file mode 100644 index f70da01..0000000 --- a/components/icons/src/icons/duplicate-cell.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const DuplicateCell: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/duplicate.tsx b/components/icons/src/icons/duplicate.tsx deleted file mode 100644 index a3b42ff..0000000 --- a/components/icons/src/icons/duplicate.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Duplicate: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/edit-notes.tsx b/components/icons/src/icons/edit-notes.tsx deleted file mode 100644 index 515b584..0000000 --- a/components/icons/src/icons/edit-notes.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const EditNotes: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/edit.tsx b/components/icons/src/icons/edit.tsx deleted file mode 100644 index 6cc464a..0000000 --- a/components/icons/src/icons/edit.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Edit: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/elaborate.tsx b/components/icons/src/icons/elaborate.tsx deleted file mode 100644 index e0dbd20..0000000 --- a/components/icons/src/icons/elaborate.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Elaborate: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/emoji.tsx b/components/icons/src/icons/emoji.tsx deleted file mode 100644 index 75db695..0000000 --- a/components/icons/src/icons/emoji.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Emoji: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/end-footnote.tsx b/components/icons/src/icons/end-footnote.tsx deleted file mode 100644 index 831db1a..0000000 --- a/components/icons/src/icons/end-footnote.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const EndFootnote: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/equalto.tsx b/components/icons/src/icons/equalto.tsx deleted file mode 100644 index a2b8884..0000000 --- a/components/icons/src/icons/equalto.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Equalto: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/erase.tsx b/components/icons/src/icons/erase.tsx deleted file mode 100644 index 95591bc..0000000 --- a/components/icons/src/icons/erase.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Erase: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/error-treeview.tsx b/components/icons/src/icons/error-treeview.tsx deleted file mode 100644 index 6a722e0..0000000 --- a/components/icons/src/icons/error-treeview.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ErrorTreeview: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/even-page-break.tsx b/components/icons/src/icons/even-page-break.tsx deleted file mode 100644 index 834b287..0000000 --- a/components/icons/src/icons/even-page-break.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const EvenPageBreak: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/exit-full-screen.tsx b/components/icons/src/icons/exit-full-screen.tsx deleted file mode 100644 index 0e02a8d..0000000 --- a/components/icons/src/icons/exit-full-screen.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ExitFullScreen: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/expand.tsx b/components/icons/src/icons/expand.tsx deleted file mode 100644 index 114cdb8..0000000 --- a/components/icons/src/icons/expand.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Expand: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/export-csv.tsx b/components/icons/src/icons/export-csv.tsx deleted file mode 100644 index 7ded746..0000000 --- a/components/icons/src/icons/export-csv.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ExportCsv: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/export-excel.tsx b/components/icons/src/icons/export-excel.tsx deleted file mode 100644 index 9d484cf..0000000 --- a/components/icons/src/icons/export-excel.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ExportExcel: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/export-pdf-1.tsx b/components/icons/src/icons/export-pdf-arrow.tsx similarity index 96% rename from components/icons/src/icons/export-pdf-1.tsx rename to components/icons/src/icons/export-pdf-arrow.tsx index f97faf1..2e27407 100644 --- a/components/icons/src/icons/export-pdf-1.tsx +++ b/components/icons/src/icons/export-pdf-arrow.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const ExportPdf1: IconComponent = createIcon(path); +export const ExportPdfArrowIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/export-pdf.tsx b/components/icons/src/icons/export-pdf.tsx deleted file mode 100644 index ff0a367..0000000 --- a/components/icons/src/icons/export-pdf.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ExportPdf: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/export-png.tsx b/components/icons/src/icons/export-png.tsx deleted file mode 100644 index 884cd47..0000000 --- a/components/icons/src/icons/export-png.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ExportPng: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/export-svg.tsx b/components/icons/src/icons/export-svg.tsx deleted file mode 100644 index d81aafb..0000000 --- a/components/icons/src/icons/export-svg.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ExportSvg: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/export-word-1.tsx b/components/icons/src/icons/export-word.tsx similarity index 88% rename from components/icons/src/icons/export-word-1.tsx rename to components/icons/src/icons/export-word.tsx index 321f187..10043a2 100644 --- a/components/icons/src/icons/export-word-1.tsx +++ b/components/icons/src/icons/export-word.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const ExportWord1: IconComponent = createIcon(path); +export const ExportWordIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/export-xls.tsx b/components/icons/src/icons/export-xls.tsx deleted file mode 100644 index 03ceec9..0000000 --- a/components/icons/src/icons/export-xls.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ExportXls: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/export.tsx b/components/icons/src/icons/export.tsx deleted file mode 100644 index 2d3e318..0000000 --- a/components/icons/src/icons/export.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Export: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/eye-slash.tsx b/components/icons/src/icons/eye-slash.tsx deleted file mode 100644 index a1b5a78..0000000 --- a/components/icons/src/icons/eye-slash.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const EyeSlash: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/eye.tsx b/components/icons/src/icons/eye.tsx deleted file mode 100644 index 21009d1..0000000 --- a/components/icons/src/icons/eye.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Eye: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/fade.tsx b/components/icons/src/icons/fade.tsx deleted file mode 100644 index 952c89d..0000000 --- a/components/icons/src/icons/fade.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Fade: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/field-settings.tsx b/components/icons/src/icons/field-settings.tsx deleted file mode 100644 index b4b5c89..0000000 --- a/components/icons/src/icons/field-settings.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FieldSettings: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/file-document.tsx b/components/icons/src/icons/file-document.tsx deleted file mode 100644 index a38206d..0000000 --- a/components/icons/src/icons/file-document.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FileDocument: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/file-new.tsx b/components/icons/src/icons/file-new.tsx deleted file mode 100644 index 1394ff1..0000000 --- a/components/icons/src/icons/file-new.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FileNew: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/filter-active.tsx b/components/icons/src/icons/filter-active.tsx deleted file mode 100644 index f142f31..0000000 --- a/components/icons/src/icons/filter-active.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FilterActive: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/filter-clear.tsx b/components/icons/src/icons/filter-clear.tsx deleted file mode 100644 index ec57053..0000000 --- a/components/icons/src/icons/filter-clear.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FilterClear: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/filter-main.tsx b/components/icons/src/icons/filter-main.tsx deleted file mode 100644 index 7935705..0000000 --- a/components/icons/src/icons/filter-main.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FilterMain: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/filter.tsx b/components/icons/src/icons/filter.tsx deleted file mode 100644 index ec2f7c6..0000000 --- a/components/icons/src/icons/filter.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Filter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/filtered-sort-ascending.tsx b/components/icons/src/icons/filtered-sort-ascending.tsx deleted file mode 100644 index 2e71f14..0000000 --- a/components/icons/src/icons/filtered-sort-ascending.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FilteredSortAscending: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/filtered-sort-descending.tsx b/components/icons/src/icons/filtered-sort-descending.tsx deleted file mode 100644 index da2bef2..0000000 --- a/components/icons/src/icons/filtered-sort-descending.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FilteredSortDescending: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/filtered.tsx b/components/icons/src/icons/filtered.tsx deleted file mode 100644 index 37f9e6d..0000000 --- a/components/icons/src/icons/filtered.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Filtered: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/filters.tsx b/components/icons/src/icons/filters.tsx deleted file mode 100644 index d438558..0000000 --- a/components/icons/src/icons/filters.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Filters: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/first-page.tsx b/components/icons/src/icons/first-page.tsx deleted file mode 100644 index 9542557..0000000 --- a/components/icons/src/icons/first-page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FirstPage: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/fixed-column-width.tsx b/components/icons/src/icons/fixed-column-width.tsx deleted file mode 100644 index d2dba18..0000000 --- a/components/icons/src/icons/fixed-column-width.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FixedColumnWidth: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/flags.tsx b/components/icons/src/icons/flags.tsx deleted file mode 100644 index ca845a5..0000000 --- a/components/icons/src/icons/flags.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Flags: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/flip-horizontal.tsx b/components/icons/src/icons/flip-horizontal.tsx deleted file mode 100644 index 18fe730..0000000 --- a/components/icons/src/icons/flip-horizontal.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FlipHorizontal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/flip-vertical.tsx b/components/icons/src/icons/flip-vertical.tsx deleted file mode 100644 index b5522a0..0000000 --- a/components/icons/src/icons/flip-vertical.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FlipVertical: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/folder-open.tsx b/components/icons/src/icons/folder-open.tsx deleted file mode 100644 index e91839c..0000000 --- a/components/icons/src/icons/folder-open.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FolderOpen: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/folder.tsx b/components/icons/src/icons/folder.tsx deleted file mode 100644 index 2084d9d..0000000 --- a/components/icons/src/icons/folder.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Folder: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/font-color.tsx b/components/icons/src/icons/font-color.tsx deleted file mode 100644 index f11c1de..0000000 --- a/components/icons/src/icons/font-color.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FontColor: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/font-name.tsx b/components/icons/src/icons/font-name.tsx deleted file mode 100644 index 02d9217..0000000 --- a/components/icons/src/icons/font-name.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FontName: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/font-size.tsx b/components/icons/src/icons/font-size.tsx deleted file mode 100644 index d4f511c..0000000 --- a/components/icons/src/icons/font-size.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FontSize: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/food-and-drinks.tsx b/components/icons/src/icons/food-and-drinks.tsx deleted file mode 100644 index a84c53b..0000000 --- a/components/icons/src/icons/food-and-drinks.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FoodAndDrinks: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/footer.tsx b/components/icons/src/icons/footer.tsx deleted file mode 100644 index 39db4a7..0000000 --- a/components/icons/src/icons/footer.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Footer: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/form-field.tsx b/components/icons/src/icons/form-field.tsx deleted file mode 100644 index d737124..0000000 --- a/components/icons/src/icons/form-field.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FormField: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/format-painter.tsx b/components/icons/src/icons/format-painter.tsx deleted file mode 100644 index 2716337..0000000 --- a/components/icons/src/icons/format-painter.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FormatPainter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-bevel.tsx b/components/icons/src/icons/frame-bevel.tsx deleted file mode 100644 index 860dd61..0000000 --- a/components/icons/src/icons/frame-bevel.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FrameBevel: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-custom.tsx b/components/icons/src/icons/frame-custom.tsx deleted file mode 100644 index fd2df75..0000000 --- a/components/icons/src/icons/frame-custom.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FrameCustom: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-11.tsx b/components/icons/src/icons/frame-extra-tall.tsx similarity index 77% rename from components/icons/src/icons/frame-11.tsx rename to components/icons/src/icons/frame-extra-tall.tsx index d68f175..61f0b5a 100644 --- a/components/icons/src/icons/frame-11.tsx +++ b/components/icons/src/icons/frame-extra-tall.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame11: IconComponent = createIcon(path); +export const FrameExtraTallIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-5.tsx b/components/icons/src/icons/frame-full-width.tsx similarity index 82% rename from components/icons/src/icons/frame-5.tsx rename to components/icons/src/icons/frame-full-width.tsx index 9e6ee77..99458ad 100644 --- a/components/icons/src/icons/frame-5.tsx +++ b/components/icons/src/icons/frame-full-width.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame5: IconComponent = createIcon(path); +export const FrameFullWidthIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-hook.tsx b/components/icons/src/icons/frame-hook.tsx deleted file mode 100644 index 9c3b31e..0000000 --- a/components/icons/src/icons/frame-hook.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FrameHook: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-2.tsx b/components/icons/src/icons/frame-horizontal-narrow.tsx similarity index 80% rename from components/icons/src/icons/frame-2.tsx rename to components/icons/src/icons/frame-horizontal-narrow.tsx index 5990fba..ec5e59c 100644 --- a/components/icons/src/icons/frame-2.tsx +++ b/components/icons/src/icons/frame-horizontal-narrow.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame2: IconComponent = createIcon(path); +export const FrameHorizontalNarrowIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-inset.tsx b/components/icons/src/icons/frame-inset.tsx deleted file mode 100644 index 7b0dfb8..0000000 --- a/components/icons/src/icons/frame-inset.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FrameInset: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-4.tsx b/components/icons/src/icons/frame-large.tsx similarity index 83% rename from components/icons/src/icons/frame-4.tsx rename to components/icons/src/icons/frame-large.tsx index 4ea3a97..ffd65c3 100644 --- a/components/icons/src/icons/frame-4.tsx +++ b/components/icons/src/icons/frame-large.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame4: IconComponent = createIcon(path); +export const FrameLargeIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-line.tsx b/components/icons/src/icons/frame-line.tsx deleted file mode 100644 index d3c54f1..0000000 --- a/components/icons/src/icons/frame-line.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FrameLine: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-mat.tsx b/components/icons/src/icons/frame-mat.tsx deleted file mode 100644 index f4af887..0000000 --- a/components/icons/src/icons/frame-mat.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FrameMat: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-3.tsx b/components/icons/src/icons/frame-medium.tsx similarity index 83% rename from components/icons/src/icons/frame-3.tsx rename to components/icons/src/icons/frame-medium.tsx index 508f2db..7e44aeb 100644 --- a/components/icons/src/icons/frame-3.tsx +++ b/components/icons/src/icons/frame-medium.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame3: IconComponent = createIcon(path); +export const FrameMediumIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-6.tsx b/components/icons/src/icons/frame-narrow.tsx similarity index 83% rename from components/icons/src/icons/frame-6.tsx rename to components/icons/src/icons/frame-narrow.tsx index 9699b74..7e62c58 100644 --- a/components/icons/src/icons/frame-6.tsx +++ b/components/icons/src/icons/frame-narrow.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame6: IconComponent = createIcon(path); +export const FrameNarrowIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-none.tsx b/components/icons/src/icons/frame-none.tsx deleted file mode 100644 index 5e6554c..0000000 --- a/components/icons/src/icons/frame-none.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FrameNone: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-1.tsx b/components/icons/src/icons/frame-square.tsx similarity index 83% rename from components/icons/src/icons/frame-1.tsx rename to components/icons/src/icons/frame-square.tsx index 66b86f8..65288c1 100644 --- a/components/icons/src/icons/frame-1.tsx +++ b/components/icons/src/icons/frame-square.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame1: IconComponent = createIcon(path); +export const FrameSquareIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-10.tsx b/components/icons/src/icons/frame-tall.tsx similarity index 78% rename from components/icons/src/icons/frame-10.tsx rename to components/icons/src/icons/frame-tall.tsx index 91e63b7..62092c7 100644 --- a/components/icons/src/icons/frame-10.tsx +++ b/components/icons/src/icons/frame-tall.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame10: IconComponent = createIcon(path); +export const FrameTallIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-7.tsx b/components/icons/src/icons/frame-vertical-narrow.tsx similarity index 75% rename from components/icons/src/icons/frame-7.tsx rename to components/icons/src/icons/frame-vertical-narrow.tsx index 71d694d..bf42079 100644 --- a/components/icons/src/icons/frame-7.tsx +++ b/components/icons/src/icons/frame-vertical-narrow.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame7: IconComponent = createIcon(path); +export const FrameVerticalNarrowIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-9.tsx b/components/icons/src/icons/frame-vertical-wide.tsx similarity index 76% rename from components/icons/src/icons/frame-9.tsx rename to components/icons/src/icons/frame-vertical-wide.tsx index 58ce6e7..df70b21 100644 --- a/components/icons/src/icons/frame-9.tsx +++ b/components/icons/src/icons/frame-vertical-wide.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame9: IconComponent = createIcon(path); +export const FrameVerticalWideIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/frame-8.tsx b/components/icons/src/icons/frame-vertical.tsx similarity index 77% rename from components/icons/src/icons/frame-8.tsx rename to components/icons/src/icons/frame-vertical.tsx index b759a97..39bce73 100644 --- a/components/icons/src/icons/frame-8.tsx +++ b/components/icons/src/icons/frame-vertical.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Frame8: IconComponent = createIcon(path); +export const FrameVerticalIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/freeze-column.tsx b/components/icons/src/icons/freeze-column.tsx deleted file mode 100644 index 343d90b..0000000 --- a/components/icons/src/icons/freeze-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FreezeColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/freeze-pane.tsx b/components/icons/src/icons/freeze-pane.tsx deleted file mode 100644 index 03cbfe3..0000000 --- a/components/icons/src/icons/freeze-pane.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FreezePane: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/freeze-row.tsx b/components/icons/src/icons/freeze-row.tsx deleted file mode 100644 index 954bc49..0000000 --- a/components/icons/src/icons/freeze-row.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FreezeRow: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/full-screen.tsx b/components/icons/src/icons/full-screen.tsx deleted file mode 100644 index f75ef0a..0000000 --- a/components/icons/src/icons/full-screen.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const FullScreen: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/gantt-gripper.tsx b/components/icons/src/icons/gantt-gripper.tsx deleted file mode 100644 index dea01b2..0000000 --- a/components/icons/src/icons/gantt-gripper.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const GanttGripper: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/grain.tsx b/components/icons/src/icons/grain.tsx deleted file mode 100644 index 5cb9cc9..0000000 --- a/components/icons/src/icons/grain.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Grain: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/grammar-check.tsx b/components/icons/src/icons/grammar-check.tsx deleted file mode 100644 index ac1b332..0000000 --- a/components/icons/src/icons/grammar-check.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const GrammarCheck: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/grand-total.tsx b/components/icons/src/icons/grand-total.tsx deleted file mode 100644 index 959cf36..0000000 --- a/components/icons/src/icons/grand-total.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const GrandTotal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/greater-than.tsx b/components/icons/src/icons/greater-than.tsx deleted file mode 100644 index 06e5b57..0000000 --- a/components/icons/src/icons/greater-than.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const GreaterThan: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/grid-view.tsx b/components/icons/src/icons/grid-view.tsx deleted file mode 100644 index a79cd0a..0000000 --- a/components/icons/src/icons/grid-view.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const GridView: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/grip-vertical.tsx b/components/icons/src/icons/grip-vertical.tsx deleted file mode 100644 index feb83e1..0000000 --- a/components/icons/src/icons/grip-vertical.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const GripVertical: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/group-2.tsx b/components/icons/src/icons/group-combine.tsx similarity index 91% rename from components/icons/src/icons/group-2.tsx rename to components/icons/src/icons/group-combine.tsx index d7e1d1f..60cf8f9 100644 --- a/components/icons/src/icons/group-2.tsx +++ b/components/icons/src/icons/group-combine.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Group2: IconComponent = createIcon(path); +export const GroupCombineIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/group-icon.tsx b/components/icons/src/icons/group-icon.tsx deleted file mode 100644 index 9635487..0000000 --- a/components/icons/src/icons/group-icon.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const GroupIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/group-1.tsx b/components/icons/src/icons/group-items.tsx similarity index 93% rename from components/icons/src/icons/group-1.tsx rename to components/icons/src/icons/group-items.tsx index 5284e6f..67596cc 100644 --- a/components/icons/src/icons/group-1.tsx +++ b/components/icons/src/icons/group-items.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Group1: IconComponent = createIcon(path); +export const GroupItemsIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/hand-gestures.tsx b/components/icons/src/icons/hand-gestures.tsx deleted file mode 100644 index 467f619..0000000 --- a/components/icons/src/icons/hand-gestures.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const HandGestures: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/header.tsx b/components/icons/src/icons/header.tsx deleted file mode 100644 index 2d7a0cb..0000000 --- a/components/icons/src/icons/header.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Header: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/hide-formula-bar.tsx b/components/icons/src/icons/hide-formula-bar.tsx deleted file mode 100644 index bb8bb7b..0000000 --- a/components/icons/src/icons/hide-formula-bar.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const HideFormulaBar: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/hide-gridlines.tsx b/components/icons/src/icons/hide-gridlines.tsx deleted file mode 100644 index c9f818f..0000000 --- a/components/icons/src/icons/hide-gridlines.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const HideGridlines: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/hide-headings.tsx b/components/icons/src/icons/hide-headings.tsx deleted file mode 100644 index 9a26251..0000000 --- a/components/icons/src/icons/hide-headings.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const HideHeadings: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/highlight-color.tsx b/components/icons/src/icons/highlight-color.tsx deleted file mode 100644 index f776066..0000000 --- a/components/icons/src/icons/highlight-color.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const HighlightColor: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/highlight.tsx b/components/icons/src/icons/highlight.tsx deleted file mode 100644 index 1b8d3f1..0000000 --- a/components/icons/src/icons/highlight.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Highlight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/home.tsx b/components/icons/src/icons/home.tsx deleted file mode 100644 index ad38af1..0000000 --- a/components/icons/src/icons/home.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Home: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/hyperlink-copy.tsx b/components/icons/src/icons/hyperlink-copy.tsx deleted file mode 100644 index e184454..0000000 --- a/components/icons/src/icons/hyperlink-copy.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const HyperlinkCopy: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/hyperlink-edit.tsx b/components/icons/src/icons/hyperlink-edit.tsx deleted file mode 100644 index 9561fff..0000000 --- a/components/icons/src/icons/hyperlink-edit.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const HyperlinkEdit: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/hyperlink-open.tsx b/components/icons/src/icons/hyperlink-open.tsx deleted file mode 100644 index e93cd13..0000000 --- a/components/icons/src/icons/hyperlink-open.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const HyperlinkOpen: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/hyperlink-remove.tsx b/components/icons/src/icons/hyperlink-remove.tsx deleted file mode 100644 index 3a5affa..0000000 --- a/components/icons/src/icons/hyperlink-remove.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const HyperlinkRemove: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/iconsets.tsx b/components/icons/src/icons/iconsets.tsx deleted file mode 100644 index 197f669..0000000 --- a/components/icons/src/icons/iconsets.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Iconsets: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/caption-1.tsx b/components/icons/src/icons/image-caption.tsx similarity index 85% rename from components/icons/src/icons/caption-1.tsx rename to components/icons/src/icons/image-caption.tsx index 43278b6..3ab74f2 100644 --- a/components/icons/src/icons/caption-1.tsx +++ b/components/icons/src/icons/image-caption.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Caption1: IconComponent = createIcon(path); +export const ImageCaptionIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/image.tsx b/components/icons/src/icons/image.tsx deleted file mode 100644 index c9cce8d..0000000 --- a/components/icons/src/icons/image.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Image: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/import-1.tsx b/components/icons/src/icons/import-turbo.tsx similarity index 94% rename from components/icons/src/icons/import-1.tsx rename to components/icons/src/icons/import-turbo.tsx index 9c523e5..4420969 100644 --- a/components/icons/src/icons/import-1.tsx +++ b/components/icons/src/icons/import-turbo.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Import1: IconComponent = createIcon(path); +export const ImportTurboIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/import-word.tsx b/components/icons/src/icons/import-word.tsx deleted file mode 100644 index 0ee21f8..0000000 --- a/components/icons/src/icons/import-word.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ImportWord: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/import.tsx b/components/icons/src/icons/import.tsx deleted file mode 100644 index 188c2a1..0000000 --- a/components/icons/src/icons/import.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Import: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/increase-indent-rtl.tsx b/components/icons/src/icons/increase-indent-rtl.tsx deleted file mode 100644 index 884c8ff..0000000 --- a/components/icons/src/icons/increase-indent-rtl.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const IncreaseIndentRtl: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/increase-indent.tsx b/components/icons/src/icons/increase-indent.tsx deleted file mode 100644 index a632ab7..0000000 --- a/components/icons/src/icons/increase-indent.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const IncreaseIndent: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/index.ts b/components/icons/src/icons/index.ts deleted file mode 100644 index 0910f00..0000000 --- a/components/icons/src/icons/index.ts +++ /dev/null @@ -1,544 +0,0 @@ -export * from './above-average'; -export * from './activities'; -export * from './add-chart-element'; -export * from './add-notes'; -export * from './adjustment'; -export * from './agenda-date-range'; -export * from './ai-chat'; -export * from './align-bottom'; -export * from './align-center'; -export * from './align-left'; -export * from './align-middle'; -export * from './align-right'; -export * from './align-top'; -export * from './all'; -export * from './animals'; -export * from './annotation-edit'; -export * from './area'; -export * from './arrow-down'; -export * from './arrow-head-fill'; -export * from './arrow-head'; -export * from './arrow-left'; -export * from './arrow-right-up'; -export * from './arrow-right'; -export * from './arrow-tail-fill'; -export * from './arrow-tail'; -export * from './arrow-up'; -export * from './audio'; -export * from './auto-fit-all-column'; -export * from './auto-fit-column'; -export * from './auto-fit-content'; -export * from './auto-fit-window'; -export * from './bar-head'; -export * from './bar-tail'; -export * from './below-average'; -export * from './between'; -export * from './blockquote'; -export * from './bold'; -export * from './bookmark'; -export * from './border-all'; -export * from './border-bottom'; -export * from './border-box'; -export * from './border-center'; -export * from './border-custom'; -export * from './border-diagonal-1'; -export * from './border-diagonal-2'; -export * from './border-diagonal-down'; -export * from './border-diagonal-up'; -export * from './border-frame'; -export * from './border-inner'; -export * from './border-left'; -export * from './border-middle'; -export * from './border-none-1'; -export * from './border-none'; -export * from './border-outer'; -export * from './border-right'; -export * from './border-shadow-1'; -export * from './border-shadow-2'; -export * from './border-top'; -export * from './bottom-10-items'; -export * from './bottom-10'; -export * from './box'; -export * from './break-page'; -export * from './break-section'; -export * from './break'; -export * from './brightness'; -export * from './bring-forward'; -export * from './bring-to-center'; -export * from './bring-to-front'; -export * from './bring-to-view'; -export * from './building-block'; -export * from './bullet-1'; -export * from './bullet-2'; -export * from './bullet-3'; -export * from './bullet-4'; -export * from './bullet-5'; -export * from './bullet-6'; -export * from './bullet-7'; -export * from './button-field'; -export * from './calculate-sheet'; -export * from './calculated-member'; -export * from './calculation'; -export * from './caption-1'; -export * from './caption'; -export * from './cell'; -export * from './change-case'; -export * from './change-scale-ratio'; -export * from './changes-accept'; -export * from './changes-reject'; -export * from './changes-track'; -export * from './character-style'; -export * from './chart-2d-100-percent-stacked-area'; -export * from './chart-2d-100-percent-stacked-bar'; -export * from './chart-2d-100-percent-stacked-column'; -export * from './chart-2d-100-percent-stacked-line-marked'; -export * from './chart-2d-100-percent-stacked-line'; -export * from './chart-2d-area'; -export * from './chart-2d-clustered-bar'; -export * from './chart-2d-clustered-column'; -export * from './chart-2d-line-marked'; -export * from './chart-2d-line'; -export * from './chart-2d-pie-2'; -export * from './chart-2d-stacked-area'; -export * from './chart-2d-stacked-bar'; -export * from './chart-2d-stacked-column'; -export * from './chart-2d-stacked-line-marked'; -export * from './chart-2d-stacked-line'; -export * from './chart-axes-primary-horizontal'; -export * from './chart-axes-primary-vertical'; -export * from './chart-axes'; -export * from './chart-axis-titles-primary-horizontal'; -export * from './chart-axis-titles-primary-vertical'; -export * from './chart-axis-titles'; -export * from './chart-data-labels-center'; -export * from './chart-data-labels-inside-base'; -export * from './chart-data-labels-inside-end'; -export * from './chart-data-labels-none'; -export * from './chart-data-labels-outside-end'; -export * from './chart-data-labels'; -export * from './chart-donut'; -export * from './chart-gridlines'; -export * from './chart-insert-bar'; -export * from './chart-insert-column'; -export * from './chart-insert-line'; -export * from './chart-insert-pie'; -export * from './chart-insert-x-y-scatter'; -export * from './chart-legend-bottom'; -export * from './chart-legend-left'; -export * from './chart-legend-none'; -export * from './chart-legend-right'; -export * from './chart-legend-top'; -export * from './chart-legend'; -export * from './chart-lines'; -export * from './chart-primary-major-horizontal'; -export * from './chart-primary-major-vertical'; -export * from './chart-primary-minor-horizontal'; -export * from './chart-primary-minor-vertical'; -export * from './chart-switch-row-column'; -export * from './chart-title-centered-overlay'; -export * from './chart-title-none'; -export * from './chart-title'; -export * from './chart'; -export * from './check-box'; -export * from './check-large'; -export * from './check-small'; -export * from './check-tick'; -export * from './check'; -export * from './chevron-down-double'; -export * from './chevron-down-fill'; -export * from './chevron-down-small'; -export * from './chevron-down'; -export * from './chevron-left-double'; -export * from './chevron-left-fill'; -export * from './chevron-left-small'; -export * from './chevron-left'; -export * from './chevron-right-double'; -export * from './chevron-right-fill'; -export * from './chevron-right-small'; -export * from './chevron-right'; -export * from './chevron-up-double'; -export * from './chevron-up-fill'; -export * from './chevron-up-small'; -export * from './chevron-up'; -export * from './circle-add'; -export * from './circle-check'; -export * from './circle-close'; -export * from './circle-head-fill'; -export * from './circle-head'; -export * from './circle-info'; -export * from './circle-remove'; -export * from './circle-tail-fill'; -export * from './circle-tail'; -export * from './circle'; -export * from './clear-form'; -export * from './clear-format'; -export * from './clear-rules'; -export * from './clock'; -export * from './close'; -export * from './code-view'; -export * from './collapse-2'; -export * from './color-scales'; -export * from './columns'; -export * from './combo-box'; -export * from './comment-add'; -export * from './comment-reopen'; -export * from './comment-resolve'; -export * from './comment-show'; -export * from './conditional-formatting-large'; -export * from './conditional-formatting'; -export * from './content-control'; -export * from './continue-numbering'; -export * from './continuous-page-break'; -export * from './contrast'; -export * from './copy'; -export * from './critical-path'; -export * from './crop'; -export * from './cut'; -export * from './data-bars'; -export * from './data-validation'; -export * from './date-occurring'; -export * from './day'; -export * from './decrease-indent-rtl'; -export * from './decrease-indent'; -export * from './delete-column'; -export * from './delete-notes'; -export * from './delete-row'; -export * from './description'; -export * from './dimension'; -export * from './display'; -export * from './double-check'; -export * from './download'; -export * from './drag-and-drop-indicator'; -export * from './drag-and-drop'; -export * from './drag-fill'; -export * from './drop-down'; -export * from './dropdown-list'; -export * from './duplicate-cell'; -export * from './duplicate'; -export * from './edit-notes'; -export * from './edit'; -export * from './elaborate'; -export * from './emoji'; -export * from './end-footnote'; -export * from './equalto'; -export * from './erase'; -export * from './error-treeview'; -export * from './even-page-break'; -export * from './exit-full-screen'; -export * from './expand'; -export * from './export-csv'; -export * from './export-excel'; -export * from './export-pdf-1'; -export * from './export-pdf'; -export * from './export-png'; -export * from './export-svg'; -export * from './export-word-1'; -export * from './export-xls'; -export * from './export'; -export * from './eye-slash'; -export * from './eye'; -export * from './fade'; -export * from './field-settings'; -export * from './file-document'; -export * from './file-new'; -export * from './filter-active'; -export * from './filter-clear'; -export * from './filter-main'; -export * from './filter'; -export * from './filtered-sort-ascending'; -export * from './filtered-sort-descending'; -export * from './filtered'; -export * from './filters'; -export * from './first-page'; -export * from './fixed-column-width'; -export * from './flags'; -export * from './flip-horizontal'; -export * from './flip-vertical'; -export * from './folder-open'; -export * from './folder'; -export * from './font-color'; -export * from './font-name'; -export * from './font-size'; -export * from './food-and-drinks'; -export * from './footer'; -export * from './form-field'; -export * from './format-painter'; -export * from './frame-1'; -export * from './frame-10'; -export * from './frame-11'; -export * from './frame-2'; -export * from './frame-3'; -export * from './frame-4'; -export * from './frame-5'; -export * from './frame-6'; -export * from './frame-7'; -export * from './frame-8'; -export * from './frame-9'; -export * from './frame-bevel'; -export * from './frame-custom'; -export * from './frame-hook'; -export * from './frame-inset'; -export * from './frame-line'; -export * from './frame-mat'; -export * from './frame-none'; -export * from './freeze-column'; -export * from './freeze-pane'; -export * from './freeze-row'; -export * from './full-screen'; -export * from './gantt-gripper'; -export * from './grain'; -export * from './grammar-check'; -export * from './grand-total'; -export * from './greater-than'; -export * from './grid-view'; -export * from './grip-vertical'; -export * from './group-1'; -export * from './group-2'; -export * from './group-icon'; -export * from './hand-gestures'; -export * from './header'; -export * from './hide-formula-bar'; -export * from './hide-gridlines'; -export * from './hide-headings'; -export * from './highlight-color'; -export * from './highlight'; -export * from './home'; -export * from './hyperlink-copy'; -export * from './hyperlink-edit'; -export * from './hyperlink-open'; -export * from './hyperlink-remove'; -export * from './iconsets'; -export * from './image'; -export * from './import-1'; -export * from './import-word'; -export * from './import'; -export * from './increase-indent-rtl'; -export * from './increase-indent'; -export * from './insert-above'; -export * from './insert-below'; -export * from './insert-code'; -export * from './insert-left'; -export * from './insert-right'; -export * from './insert-sheet'; -export * from './intermediate-state-2'; -export * from './intermediate-state'; -export * from './italic'; -export * from './justify'; -export * from './kpi'; -export * from './last-page'; -export * from './launcher'; -export * from './layers'; -export * from './length'; -export * from './less-than'; -export * from './level-1'; -export * from './level-2'; -export * from './level-3'; -export * from './level-4'; -export * from './level-5'; -export * from './line-normal'; -export * from './line-small'; -export * from './line-spacing'; -export * from './line-very-small'; -export * from './line'; -export * from './link-remove'; -export * from './link'; -export * from './linked-style'; -export * from './list-ordered-rtl'; -export * from './list-ordered'; -export * from './list-unordered-rtl'; -export * from './list-unordered'; -export * from './location'; -export * from './lock'; -export * from './lower-case'; -export * from './mdx'; -export * from './menu'; -export * from './merge-cells'; -export * from './microphone'; -export * from './month-agenda'; -export * from './month'; -export * from './more-chevron'; -export * from './more-horizontal-1'; -export * from './more-scatter-charts'; -export * from './more-vertical-1'; -export * from './more-vertical-2'; -export * from './mouse-pointer'; -export * from './multiple-comment-resolve'; -export * from './multiple-comment'; -export * from './named-set'; -export * from './nature'; -export * from './none'; -export * from './notes'; -export * from './number-formatting'; -export * from './objects'; -export * from './odd-page-break'; -export * from './opacity'; -export * from './open-link'; -export * from './order'; -export * from './organize-pdf'; -export * from './page-column-left'; -export * from './page-column-one'; -export * from './page-column-right'; -export * from './page-column-three'; -export * from './page-column-two'; -export * from './page-column'; -export * from './page-columns'; -export * from './page-numbering'; -export * from './page-setup'; -export * from './page-size'; -export * from './page-text-wrap'; -export * from './paint-bucket'; -export * from './pan'; -export * from './paragraph'; -export * from './password'; -export * from './paste-match-destination'; -export * from './paste-style'; -export * from './paste-text-only'; -export * from './paste'; -export * from './pause'; -export * from './pentagon'; -export * from './people'; -export * from './perimeter'; -export * from './play'; -export * from './plus'; -export * from './preformat-code'; -export * from './print-layout'; -export * from './print'; -export * from './properties-1'; -export * from './properties-2'; -export * from './protect-sheet'; -export * from './protect-workbook'; -export * from './radio-button'; -export * from './radius'; -export * from './reapply'; -export * from './rectangle'; -export * from './recurrence-edit'; -export * from './redact'; -export * from './redaction'; -export * from './redo'; -export * from './refresh'; -export * from './rename'; -export * from './repeat'; -export * from './repeating-section'; -export * from './rephrase'; -export * from './replace'; -export * from './reset'; -export * from './resize'; -export * from './resizer-horizontal'; -export * from './resizer-right'; -export * from './resizer-vertical'; -export * from './resizer'; -export * from './restart-at-1'; -export * from './saturation'; -export * from './save-as'; -export * from './save'; -export * from './search'; -export * from './select-all'; -export * from './selection'; -export * from './send-backward'; -export * from './send-to-back'; -export * from './send'; -export * from './settings'; -export * from './shapes'; -export * from './sharpness'; -export * from './shorten'; -export * from './show-hide-panel'; -export * from './signature'; -export * from './smart-paste'; -export * from './sort-ascending-2'; -export * from './sort-ascending'; -export * from './sort-descending-2'; -export * from './sort-descending'; -export * from './sorting-1'; -export * from './sorting-2'; -export * from './sorting-3'; -export * from './spacing-after'; -export * from './spacing-before'; -export * from './spell-check'; -export * from './split-horizontal'; -export * from './split-vertical'; -export * from './square-head-fill'; -export * from './square-head'; -export * from './square-tail-fill'; -export * from './square-tail'; -export * from './squiggly'; -export * from './stamp'; -export * from './star-filled'; -export * from './stop-rectangle'; -export * from './strikethrough'; -export * from './stroke-width'; -export * from './style'; -export * from './sub-total'; -export * from './subscript'; -export * from './sum'; -export * from './superscript'; -export * from './symbols'; -export * from './table-2'; -export * from './table-align-center'; -export * from './table-align-left'; -export * from './table-align-right'; -export * from './table-border-custom'; -export * from './table-cell-none'; -export * from './table-cell'; -export * from './table-delete'; -export * from './table-header'; -export * from './table-insert-column'; -export * from './table-insert-row'; -export * from './table-merge'; -export * from './table-nested'; -export * from './table-of-content'; -export * from './table-overwrite-cells'; -export * from './table-update'; -export * from './table'; -export * from './text-alternative'; -export * from './text-annotation'; -export * from './text-form'; -export * from './text-header'; -export * from './text-outline'; -export * from './text-that-contains'; -export * from './text-wrap'; -export * from './thumbnail'; -export * from './thumbs-down-fill'; -export * from './thumbs-down'; -export * from './thumbs-up-fill'; -export * from './thumbs-up'; -export * from './time-zone'; -export * from './timeline-day'; -export * from './timeline-month'; -export * from './timeline-today'; -export * from './timeline-week'; -export * from './timeline-work-week'; -export * from './tint'; -export * from './top-10'; -export * from './top-bottom-rules'; -export * from './transform-left'; -export * from './transform-right'; -export * from './transform'; -export * from './translate'; -export * from './trash'; -export * from './travel-and-places'; -export * from './triangle'; -export * from './two-column'; -export * from './two-row'; -export * from './underline'; -export * from './undo'; -export * from './unfiltered'; -export * from './ungroup-1'; -export * from './ungroup-2'; -export * from './unlock'; -export * from './upload-1'; -export * from './upper-case'; -export * from './user-defined'; -export * from './user'; -export * from './vertical-align-bottom'; -export * from './vertical-align-center'; -export * from './vertical-align-top'; -export * from './video'; -export * from './view-side'; -export * from './volume'; -export * from './warning'; -export * from './web-layout'; -export * from './week'; -export * from './xml-mapping'; -export * from './zoom-in'; -export * from './zoom-out'; -export * from './zoom-to-fit'; diff --git a/components/icons/src/icons/insert-above.tsx b/components/icons/src/icons/insert-above.tsx deleted file mode 100644 index bc808c8..0000000 --- a/components/icons/src/icons/insert-above.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const InsertAbove: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/insert-below.tsx b/components/icons/src/icons/insert-below.tsx deleted file mode 100644 index 2b5e004..0000000 --- a/components/icons/src/icons/insert-below.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const InsertBelow: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/insert-code.tsx b/components/icons/src/icons/insert-code.tsx deleted file mode 100644 index 68caca7..0000000 --- a/components/icons/src/icons/insert-code.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const InsertCode: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/insert-left.tsx b/components/icons/src/icons/insert-left.tsx deleted file mode 100644 index 0bdead5..0000000 --- a/components/icons/src/icons/insert-left.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const InsertLeft: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/insert-right.tsx b/components/icons/src/icons/insert-right.tsx deleted file mode 100644 index b898eef..0000000 --- a/components/icons/src/icons/insert-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const InsertRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/insert-sheet.tsx b/components/icons/src/icons/insert-sheet.tsx deleted file mode 100644 index cfdd87a..0000000 --- a/components/icons/src/icons/insert-sheet.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const InsertSheet: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/intermediate-state-2.tsx b/components/icons/src/icons/intermediate-bar.tsx similarity index 75% rename from components/icons/src/icons/intermediate-state-2.tsx rename to components/icons/src/icons/intermediate-bar.tsx index 900ace4..8892072 100644 --- a/components/icons/src/icons/intermediate-state-2.tsx +++ b/components/icons/src/icons/intermediate-bar.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const IntermediateState2: IconComponent = createIcon(path); +export const IntermediateBarIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/italic.tsx b/components/icons/src/icons/italic.tsx deleted file mode 100644 index 91661ce..0000000 --- a/components/icons/src/icons/italic.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Italic: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/justify.tsx b/components/icons/src/icons/justify.tsx deleted file mode 100644 index b4136cb..0000000 --- a/components/icons/src/icons/justify.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Justify: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/kpi.tsx b/components/icons/src/icons/kpi.tsx deleted file mode 100644 index cc426be..0000000 --- a/components/icons/src/icons/kpi.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Kpi: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/last-page.tsx b/components/icons/src/icons/last-page.tsx deleted file mode 100644 index d2afe4b..0000000 --- a/components/icons/src/icons/last-page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const LastPage: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/launcher.tsx b/components/icons/src/icons/launcher.tsx deleted file mode 100644 index e7ad1cc..0000000 --- a/components/icons/src/icons/launcher.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Launcher: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/layers.tsx b/components/icons/src/icons/layers.tsx deleted file mode 100644 index 601f518..0000000 --- a/components/icons/src/icons/layers.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Layers: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/length.tsx b/components/icons/src/icons/length.tsx deleted file mode 100644 index bf394af..0000000 --- a/components/icons/src/icons/length.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Length: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/less-than.tsx b/components/icons/src/icons/less-than.tsx deleted file mode 100644 index 5001906..0000000 --- a/components/icons/src/icons/less-than.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const LessThan: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/level-1.tsx b/components/icons/src/icons/level-1.tsx deleted file mode 100644 index 9127676..0000000 --- a/components/icons/src/icons/level-1.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Level1: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/level-2.tsx b/components/icons/src/icons/level-2.tsx deleted file mode 100644 index 9035d0c..0000000 --- a/components/icons/src/icons/level-2.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Level2: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/level-3.tsx b/components/icons/src/icons/level-3.tsx deleted file mode 100644 index 7f6ce71..0000000 --- a/components/icons/src/icons/level-3.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Level3: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/level-4.tsx b/components/icons/src/icons/level-4.tsx deleted file mode 100644 index 63efcc3..0000000 --- a/components/icons/src/icons/level-4.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Level4: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/level-5.tsx b/components/icons/src/icons/level-5.tsx deleted file mode 100644 index 2b3af32..0000000 --- a/components/icons/src/icons/level-5.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Level5: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/line-normal.tsx b/components/icons/src/icons/line-normal.tsx deleted file mode 100644 index aaf643d..0000000 --- a/components/icons/src/icons/line-normal.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const LineNormal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/line-small.tsx b/components/icons/src/icons/line-small.tsx deleted file mode 100644 index f9d80aa..0000000 --- a/components/icons/src/icons/line-small.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const LineSmall: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/line-spacing.tsx b/components/icons/src/icons/line-spacing.tsx deleted file mode 100644 index 45ec577..0000000 --- a/components/icons/src/icons/line-spacing.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const LineSpacing: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/line-very-small.tsx b/components/icons/src/icons/line-very-small.tsx deleted file mode 100644 index 156e12b..0000000 --- a/components/icons/src/icons/line-very-small.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const LineVerySmall: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/line.tsx b/components/icons/src/icons/line.tsx deleted file mode 100644 index 2d75c20..0000000 --- a/components/icons/src/icons/line.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Line: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/link-remove.tsx b/components/icons/src/icons/link-remove.tsx deleted file mode 100644 index 379e119..0000000 --- a/components/icons/src/icons/link-remove.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const LinkRemove: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/link.tsx b/components/icons/src/icons/link.tsx deleted file mode 100644 index 126b9b9..0000000 --- a/components/icons/src/icons/link.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Link: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/linked-style.tsx b/components/icons/src/icons/linked-style.tsx deleted file mode 100644 index 8be9102..0000000 --- a/components/icons/src/icons/linked-style.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const LinkedStyle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/list-ordered-rtl.tsx b/components/icons/src/icons/list-ordered-rtl.tsx deleted file mode 100644 index f3f1022..0000000 --- a/components/icons/src/icons/list-ordered-rtl.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ListOrderedRtl: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/list-ordered.tsx b/components/icons/src/icons/list-ordered.tsx deleted file mode 100644 index a4cdb26..0000000 --- a/components/icons/src/icons/list-ordered.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ListOrdered: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/list-unordered-rtl.tsx b/components/icons/src/icons/list-unordered-rtl.tsx deleted file mode 100644 index 82076a3..0000000 --- a/components/icons/src/icons/list-unordered-rtl.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ListUnorderedRtl: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/list-unordered.tsx b/components/icons/src/icons/list-unordered.tsx deleted file mode 100644 index ac4c68d..0000000 --- a/components/icons/src/icons/list-unordered.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ListUnordered: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/location.tsx b/components/icons/src/icons/location.tsx deleted file mode 100644 index bbeac22..0000000 --- a/components/icons/src/icons/location.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Location: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/lock.tsx b/components/icons/src/icons/lock.tsx deleted file mode 100644 index cd55884..0000000 --- a/components/icons/src/icons/lock.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Lock: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/lower-case.tsx b/components/icons/src/icons/lower-case.tsx deleted file mode 100644 index 551eee9..0000000 --- a/components/icons/src/icons/lower-case.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const LowerCase: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/mdx.tsx b/components/icons/src/icons/mdx.tsx deleted file mode 100644 index 8e336fa..0000000 --- a/components/icons/src/icons/mdx.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Mdx: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/menu.tsx b/components/icons/src/icons/menu.tsx deleted file mode 100644 index 1269453..0000000 --- a/components/icons/src/icons/menu.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Menu: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/merge-cells.tsx b/components/icons/src/icons/merge-cells.tsx deleted file mode 100644 index bd49507..0000000 --- a/components/icons/src/icons/merge-cells.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const MergeCells: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/microphone.tsx b/components/icons/src/icons/microphone.tsx deleted file mode 100644 index 6dde141..0000000 --- a/components/icons/src/icons/microphone.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Microphone: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/month-agenda.tsx b/components/icons/src/icons/month-agenda.tsx deleted file mode 100644 index 75fb390..0000000 --- a/components/icons/src/icons/month-agenda.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const MonthAgenda: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/month.tsx b/components/icons/src/icons/month.tsx deleted file mode 100644 index 43cf9a8..0000000 --- a/components/icons/src/icons/month.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Month: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/more-chevron.tsx b/components/icons/src/icons/more-chevron.tsx deleted file mode 100644 index 2849baa..0000000 --- a/components/icons/src/icons/more-chevron.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const MoreChevron: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/more-horizontal-1.tsx b/components/icons/src/icons/more-horizontal-filled.tsx similarity index 88% rename from components/icons/src/icons/more-horizontal-1.tsx rename to components/icons/src/icons/more-horizontal-filled.tsx index 17e3fde..4a7b2c9 100644 --- a/components/icons/src/icons/more-horizontal-1.tsx +++ b/components/icons/src/icons/more-horizontal-filled.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const MoreHorizontal1: IconComponent = createIcon(path); +export const MoreHorizontalFilledIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/more-scatter-charts.tsx b/components/icons/src/icons/more-scatter-charts.tsx deleted file mode 100644 index 59e190c..0000000 --- a/components/icons/src/icons/more-scatter-charts.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const MoreScatterCharts: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/more-vertical-1.tsx b/components/icons/src/icons/more-vertical-filled.tsx similarity index 89% rename from components/icons/src/icons/more-vertical-1.tsx rename to components/icons/src/icons/more-vertical-filled.tsx index 459f028..2cb99fb 100644 --- a/components/icons/src/icons/more-vertical-1.tsx +++ b/components/icons/src/icons/more-vertical-filled.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const MoreVertical1: IconComponent = createIcon(path); +export const MoreVerticalFilledIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/more-vertical-2.tsx b/components/icons/src/icons/more-vertical-outlined.tsx similarity index 92% rename from components/icons/src/icons/more-vertical-2.tsx rename to components/icons/src/icons/more-vertical-outlined.tsx index 5afdfed..c0f25d4 100644 --- a/components/icons/src/icons/more-vertical-2.tsx +++ b/components/icons/src/icons/more-vertical-outlined.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const MoreVertical2: IconComponent = createIcon(path); +export const MoreVerticalOutlinedIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/mouse-pointer.tsx b/components/icons/src/icons/mouse-pointer.tsx deleted file mode 100644 index 5f3cefc..0000000 --- a/components/icons/src/icons/mouse-pointer.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const MousePointer: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/multiple-comment-resolve.tsx b/components/icons/src/icons/multiple-comment-resolve.tsx deleted file mode 100644 index 0305f38..0000000 --- a/components/icons/src/icons/multiple-comment-resolve.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const MultipleCommentResolve: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/multiple-comment.tsx b/components/icons/src/icons/multiple-comment.tsx deleted file mode 100644 index 9d0fb54..0000000 --- a/components/icons/src/icons/multiple-comment.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const MultipleComment: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/named-set.tsx b/components/icons/src/icons/named-set.tsx deleted file mode 100644 index 0f09e93..0000000 --- a/components/icons/src/icons/named-set.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const NamedSet: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/nature.tsx b/components/icons/src/icons/nature.tsx deleted file mode 100644 index 87c1680..0000000 --- a/components/icons/src/icons/nature.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Nature: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/none.tsx b/components/icons/src/icons/none.tsx deleted file mode 100644 index 3a1814b..0000000 --- a/components/icons/src/icons/none.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = <>; -export const None: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/notes.tsx b/components/icons/src/icons/notes.tsx deleted file mode 100644 index e8e826c..0000000 --- a/components/icons/src/icons/notes.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Notes: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/number-formatting.tsx b/components/icons/src/icons/number-formatting.tsx deleted file mode 100644 index b01b6cc..0000000 --- a/components/icons/src/icons/number-formatting.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const NumberFormatting: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/objects.tsx b/components/icons/src/icons/objects.tsx deleted file mode 100644 index 8e7ab3f..0000000 --- a/components/icons/src/icons/objects.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = <>; -export const Objects: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/odd-page-break.tsx b/components/icons/src/icons/odd-page-break.tsx deleted file mode 100644 index 3f851f6..0000000 --- a/components/icons/src/icons/odd-page-break.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const OddPageBreak: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/opacity.tsx b/components/icons/src/icons/opacity.tsx deleted file mode 100644 index 41a62f8..0000000 --- a/components/icons/src/icons/opacity.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Opacity: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/open-link.tsx b/components/icons/src/icons/open-link.tsx deleted file mode 100644 index d8afc65..0000000 --- a/components/icons/src/icons/open-link.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const OpenLink: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/order.tsx b/components/icons/src/icons/order.tsx deleted file mode 100644 index 328e028..0000000 --- a/components/icons/src/icons/order.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Order: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/organize-pdf.tsx b/components/icons/src/icons/organize-pdf.tsx deleted file mode 100644 index 774f879..0000000 --- a/components/icons/src/icons/organize-pdf.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const OrganizePdf: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-column-left.tsx b/components/icons/src/icons/page-column-left.tsx deleted file mode 100644 index e729f8b..0000000 --- a/components/icons/src/icons/page-column-left.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageColumnLeft: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-column-one.tsx b/components/icons/src/icons/page-column-one.tsx deleted file mode 100644 index f8b9606..0000000 --- a/components/icons/src/icons/page-column-one.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageColumnOne: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-column-right.tsx b/components/icons/src/icons/page-column-right.tsx deleted file mode 100644 index f3098c1..0000000 --- a/components/icons/src/icons/page-column-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageColumnRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-column-three.tsx b/components/icons/src/icons/page-column-three.tsx deleted file mode 100644 index d2c9ac1..0000000 --- a/components/icons/src/icons/page-column-three.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageColumnThree: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-column-two.tsx b/components/icons/src/icons/page-column-two.tsx deleted file mode 100644 index 29e0489..0000000 --- a/components/icons/src/icons/page-column-two.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageColumnTwo: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-column.tsx b/components/icons/src/icons/page-column.tsx deleted file mode 100644 index 0a8cd4e..0000000 --- a/components/icons/src/icons/page-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-columns.tsx b/components/icons/src/icons/page-columns.tsx deleted file mode 100644 index 21685e3..0000000 --- a/components/icons/src/icons/page-columns.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageColumns: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-numbering.tsx b/components/icons/src/icons/page-numbering.tsx deleted file mode 100644 index 6aef7dc..0000000 --- a/components/icons/src/icons/page-numbering.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageNumbering: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-setup.tsx b/components/icons/src/icons/page-setup.tsx deleted file mode 100644 index 599976b..0000000 --- a/components/icons/src/icons/page-setup.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageSetup: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-size.tsx b/components/icons/src/icons/page-size.tsx deleted file mode 100644 index fe07c95..0000000 --- a/components/icons/src/icons/page-size.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageSize: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/page-text-wrap.tsx b/components/icons/src/icons/page-text-wrap.tsx deleted file mode 100644 index 25ae65f..0000000 --- a/components/icons/src/icons/page-text-wrap.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PageTextWrap: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/paint-bucket.tsx b/components/icons/src/icons/paint-bucket.tsx deleted file mode 100644 index 62763fb..0000000 --- a/components/icons/src/icons/paint-bucket.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PaintBucket: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/pan.tsx b/components/icons/src/icons/pan.tsx deleted file mode 100644 index 05a4d4f..0000000 --- a/components/icons/src/icons/pan.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Pan: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/paragraph.tsx b/components/icons/src/icons/paragraph.tsx deleted file mode 100644 index 1c775ce..0000000 --- a/components/icons/src/icons/paragraph.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Paragraph: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/password.tsx b/components/icons/src/icons/password.tsx deleted file mode 100644 index 32be966..0000000 --- a/components/icons/src/icons/password.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Password: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/paste-match-destination.tsx b/components/icons/src/icons/paste-match-destination.tsx deleted file mode 100644 index f1347e6..0000000 --- a/components/icons/src/icons/paste-match-destination.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PasteMatchDestination: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/paste-style.tsx b/components/icons/src/icons/paste-style.tsx deleted file mode 100644 index 73b7130..0000000 --- a/components/icons/src/icons/paste-style.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PasteStyle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/paste-text-only.tsx b/components/icons/src/icons/paste-text-only.tsx deleted file mode 100644 index 898bd5b..0000000 --- a/components/icons/src/icons/paste-text-only.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PasteTextOnly: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/paste.tsx b/components/icons/src/icons/paste.tsx deleted file mode 100644 index 67d01d0..0000000 --- a/components/icons/src/icons/paste.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Paste: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/pause.tsx b/components/icons/src/icons/pause.tsx deleted file mode 100644 index 173f4d7..0000000 --- a/components/icons/src/icons/pause.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Pause: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/pentagon.tsx b/components/icons/src/icons/pentagon.tsx deleted file mode 100644 index 226ae10..0000000 --- a/components/icons/src/icons/pentagon.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Pentagon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/people.tsx b/components/icons/src/icons/people.tsx deleted file mode 100644 index c193971..0000000 --- a/components/icons/src/icons/people.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const People: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/perimeter.tsx b/components/icons/src/icons/perimeter.tsx deleted file mode 100644 index 1accc0f..0000000 --- a/components/icons/src/icons/perimeter.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Perimeter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/play.tsx b/components/icons/src/icons/play.tsx deleted file mode 100644 index 97d77dd..0000000 --- a/components/icons/src/icons/play.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Play: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/plus.tsx b/components/icons/src/icons/plus.tsx deleted file mode 100644 index fd59c0a..0000000 --- a/components/icons/src/icons/plus.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Plus: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/preformat-code.tsx b/components/icons/src/icons/preformat-code.tsx deleted file mode 100644 index f7c551c..0000000 --- a/components/icons/src/icons/preformat-code.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PreformatCode: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/print-layout.tsx b/components/icons/src/icons/print-layout.tsx deleted file mode 100644 index ff57b8b..0000000 --- a/components/icons/src/icons/print-layout.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const PrintLayout: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/print.tsx b/components/icons/src/icons/print.tsx deleted file mode 100644 index b444bd7..0000000 --- a/components/icons/src/icons/print.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Print: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/properties-2.tsx b/components/icons/src/icons/properties-panel.tsx similarity index 92% rename from components/icons/src/icons/properties-2.tsx rename to components/icons/src/icons/properties-panel.tsx index 6dd015a..1494aef 100644 --- a/components/icons/src/icons/properties-2.tsx +++ b/components/icons/src/icons/properties-panel.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Properties2: IconComponent = createIcon(path); +export const PropertiesPanelIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/properties-1.tsx b/components/icons/src/icons/properties-tools.tsx similarity index 96% rename from components/icons/src/icons/properties-1.tsx rename to components/icons/src/icons/properties-tools.tsx index f56d112..fe90e52 100644 --- a/components/icons/src/icons/properties-1.tsx +++ b/components/icons/src/icons/properties-tools.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Properties1: IconComponent = createIcon(path); +export const PropertiesToolsIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/protect-sheet.tsx b/components/icons/src/icons/protect-sheet.tsx deleted file mode 100644 index 5a62566..0000000 --- a/components/icons/src/icons/protect-sheet.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ProtectSheet: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/protect-workbook.tsx b/components/icons/src/icons/protect-workbook.tsx deleted file mode 100644 index 30a5ea5..0000000 --- a/components/icons/src/icons/protect-workbook.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ProtectWorkbook: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/radio-button.tsx b/components/icons/src/icons/radio-button.tsx deleted file mode 100644 index c78c351..0000000 --- a/components/icons/src/icons/radio-button.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const RadioButton: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/radius.tsx b/components/icons/src/icons/radius.tsx deleted file mode 100644 index c475876..0000000 --- a/components/icons/src/icons/radius.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Radius: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/reapply.tsx b/components/icons/src/icons/reapply.tsx deleted file mode 100644 index 34d7a98..0000000 --- a/components/icons/src/icons/reapply.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Reapply: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/rectangle.tsx b/components/icons/src/icons/rectangle.tsx deleted file mode 100644 index 1fec4c5..0000000 --- a/components/icons/src/icons/rectangle.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Rectangle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/recurrence-edit.tsx b/components/icons/src/icons/recurrence-edit.tsx deleted file mode 100644 index fddc1cb..0000000 --- a/components/icons/src/icons/recurrence-edit.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const RecurrenceEdit: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/redact.tsx b/components/icons/src/icons/redact.tsx deleted file mode 100644 index 826c47a..0000000 --- a/components/icons/src/icons/redact.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Redact: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/redaction.tsx b/components/icons/src/icons/redaction.tsx deleted file mode 100644 index 46f6465..0000000 --- a/components/icons/src/icons/redaction.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Redaction: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/redo.tsx b/components/icons/src/icons/redo.tsx deleted file mode 100644 index 9df4398..0000000 --- a/components/icons/src/icons/redo.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Redo: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/refresh.tsx b/components/icons/src/icons/refresh.tsx deleted file mode 100644 index 346e611..0000000 --- a/components/icons/src/icons/refresh.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Refresh: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/rename.tsx b/components/icons/src/icons/rename.tsx deleted file mode 100644 index 21148bf..0000000 --- a/components/icons/src/icons/rename.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Rename: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/repeat.tsx b/components/icons/src/icons/repeat.tsx deleted file mode 100644 index a5843cf..0000000 --- a/components/icons/src/icons/repeat.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Repeat: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/repeating-section.tsx b/components/icons/src/icons/repeating-section.tsx deleted file mode 100644 index 17dd167..0000000 --- a/components/icons/src/icons/repeating-section.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const RepeatingSection: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/rephrase.tsx b/components/icons/src/icons/rephrase.tsx deleted file mode 100644 index d59d996..0000000 --- a/components/icons/src/icons/rephrase.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Rephrase: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/replace.tsx b/components/icons/src/icons/replace.tsx deleted file mode 100644 index 16b0893..0000000 --- a/components/icons/src/icons/replace.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Replace: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/reset.tsx b/components/icons/src/icons/reset.tsx deleted file mode 100644 index 26b8035..0000000 --- a/components/icons/src/icons/reset.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Reset: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/resize.tsx b/components/icons/src/icons/resize.tsx deleted file mode 100644 index 16b9c9b..0000000 --- a/components/icons/src/icons/resize.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Resize: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/resizer-horizontal.tsx b/components/icons/src/icons/resizer-horizontal.tsx deleted file mode 100644 index 5dbbee7..0000000 --- a/components/icons/src/icons/resizer-horizontal.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ResizerHorizontal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/resizer-right.tsx b/components/icons/src/icons/resizer-right.tsx deleted file mode 100644 index a8fdfb7..0000000 --- a/components/icons/src/icons/resizer-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ResizerRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/resizer-vertical.tsx b/components/icons/src/icons/resizer-vertical.tsx deleted file mode 100644 index bddb777..0000000 --- a/components/icons/src/icons/resizer-vertical.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ResizerVertical: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/resizer.tsx b/components/icons/src/icons/resizer.tsx deleted file mode 100644 index bf7ed93..0000000 --- a/components/icons/src/icons/resizer.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Resizer: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/restart-at-1.tsx b/components/icons/src/icons/restart-at.tsx similarity index 92% rename from components/icons/src/icons/restart-at-1.tsx rename to components/icons/src/icons/restart-at.tsx index c9c6533..16fc44b 100644 --- a/components/icons/src/icons/restart-at-1.tsx +++ b/components/icons/src/icons/restart-at.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const RestartAt1: IconComponent = createIcon(path); +export const RestartAtIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/saturation.tsx b/components/icons/src/icons/saturation.tsx deleted file mode 100644 index f146dee..0000000 --- a/components/icons/src/icons/saturation.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Saturation: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/save-as.tsx b/components/icons/src/icons/save-as.tsx deleted file mode 100644 index 1a41ad7..0000000 --- a/components/icons/src/icons/save-as.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SaveAs: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/save.tsx b/components/icons/src/icons/save.tsx deleted file mode 100644 index f277d94..0000000 --- a/components/icons/src/icons/save.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Save: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/search.tsx b/components/icons/src/icons/search.tsx deleted file mode 100644 index f5a8d23..0000000 --- a/components/icons/src/icons/search.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Search: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/select-all.tsx b/components/icons/src/icons/select-all.tsx deleted file mode 100644 index d87b9aa..0000000 --- a/components/icons/src/icons/select-all.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SelectAll: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/selection.tsx b/components/icons/src/icons/selection.tsx deleted file mode 100644 index 607954b..0000000 --- a/components/icons/src/icons/selection.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Selection: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/send-backward.tsx b/components/icons/src/icons/send-backward.tsx deleted file mode 100644 index d7ed707..0000000 --- a/components/icons/src/icons/send-backward.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SendBackward: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/send-to-back.tsx b/components/icons/src/icons/send-to-back.tsx deleted file mode 100644 index 528a471..0000000 --- a/components/icons/src/icons/send-to-back.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SendToBack: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/send.tsx b/components/icons/src/icons/send.tsx deleted file mode 100644 index 4a8c013..0000000 --- a/components/icons/src/icons/send.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Send: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/settings.tsx b/components/icons/src/icons/settings.tsx deleted file mode 100644 index bfaa379..0000000 --- a/components/icons/src/icons/settings.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Settings: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/shapes.tsx b/components/icons/src/icons/shapes.tsx deleted file mode 100644 index 1d392fa..0000000 --- a/components/icons/src/icons/shapes.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Shapes: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sharpness.tsx b/components/icons/src/icons/sharpness.tsx deleted file mode 100644 index 929ca7e..0000000 --- a/components/icons/src/icons/sharpness.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Sharpness: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/shorten.tsx b/components/icons/src/icons/shorten.tsx deleted file mode 100644 index f30a3a7..0000000 --- a/components/icons/src/icons/shorten.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Shorten: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/show-hide-panel.tsx b/components/icons/src/icons/show-hide-panel.tsx deleted file mode 100644 index e824091..0000000 --- a/components/icons/src/icons/show-hide-panel.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ShowHidePanel: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/signature.tsx b/components/icons/src/icons/signature.tsx deleted file mode 100644 index 32f4d8b..0000000 --- a/components/icons/src/icons/signature.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Signature: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/smart-paste.tsx b/components/icons/src/icons/smart-paste.tsx deleted file mode 100644 index 5be4077..0000000 --- a/components/icons/src/icons/smart-paste.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SmartPaste: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sort-ascending-2.tsx b/components/icons/src/icons/sort-ascending-boxed.tsx similarity index 89% rename from components/icons/src/icons/sort-ascending-2.tsx rename to components/icons/src/icons/sort-ascending-boxed.tsx index c8c4845..e58caf9 100644 --- a/components/icons/src/icons/sort-ascending-2.tsx +++ b/components/icons/src/icons/sort-ascending-boxed.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const SortAscending2: IconComponent = createIcon(path); +export const SortAscendingBoxedIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sorting-2.tsx b/components/icons/src/icons/sort-ascending-list.tsx similarity index 85% rename from components/icons/src/icons/sorting-2.tsx rename to components/icons/src/icons/sort-ascending-list.tsx index c7e8c01..ba0553a 100644 --- a/components/icons/src/icons/sorting-2.tsx +++ b/components/icons/src/icons/sort-ascending-list.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Sorting2: IconComponent = createIcon(path); +export const SortAscendingListIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sort-ascending.tsx b/components/icons/src/icons/sort-ascending.tsx deleted file mode 100644 index ddc5c58..0000000 --- a/components/icons/src/icons/sort-ascending.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SortAscending: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sorting-1.tsx b/components/icons/src/icons/sort-bidirectional.tsx similarity index 86% rename from components/icons/src/icons/sorting-1.tsx rename to components/icons/src/icons/sort-bidirectional.tsx index 9964d78..2a04d6c 100644 --- a/components/icons/src/icons/sorting-1.tsx +++ b/components/icons/src/icons/sort-bidirectional.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Sorting1: IconComponent = createIcon(path); +export const SortBidirectionalIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sort-descending-2.tsx b/components/icons/src/icons/sort-descending-boxed.tsx similarity index 89% rename from components/icons/src/icons/sort-descending-2.tsx rename to components/icons/src/icons/sort-descending-boxed.tsx index 15c323c..58d9b45 100644 --- a/components/icons/src/icons/sort-descending-2.tsx +++ b/components/icons/src/icons/sort-descending-boxed.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const SortDescending2: IconComponent = createIcon(path); +export const SortDescendingBoxedIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sorting-3.tsx b/components/icons/src/icons/sort-descending-list.tsx similarity index 83% rename from components/icons/src/icons/sorting-3.tsx rename to components/icons/src/icons/sort-descending-list.tsx index 0e079b9..247d603 100644 --- a/components/icons/src/icons/sorting-3.tsx +++ b/components/icons/src/icons/sort-descending-list.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Sorting3: IconComponent = createIcon(path); +export const SortDescendingListIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sort-descending.tsx b/components/icons/src/icons/sort-descending.tsx deleted file mode 100644 index e35430c..0000000 --- a/components/icons/src/icons/sort-descending.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SortDescending: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/spacing-after.tsx b/components/icons/src/icons/spacing-after.tsx deleted file mode 100644 index 172ec9e..0000000 --- a/components/icons/src/icons/spacing-after.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SpacingAfter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/spacing-before.tsx b/components/icons/src/icons/spacing-before.tsx deleted file mode 100644 index a852a63..0000000 --- a/components/icons/src/icons/spacing-before.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SpacingBefore: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/spell-check.tsx b/components/icons/src/icons/spell-check.tsx deleted file mode 100644 index d1e13ce..0000000 --- a/components/icons/src/icons/spell-check.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SpellCheck: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/split-horizontal.tsx b/components/icons/src/icons/split-horizontal.tsx deleted file mode 100644 index e700ed1..0000000 --- a/components/icons/src/icons/split-horizontal.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SplitHorizontal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/split-vertical.tsx b/components/icons/src/icons/split-vertical.tsx deleted file mode 100644 index e6ffe29..0000000 --- a/components/icons/src/icons/split-vertical.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SplitVertical: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/square-head-fill.tsx b/components/icons/src/icons/square-head-fill.tsx deleted file mode 100644 index e4a5953..0000000 --- a/components/icons/src/icons/square-head-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SquareHeadFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/square-head.tsx b/components/icons/src/icons/square-head.tsx deleted file mode 100644 index 2b97a67..0000000 --- a/components/icons/src/icons/square-head.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SquareHead: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/square-tail-fill.tsx b/components/icons/src/icons/square-tail-fill.tsx deleted file mode 100644 index 80310da..0000000 --- a/components/icons/src/icons/square-tail-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SquareTailFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/square-tail.tsx b/components/icons/src/icons/square-tail.tsx deleted file mode 100644 index dc91f6f..0000000 --- a/components/icons/src/icons/square-tail.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SquareTail: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/squiggly.tsx b/components/icons/src/icons/squiggly.tsx deleted file mode 100644 index 97fecae..0000000 --- a/components/icons/src/icons/squiggly.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Squiggly: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/stamp.tsx b/components/icons/src/icons/stamp.tsx deleted file mode 100644 index ec9dfe5..0000000 --- a/components/icons/src/icons/stamp.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Stamp: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/star-filled.tsx b/components/icons/src/icons/star-filled.tsx deleted file mode 100644 index 715946f..0000000 --- a/components/icons/src/icons/star-filled.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const StarFilled: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/stop-rectangle.tsx b/components/icons/src/icons/stop-rectangle.tsx deleted file mode 100644 index bca2471..0000000 --- a/components/icons/src/icons/stop-rectangle.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const StopRectangle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/strikethrough.tsx b/components/icons/src/icons/strikethrough.tsx deleted file mode 100644 index ab25527..0000000 --- a/components/icons/src/icons/strikethrough.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Strikethrough: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/stroke-width.tsx b/components/icons/src/icons/stroke-width.tsx deleted file mode 100644 index 71b6ef3..0000000 --- a/components/icons/src/icons/stroke-width.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const StrokeWidth: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/style.tsx b/components/icons/src/icons/style.tsx deleted file mode 100644 index 673c5a7..0000000 --- a/components/icons/src/icons/style.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Style: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sub-total.tsx b/components/icons/src/icons/sub-total.tsx deleted file mode 100644 index 7e3947c..0000000 --- a/components/icons/src/icons/sub-total.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const SubTotal: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/subscript.tsx b/components/icons/src/icons/subscript.tsx deleted file mode 100644 index bb28c0e..0000000 --- a/components/icons/src/icons/subscript.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Subscript: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/sum.tsx b/components/icons/src/icons/sum.tsx deleted file mode 100644 index 42ba12b..0000000 --- a/components/icons/src/icons/sum.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Sum: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/superscript.tsx b/components/icons/src/icons/superscript.tsx deleted file mode 100644 index 213ec96..0000000 --- a/components/icons/src/icons/superscript.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Superscript: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/symbols.tsx b/components/icons/src/icons/symbols.tsx deleted file mode 100644 index 24a466c..0000000 --- a/components/icons/src/icons/symbols.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Symbols: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-align-center.tsx b/components/icons/src/icons/table-align-center.tsx deleted file mode 100644 index e652887..0000000 --- a/components/icons/src/icons/table-align-center.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableAlignCenter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-align-left.tsx b/components/icons/src/icons/table-align-left.tsx deleted file mode 100644 index 35485c3..0000000 --- a/components/icons/src/icons/table-align-left.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableAlignLeft: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-align-right.tsx b/components/icons/src/icons/table-align-right.tsx deleted file mode 100644 index f170e52..0000000 --- a/components/icons/src/icons/table-align-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableAlignRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-border-custom.tsx b/components/icons/src/icons/table-border-custom.tsx deleted file mode 100644 index 8b9568e..0000000 --- a/components/icons/src/icons/table-border-custom.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableBorderCustom: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-cell-none.tsx b/components/icons/src/icons/table-cell-none.tsx deleted file mode 100644 index 66ee61d..0000000 --- a/components/icons/src/icons/table-cell-none.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableCellNone: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-cell.tsx b/components/icons/src/icons/table-cell.tsx deleted file mode 100644 index 90a1d3c..0000000 --- a/components/icons/src/icons/table-cell.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableCell: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-delete.tsx b/components/icons/src/icons/table-delete.tsx deleted file mode 100644 index ca23ad3..0000000 --- a/components/icons/src/icons/table-delete.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableDelete: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-2.tsx b/components/icons/src/icons/table-grid.tsx similarity index 87% rename from components/icons/src/icons/table-2.tsx rename to components/icons/src/icons/table-grid.tsx index d51b8fa..92e4c8d 100644 --- a/components/icons/src/icons/table-2.tsx +++ b/components/icons/src/icons/table-grid.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Table2: IconComponent = createIcon(path); +export const TableGridIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-header.tsx b/components/icons/src/icons/table-header.tsx deleted file mode 100644 index b438c4b..0000000 --- a/components/icons/src/icons/table-header.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableHeader: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-insert-column.tsx b/components/icons/src/icons/table-insert-column.tsx deleted file mode 100644 index 399c623..0000000 --- a/components/icons/src/icons/table-insert-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableInsertColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-insert-row.tsx b/components/icons/src/icons/table-insert-row.tsx deleted file mode 100644 index 24a5a6e..0000000 --- a/components/icons/src/icons/table-insert-row.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableInsertRow: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-merge.tsx b/components/icons/src/icons/table-merge.tsx deleted file mode 100644 index 9b29851..0000000 --- a/components/icons/src/icons/table-merge.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableMerge: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-nested.tsx b/components/icons/src/icons/table-nested.tsx deleted file mode 100644 index 5ebb55d..0000000 --- a/components/icons/src/icons/table-nested.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableNested: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-of-content.tsx b/components/icons/src/icons/table-of-content.tsx deleted file mode 100644 index 42d9121..0000000 --- a/components/icons/src/icons/table-of-content.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableOfContent: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-overwrite-cells.tsx b/components/icons/src/icons/table-overwrite-cells.tsx deleted file mode 100644 index e901714..0000000 --- a/components/icons/src/icons/table-overwrite-cells.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableOverwriteCells: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table-update.tsx b/components/icons/src/icons/table-update.tsx deleted file mode 100644 index 5728bf3..0000000 --- a/components/icons/src/icons/table-update.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TableUpdate: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/table.tsx b/components/icons/src/icons/table.tsx deleted file mode 100644 index 686e80f..0000000 --- a/components/icons/src/icons/table.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Table: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/text-alternative.tsx b/components/icons/src/icons/text-alternative.tsx deleted file mode 100644 index 03f0d25..0000000 --- a/components/icons/src/icons/text-alternative.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TextAlternative: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/text-annotation.tsx b/components/icons/src/icons/text-annotation.tsx deleted file mode 100644 index b921003..0000000 --- a/components/icons/src/icons/text-annotation.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TextAnnotation: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/text-form.tsx b/components/icons/src/icons/text-form.tsx deleted file mode 100644 index 422f092..0000000 --- a/components/icons/src/icons/text-form.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TextForm: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/text-header.tsx b/components/icons/src/icons/text-header.tsx deleted file mode 100644 index caf61c3..0000000 --- a/components/icons/src/icons/text-header.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TextHeader: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/text-outline.tsx b/components/icons/src/icons/text-outline.tsx deleted file mode 100644 index 3d7bfa4..0000000 --- a/components/icons/src/icons/text-outline.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TextOutline: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/text-that-contains.tsx b/components/icons/src/icons/text-that-contains.tsx deleted file mode 100644 index fb8fc94..0000000 --- a/components/icons/src/icons/text-that-contains.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TextThatContains: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/text-wrap.tsx b/components/icons/src/icons/text-wrap.tsx deleted file mode 100644 index 4d91363..0000000 --- a/components/icons/src/icons/text-wrap.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TextWrap: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/thumbnail.tsx b/components/icons/src/icons/thumbnail.tsx deleted file mode 100644 index dc61854..0000000 --- a/components/icons/src/icons/thumbnail.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Thumbnail: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/thumbs-down-fill.tsx b/components/icons/src/icons/thumbs-down-fill.tsx deleted file mode 100644 index 75554a7..0000000 --- a/components/icons/src/icons/thumbs-down-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = <>; -export const ThumbsDownFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/thumbs-down.tsx b/components/icons/src/icons/thumbs-down.tsx deleted file mode 100644 index bc657f9..0000000 --- a/components/icons/src/icons/thumbs-down.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ThumbsDown: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/thumbs-up-fill.tsx b/components/icons/src/icons/thumbs-up-fill.tsx deleted file mode 100644 index bf65907..0000000 --- a/components/icons/src/icons/thumbs-up-fill.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = <>; -export const ThumbsUpFill: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/thumbs-up.tsx b/components/icons/src/icons/thumbs-up.tsx deleted file mode 100644 index 1f92f55..0000000 --- a/components/icons/src/icons/thumbs-up.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ThumbsUp: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/time-zone.tsx b/components/icons/src/icons/time-zone.tsx deleted file mode 100644 index 9d98deb..0000000 --- a/components/icons/src/icons/time-zone.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TimeZone: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/timeline-day.tsx b/components/icons/src/icons/timeline-day.tsx deleted file mode 100644 index 0758fff..0000000 --- a/components/icons/src/icons/timeline-day.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TimelineDay: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/timeline-month.tsx b/components/icons/src/icons/timeline-month.tsx deleted file mode 100644 index 175ad8e..0000000 --- a/components/icons/src/icons/timeline-month.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TimelineMonth: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/timeline-today.tsx b/components/icons/src/icons/timeline-today.tsx deleted file mode 100644 index 9cc60c4..0000000 --- a/components/icons/src/icons/timeline-today.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TimelineToday: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/timeline-week.tsx b/components/icons/src/icons/timeline-week.tsx deleted file mode 100644 index 31a7053..0000000 --- a/components/icons/src/icons/timeline-week.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TimelineWeek: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/timeline-work-week.tsx b/components/icons/src/icons/timeline-work-week.tsx deleted file mode 100644 index fee434d..0000000 --- a/components/icons/src/icons/timeline-work-week.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TimelineWorkWeek: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/tint.tsx b/components/icons/src/icons/tint.tsx deleted file mode 100644 index dffef6b..0000000 --- a/components/icons/src/icons/tint.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Tint: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/top-10.tsx b/components/icons/src/icons/top-10.tsx deleted file mode 100644 index 227d3ce..0000000 --- a/components/icons/src/icons/top-10.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Top10: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/top-bottom-rules.tsx b/components/icons/src/icons/top-bottom-rules.tsx deleted file mode 100644 index 35c4d78..0000000 --- a/components/icons/src/icons/top-bottom-rules.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TopBottomRules: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/transform-left.tsx b/components/icons/src/icons/transform-left.tsx deleted file mode 100644 index bd5e3aa..0000000 --- a/components/icons/src/icons/transform-left.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TransformLeft: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/transform-right.tsx b/components/icons/src/icons/transform-right.tsx deleted file mode 100644 index b62944c..0000000 --- a/components/icons/src/icons/transform-right.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TransformRight: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/transform.tsx b/components/icons/src/icons/transform.tsx deleted file mode 100644 index 7cb6103..0000000 --- a/components/icons/src/icons/transform.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Transform: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/translate.tsx b/components/icons/src/icons/translate.tsx deleted file mode 100644 index f245fe8..0000000 --- a/components/icons/src/icons/translate.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Translate: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/trash.tsx b/components/icons/src/icons/trash.tsx deleted file mode 100644 index b746fff..0000000 --- a/components/icons/src/icons/trash.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Trash: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/travel-and-places.tsx b/components/icons/src/icons/travel-and-places.tsx deleted file mode 100644 index 33bdd83..0000000 --- a/components/icons/src/icons/travel-and-places.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TravelAndPlaces: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/triangle.tsx b/components/icons/src/icons/triangle.tsx deleted file mode 100644 index 059cd53..0000000 --- a/components/icons/src/icons/triangle.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Triangle: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/two-column.tsx b/components/icons/src/icons/two-column.tsx deleted file mode 100644 index 14ef300..0000000 --- a/components/icons/src/icons/two-column.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TwoColumn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/two-row.tsx b/components/icons/src/icons/two-row.tsx deleted file mode 100644 index 0dfdea9..0000000 --- a/components/icons/src/icons/two-row.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const TwoRow: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/underline.tsx b/components/icons/src/icons/underline.tsx deleted file mode 100644 index 67b5945..0000000 --- a/components/icons/src/icons/underline.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Underline: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/undo.tsx b/components/icons/src/icons/undo.tsx deleted file mode 100644 index 60fb788..0000000 --- a/components/icons/src/icons/undo.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Undo: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/unfiltered.tsx b/components/icons/src/icons/unfiltered.tsx deleted file mode 100644 index 003f14f..0000000 --- a/components/icons/src/icons/unfiltered.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Unfiltered: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/ungroup-2.tsx b/components/icons/src/icons/ungroup-divide.tsx similarity index 92% rename from components/icons/src/icons/ungroup-2.tsx rename to components/icons/src/icons/ungroup-divide.tsx index 7765a96..90bded2 100644 --- a/components/icons/src/icons/ungroup-2.tsx +++ b/components/icons/src/icons/ungroup-divide.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Ungroup2: IconComponent = createIcon(path); +export const UngroupDivideIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/ungroup-1.tsx b/components/icons/src/icons/ungroup-items.tsx similarity index 90% rename from components/icons/src/icons/ungroup-1.tsx rename to components/icons/src/icons/ungroup-items.tsx index 84b2a93..9fcb841 100644 --- a/components/icons/src/icons/ungroup-1.tsx +++ b/components/icons/src/icons/ungroup-items.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Ungroup1: IconComponent = createIcon(path); +export const UngroupItemsIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/unlock.tsx b/components/icons/src/icons/unlock.tsx deleted file mode 100644 index a70ea34..0000000 --- a/components/icons/src/icons/unlock.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Unlock: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/upload-1.tsx b/components/icons/src/icons/upload.tsx similarity index 83% rename from components/icons/src/icons/upload-1.tsx rename to components/icons/src/icons/upload.tsx index 960e26b..889dbaa 100644 --- a/components/icons/src/icons/upload-1.tsx +++ b/components/icons/src/icons/upload.tsx @@ -1,4 +1,4 @@ import { createIcon } from '../icon'; import { IconComponent } from '../icon'; const path: React.ReactNode = ; -export const Upload1: IconComponent = createIcon(path); +export const UploadIcon: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/upper-case.tsx b/components/icons/src/icons/upper-case.tsx deleted file mode 100644 index db9d443..0000000 --- a/components/icons/src/icons/upper-case.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const UpperCase: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/user-defined.tsx b/components/icons/src/icons/user-defined.tsx deleted file mode 100644 index aac5dfa..0000000 --- a/components/icons/src/icons/user-defined.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const UserDefined: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/user.tsx b/components/icons/src/icons/user.tsx deleted file mode 100644 index 117b11b..0000000 --- a/components/icons/src/icons/user.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const User: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/vertical-align-bottom.tsx b/components/icons/src/icons/vertical-align-bottom.tsx deleted file mode 100644 index d59343e..0000000 --- a/components/icons/src/icons/vertical-align-bottom.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const VerticalAlignBottom: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/vertical-align-center.tsx b/components/icons/src/icons/vertical-align-center.tsx deleted file mode 100644 index f30aa06..0000000 --- a/components/icons/src/icons/vertical-align-center.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const VerticalAlignCenter: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/vertical-align-top.tsx b/components/icons/src/icons/vertical-align-top.tsx deleted file mode 100644 index e05343f..0000000 --- a/components/icons/src/icons/vertical-align-top.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const VerticalAlignTop: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/video.tsx b/components/icons/src/icons/video.tsx deleted file mode 100644 index 9498293..0000000 --- a/components/icons/src/icons/video.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Video: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/view-side.tsx b/components/icons/src/icons/view-side.tsx deleted file mode 100644 index ade789c..0000000 --- a/components/icons/src/icons/view-side.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ViewSide: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/volume.tsx b/components/icons/src/icons/volume.tsx deleted file mode 100644 index 8ee0065..0000000 --- a/components/icons/src/icons/volume.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Volume: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/warning.tsx b/components/icons/src/icons/warning.tsx deleted file mode 100644 index f5d3147..0000000 --- a/components/icons/src/icons/warning.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Warning: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/web-layout.tsx b/components/icons/src/icons/web-layout.tsx deleted file mode 100644 index 4b0ce31..0000000 --- a/components/icons/src/icons/web-layout.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const WebLayout: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/week.tsx b/components/icons/src/icons/week.tsx deleted file mode 100644 index 7d967c7..0000000 --- a/components/icons/src/icons/week.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const Week: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/xml-mapping.tsx b/components/icons/src/icons/xml-mapping.tsx deleted file mode 100644 index debaa14..0000000 --- a/components/icons/src/icons/xml-mapping.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const XmlMapping: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/zoom-in.tsx b/components/icons/src/icons/zoom-in.tsx deleted file mode 100644 index 7a2950f..0000000 --- a/components/icons/src/icons/zoom-in.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ZoomIn: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/zoom-out.tsx b/components/icons/src/icons/zoom-out.tsx deleted file mode 100644 index a5c695e..0000000 --- a/components/icons/src/icons/zoom-out.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ZoomOut: IconComponent = createIcon(path); diff --git a/components/icons/src/icons/zoom-to-fit.tsx b/components/icons/src/icons/zoom-to-fit.tsx deleted file mode 100644 index 9aaa30d..0000000 --- a/components/icons/src/icons/zoom-to-fit.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { createIcon } from '../icon'; -import { IconComponent } from '../icon'; -const path: React.ReactNode = ; -export const ZoomToFit: IconComponent = createIcon(path); diff --git a/components/icons/src/index.ts b/components/icons/src/index.ts deleted file mode 100644 index 55434fb..0000000 --- a/components/icons/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './icons/index'; -export * from './icon'; -export * from './svg-icon'; diff --git a/components/icons/src/svg-icon.tsx b/components/icons/src/svg-icon.tsx deleted file mode 100644 index c5700d6..0000000 --- a/components/icons/src/svg-icon.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from 'react'; -import { HTMLAttributes, SVGProps } from 'react'; - -/** - * Common icon props interface - */ -export interface IIcons { - /** - * Specifies the width of the icon. - * - * @default 24px - */ - width?: number | string; - - /** - * Specifies the height of the icon. - * - * @default 24px - */ - height?: number | string; - - /** - * Defines the SVG viewBox attribute which controls the visible area of the icon. - * - * @default "0 0 24 24" - */ - viewBox?: string; - - /** - * Sets the color of the SVG icon, can be any valid CSS color value. - * - * @default - - */ - color?: string; - - /** - * Additional CSS class names to apply to the icon component. - * - * @private - */ - className?: string; -} - -type SvgProps = HTMLAttributes & SVGProps; - -/** - * The SVG component displays SVG icons with a given height, width, and viewBox. - * - * @private - * @param {SvgProps} props - The props of the component. - * @returns {void} Returns the SVG element. - */ -export const SvgIcon: React.FC = ((props: SvgProps) => { - const { - height = 24, - viewBox = '0 0 24 24', - width = 24, - children, - focusable = 'false', - 'aria-hidden': ariaHidden = true, - ...restProps - } = props; - - return ( - - {children} - - ); -}); - -export default SvgIcon; diff --git a/components/icons/tsconfig.json b/components/icons/tsconfig.json deleted file mode 100644 index 1da9467..0000000 --- a/components/icons/tsconfig.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "compilerOptions": { - "allowUnreachableCode": false, // Disable unreachable code warnings. - "allowUnusedLabels": false, // Disable unused label warnings. - "declaration": true, // Generate .d.ts files. - "forceConsistentCasingInFileNames": true, // Enforce consistent file name casing. - "jsx": "react-jsx", // Enable JSX syntax support. - "module": "ESNext", // Use ESNext module system. - "moduleResolution": "Node", // Resolve modules using Node-style resolution. - "noEmitOnError": true, // Prevent emitting JS files if there are errors. - "noFallthroughCasesInSwitch": true, // Prevent fallthrough in switch cases. - "noImplicitAny": false, // Disallow implicit any types. - "noImplicitReturns": true, // Ensure functions return a value. - "noUnusedLocals": true, // Warn on unused local variables. - "noUnusedParameters": true, // Warn on unused function parameters. - "strict": false, // Enable all strict checks. - "strictBindCallApply": false, // Enable strict checking of bind, call, and apply methods. - "strictFunctionTypes": false, // Enable strict checking of function types. - "strictNullChecks": false, // Enable strict null checks. - "skipLibCheck": true, // Skip checking of declaration files. - "sourceMap": true, // Generate source maps. - "target": "ES2020", // Set ECMAScript version to ES2020. - "typeRoots": [ - "./node_modules/@types", - "./node_modules/@testing-library" - ], // Specify root directory for type declarations. - "types": [ - "node", - "react", - "react-dom" - ], // Include type declarations. - "lib": [ - "ES2020", - "DOM", - "DOM.Iterable" - ] // Include libraries for ES2020 and DOM. - }, - "include": [ - "src", - "spec", - "samples" - ], // Include only the provided directory for compilation. - "exclude": [ - "node_modules", - "dist", - "public", - "coverage", - "test-report" - ], // Exclude these directories from compilation. - "compileOnSave": false // Disable Compile-on-Save. -} \ No newline at end of file diff --git a/components/inputs/CHANGELOG.md b/components/inputs/CHANGELOG.md deleted file mode 100644 index 234d855..0000000 --- a/components/inputs/CHANGELOG.md +++ /dev/null @@ -1,63 +0,0 @@ -# Changelog - -## [Unreleased] - -## 29.2.4 (2025-05-14) - -### NumericTextBox - -The NumericTextBox component provides a specialized input field for numeric values with validation, formatting, and increment/decrement capabilities. It offers precise control over numeric input with support for various number formats, validation rules, and user interaction patterns. - -Explore the demo here - -**Key features** - -- **Value constraints:** Set minimum and maximum allowed values to restrict user input within specific numeric ranges. - -- **Step configuration:** Define increment/decrement step size for precise value adjustments using spin buttons or keyboard controls. - -- **Spin buttons:** Optional increment and decrement buttons that allow users to adjust values without typing. - -- **Number formatting:** Comprehensive formatting options including decimal places, currency symbols, and percentage formatting. - -- **LabelMode** Implements floating label functionality with configurable behavior modes to enhance form usability. - -- **Keyboard navigation:** Enhanced keyboard support for incrementing/decrementing values using arrow keys. - -### TextArea - -The TextArea component provides a multi-line text input field with enhanced functionality for collecting longer text content from users. It offers various customization options to adapt to different application requirements and design systems. - -Explore the demo here - -**Key features** - -- **Resizing options:** Supports multiple resize modes including Both, Horizontal, and Vertical to control how users can resize the input area. - -- **LabelMode:** Implements floating label functionality with configurable behavior modes to enhance form usability. - -- **Variants:** Offers multiple visual styles including Standard, Outlined, and Filled variants to match your application's design language. - -- **Customizable dimensions:** Supports setting specific dimensions through rows and cols properties or through width styling. - -- **Controlled and uncontrolled modes:** Supports both controlled mode (using the `value` prop) and uncontrolled mode (using the `defaultValue` prop) to accommodate different state management approaches. - -### TextBox - -The TextBox component provides a feature-rich input field for collecting user text input with enhanced styling options and validation states. It supports both controlled and uncontrolled input modes to fit various application requirements. - -Explore the demo here - -**Key features** - -- **Variants:** Offers multiple visual styles including Standard, Outlined, and Filled variants to match your application's design language. - -- **Sizes:** Provides size options (Small and Medium) to control the component's dimensions for different UI contexts. - -- **Color:** Supports different color schemes including Success, Warning, and Error to visually communicate validation states. - -- **LabelMode:** Implements floating label functionality with configurable behavior modes to enhance form usability. - -- **Prefix and suffix:** Supports adding custom icons at the beginning or end of the input field for enhanced visual cues. - -- **Controlled and uncontrolled modes:** Supports both controlled mode (using the `value` prop) and uncontrolled mode (using the `defaultValue` prop) to accommodate different state management approaches. \ No newline at end of file diff --git a/components/inputs/README.md b/components/inputs/README.md deleted file mode 100644 index aeb0027..0000000 --- a/components/inputs/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# React Inputs Components - -## What's Included in the React Inputs Package - -The React Inputs package includes the following list of components. - -### React Numeric TextBox - -The NumericTextBox component provides a specialized input field for numeric values with validation, formatting, and increment/decrement capabilities. It offers precise control over numeric input with support for various number formats, validation rules, and user interaction patterns. - -Explore the demo [here](https://react.syncfusion.com/numeric-textbox). - -**Key features** - -- **Value constraints:** Set minimum and maximum allowed values to restrict user input within specific numeric ranges. - -- **Step configuration:** Define increment/decrement step size for precise value adjustments using spin buttons or keyboard controls. - -- **Spin buttons:** Optional increment and decrement buttons that allow users to adjust values without typing. - -- **Number formatting:** Comprehensive formatting options including decimal places, currency symbols, and percentage formatting. - -- **LabelMode** Implements floating label functionality with configurable behavior modes to enhance form usability. - -- **Keyboard navigation:** Enhanced keyboard support for incrementing/decrementing values using arrow keys. - -### React TextArea - -The TextArea component provides a multi-line text input field with enhanced functionality for collecting longer text content from users. It offers various customization options to adapt to different application requirements and design systems. - -Explore the demo [here](https://react.syncfusion.com/textarea). - -**Key features** - -- **Resizing options:** Supports multiple resize modes including Both, Horizontal, and Vertical to control how users can resize the input area. - -- **LabelMode:** Implements floating label functionality with configurable behavior modes to enhance form usability. - -- **Variants:** Offers multiple visual styles including Standard, Outlined, and Filled variants to match your application's design language. - -- **Customizable dimensions:** Supports setting specific dimensions through rows and cols properties or through width styling. - -- **Controlled and uncontrolled modes:** Supports both controlled mode (using the `value` prop) and uncontrolled mode (using the `defaultValue` prop) to accommodate different state management approaches. - -### React TextBox - -The TextBox component provides a feature-rich input field for collecting user text input with enhanced styling options and validation states. It supports both controlled and uncontrolled input modes to fit various application requirements. - -Explore the demo [here](https://react.syncfusion.com/textbox). - -**Key features** - -- **Variants:** Offers multiple visual styles including Standard, Outlined, and Filled variants to match your application's design language. - -- **Sizes:** Provides size options (Small and Medium) to control the component's dimensions for different UI contexts. - -- **Color:** Supports different color schemes including Success, Warning, and Error to visually communicate validation states. - -- **LabelMode:** Implements floating label functionality with configurable behavior modes to enhance form usability. - -- **Prefix and suffix:** Supports adding custom icons at the beginning or end of the input field for enhanced visual cues. - -- **Controlled and uncontrolled modes:** Supports both controlled mode (using the `value` prop) and uncontrolled mode (using the `defaultValue` prop) to accommodate different state management approaches. - -

-Trusted by the world's leading companies - - Syncfusion logo - -

- -## Setup - -To install `inputs` and its dependent packages, use the following command, - -```sh -npm install @syncfusion/react-inputs -``` - -## Support - -Product support is available through following mediums. - -* [Support ticket](https://support.syncfusion.com/support/tickets/create) - Guaranteed Response in 24 hours | Unlimited tickets | Holiday support -* Live chat - -## Changelog - -Check the changelog [here](https://github.com/syncfusion/react-ui-components/blob/master/components/inputs/CHANGELOG.md). Get minor improvements and bug fixes every week to stay up to date with frequent updates. - -## License and copyright - -> This is a commercial product and requires a paid license for possession or use. Syncfusion’s licensed software, including this component, is subject to the terms and conditions of Syncfusion's [EULA](https://www.syncfusion.com/eula/es/). To acquire a license for [React UI components](https://www.syncfusion.com/react-components), you can [purchase](https://www.syncfusion.com/sales/products) or [start a free 30-day trial](https://www.syncfusion.com/account/manage-trials/start-trials). - -> A [free community license](https://www.syncfusion.com/products/communitylicense) is also available for companies and individuals whose organizations have less than $1 million USD in annual gross revenue and five or fewer developers. - -See [LICENSE FILE](https://github.com/syncfusion/react-ui-components/blob/master/license?utm_source=npm&utm_campaign=notification) for more info. - -© Copyright 2025 Syncfusion, Inc. All Rights Reserved. The Syncfusion Essential Studio license and copyright applies to this distribution. diff --git a/components/inputs/package.json b/components/inputs/package.json deleted file mode 100644 index 7bcfbce..0000000 --- a/components/inputs/package.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "name": "@syncfusion/react-inputs", - "version": "30.1.37", - "description": "A package of Pure react input components such as Textbox, Color-picker, Masked-textbox, Numeric-textbox, Slider, Upload, and Form-validator that is used to get input from the users.", - "author": "Syncfusion Inc.", - "license": "SEE LICENSE IN license", - "keywords": [ - "syncfusion", - "web-components", - "react", - "syncfusion-react", - "react-inputs", - "input box", - "textbox", - "masked textbox", - "masked input", - "input mask", - "date mask", - "mask format", - "numeric textbox", - "percent textbox", - "percentage textbox", - "currency textbox", - "numeric spinner", - "numeric up-down", - "number input", - "slider", - "range slider", - "minrange", - "slider limits", - "localization slider", - "format slider", - "slider with tooltip", - "vertical slider", - "mobile slider", - "upload", - "upload-box", - "input-file", - "floating-label", - "chunk-upload" - ], - "repository": { - "type": "git", - "url": "https://github.com/syncfusion/react-ui-components.git" - }, - "homepage": "https://www.syncfusion.com/react-ui-components", - "module": "./index.js", - "readme": "README.md", - "dependencies": { - "@syncfusion/react-base": "~30.1.37", - "@syncfusion/react-buttons": "~30.1.37", - "@syncfusion/react-popups": "~30.1.37", - "@syncfusion/react-splitbuttons": "~30.1.37" - }, - "devDependencies": { - "gulp": "4.0.2", - "gulp-typescript": "5.0.1", - "typescript": "5.7.2", - "gulp-sass": "5.1.0", - "sass": "1.83.1", - "react": "19.0.0", - "react-dom": "19.0.0", - "@types/react": "19.0.1", - "@types/react-dom": "19.0.1", - "@types/node": "^22.15.17" - }, - "scripts": { - "build": "gulp build" - }, - "typings": "index.d.ts", - "sideEffects": false -} \ No newline at end of file diff --git a/components/inputs/src/common/index.ts b/components/inputs/src/common/index.ts deleted file mode 100644 index 23adac8..0000000 --- a/components/inputs/src/common/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * input base modules - */ -export * from './inputbase'; diff --git a/components/inputs/src/common/inputbase.tsx b/components/inputs/src/common/inputbase.tsx deleted file mode 100644 index 4cde470..0000000 --- a/components/inputs/src/common/inputbase.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { ChangeEvent, useCallback, JSX, FocusEvent, KeyboardEvent as ReactKeyboardEvent, forwardRef } from 'react'; -import { SvgIcon } from '@syncfusion/react-base'; - -/** - * Constant object containing CSS class names used throughout the component. - */ - -export const CLASS_NAMES: { - RTL: string; - DISABLE: string; - WRAPPER: string; - INPUT: string; - INPUTGROUP: string; - FLOATINPUT: string; - FLOATLINE: string; - FLOATTEXT: string; - CLEARICON: string; - CLEARICONHIDE: string; - LABELTOP: string; - LABELBOTTOM: string; - VALIDINPUT: string; - TEXTBOX_FOCUS: string; -} = { - RTL: 'sf-rtl', - DISABLE: 'sf-disabled', - WRAPPER: 'sf-control-wrapper', - INPUT: 'sf-input', - INPUTGROUP: 'sf-input-group', - FLOATINPUT: 'sf-float-input', - FLOATLINE: 'sf-float-line', - FLOATTEXT: 'sf-float-text', - CLEARICON: 'sf-clear-icon', - CLEARICONHIDE: 'sf-clear-icon-hide', - LABELTOP: 'sf-label-top', - LABELBOTTOM: 'sf-label-bottom', - VALIDINPUT: 'sf-valid-input', - TEXTBOX_FOCUS: 'sf-input-focus' -}; - -export interface IInput { - placeholder: string; - className: string; - disabled?: boolean; - readOnly?: boolean; - floatLabelType?: FloatLabelType; - onChange: (event: ChangeEvent) => void; -} - -/** - * Represents the behavior options for floating labels in form fields. - * - * @enum {string} - */ -export enum FloatLabel { - /** - * Label never floats, remains in its default position regardless of field state. - */ - Never = 'Never', - - /** - * Label always appears in the floating position, regardless of field state. - */ - Always = 'Always', - - /** - * Label automatically floats when the field has content or is focused, - * and returns to default position when empty and not focused. - */ - Auto = 'Auto' -} - -/** - * Type definition for float label type. - */ -export type FloatLabelType = FloatLabel | string; - -/** - * Interface for input arguments. - */ -export interface IInputArgs { - customTag?: string; - floatLabelType?: FloatLabelType; - placeholder?: string; - width?: number | string; - value?: string; - defaultValue?: string; - type?: string; - role?: string; - name?: string; - tabIndex?: number; - onChange?: (event: ChangeEvent) => void; - onFocus?: any; - onBlur?: any; - onKeyDown?: any; -} - -export type InputArgs = IInputArgs & Omit, keyof IInputArgs>; - -export const InputBase: React.ForwardRefExoticComponent> = - forwardRef(({ - type, readOnly = false, disabled = false, floatLabelType = 'Never', onFocus, className = '', - onBlur, placeholder, onKeyDown, value, defaultValue, onChange, ...rest - }: InputArgs, ref: React.ForwardedRef) => { - const inputClassNames: () => string = () => { - return classArray.join(' '); - }; - - const classArray: string[] = [CLASS_NAMES.INPUT, className]; - - const handleFocus: (event: FocusEvent) => void = useCallback((event: FocusEvent) => { - if (onFocus) { - onFocus(event); - } - }, [onFocus]); - - const handleBlur: (event: FocusEvent) => void = useCallback((event: FocusEvent) => { - if (onBlur) { - onBlur(event); - } - }, [onBlur]); - - const handleKeyDown: (event: ReactKeyboardEvent) => void = (event: ReactKeyboardEvent) => { - if (onKeyDown) { - onKeyDown(event); - } - }; - - const handleChange: (event: ChangeEvent) => void = useCallback((event: ChangeEvent) => { - if (onChange) { - onChange(event); - } - }, [onChange]); - - const isControlled: boolean = value !== undefined; - const inputValue: { - value: string | undefined; - defaultValue?: undefined; - } | { - defaultValue: string | undefined; - value?: undefined; - } = isControlled ? { value } : { defaultValue }; - - return ( - - ); - }); - -/** - * Renders the float label element. - * - * @param {FloatLabelType} floatLabelType - The type of float label. - * @param {boolean} isFocused - Whether the input is focused. - * @param {string} inputValue - The current input value. - * @param {string} placeholder - The placeholder text. - * @param {any} id - The reference to the input element. - * @returns {React.ReactElement | null} A React element representing the float label, or null if not applicable. - */ -export const renderFloatLabelElement: (floatLabelType: FloatLabelType, - isFocused: boolean, inputValue: string | number, placeholder: string | undefined, - id: string) => React.ReactElement | null = ( - floatLabelType: FloatLabelType, - isFocused: boolean, - inputValue: string | number, - placeholder: string = '', - id: string -): React.ReactElement | null => { - if (floatLabelType === 'Never') {return null; } - return ( - <> - - - - ); -}; - -export const renderClearButton: (inputValue: string, clearInput: () => void) => JSX.Element = -(inputValue: string, clearInput: () => void) => ( - - - -); diff --git a/components/inputs/src/form-validator/form-validator.tsx b/components/inputs/src/form-validator/form-validator.tsx new file mode 100644 index 0000000..fd9a062 --- /dev/null +++ b/components/inputs/src/form-validator/form-validator.tsx @@ -0,0 +1,1092 @@ +import * as React from 'react'; +import { forwardRef, useEffect, useCallback, useRef, useImperativeHandle, FormEvent, Ref, FormHTMLAttributes, createContext } from 'react'; +import {IL10n, L10n, preRender, useProviderContext} from '@syncfusion/react-base'; + +const VALIDATION_REGEX: { [key: string]: RegExp } = { + EMAIL: /^(?!.*\.\.)[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, + // eslint-disable-next-line security/detect-unsafe-regex + URL: /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/[^\s]*)?$/, + DATE_ISO: /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/, + DIGITS: /^[0-9]*$/, + PHONE: /^[+]?[0-9]{9,13}$/, + CREDIT_CARD: /^\d{13,16}$/ +}; + +/** + * Specifies the possible value types for form fields. + */ +export type FormValueType = string | number | boolean | Date | File | FileList | string[] | number[] | React.ReactNode | null | undefined; + +interface FormContextProps { + registerField?: (fieldName: string) => void; +} + +const FormContext: React.Context = createContext(null); +const FormProvider: React.Provider = FormContext.Provider; + +/** + * Defines the structure for a validation rule in the form system. + * + * A ValidationRule is a two-part array containing: + * - A validation condition (as boolean, RegExp, number, function, etc.) + * - An optional custom error message to show when validation fails + * + * The first element's type depends on the specific rule: + * - For boolean rules (like 'required'): true/false + * - For pattern rules (like 'email', 'regex'): RegExp object + * - For range rules: number or number[] (min/max values) + * - For equality checks: string (field name to compare with) + * - For custom validation: () => boolean function + * + * ```tsx + * const requiredRule: ValidationRule = [true, 'This field is required']; + * ``` + */ +export type ValidationRule = [boolean | RegExp | number | number[] | string | Date | (() => boolean), string?]; + +/** + * Defines the comprehensive set of validation rules that can be applied to form fields. + * + * This interface outlines all available validation types that can be configured for + * each field in the form. Each validation type accepts a ValidationRule containing + * both the validation criteria and an optional custom error message. + */ +export interface FieldValidationRules { + /** + * Validates that the field has a non-empty value. When configured with [true], the field cannot be empty, null, or undefined. + * + * ```tsx + * required: [true, 'This field must be filled in'] + * ``` + */ + required?: ValidationRule; + + /** + * Validates that the input conforms to a standard email address format. Checks for proper formatting with @ symbol and domain structure. + * + * ```tsx + * email: [true, 'Please enter a valid email address'] + * ``` + */ + email?: ValidationRule; + + /** + * Validates that the input is a properly formatted URL. Checks for proper protocol, domain structure, and path format. + * + * ```tsx + * url: [true, 'Please enter a valid website URL'] + * ``` + */ + url?: ValidationRule; + + /** + * Validates that the input can be parsed as a valid date. Uses Date.parse() to validate the string can be converted to a date. + * + * ```tsx + * date: [true, 'Please enter a valid date'] + * ``` + */ + date?: ValidationRule; + + /** + * Validates that the input follows ISO date format (YYYY-MM-DD). Ensures strict compliance with the ISO date standard. + * + * ```tsx + * dateIso: [true, 'Date must be in YYYY-MM-DD format'] + * ``` + */ + dateIso?: ValidationRule; + + /** + * Validates that the input contains a valid numeric value. Ensures the field can be converted to a number without errors. + * + * ```tsx + * number: [true, 'Please enter a number'] + * ``` + */ + number?: ValidationRule; + + /** + * Validates that the input contains only numeric digits (0-9). Rejects inputs containing decimal points, signs, letter characters or spaces. + * + * ```tsx + * digits: [true, 'Please enter only numeric digits'] + * ``` + */ + digits?: ValidationRule; + + /** + * Validates that the input is a valid credit card number. Checks length (13-16 digits) and applies Luhn algorithm validation. + * + * ```tsx + * creditCard: [true, 'Invalid credit card number'] + * ``` + */ + creditCard?: ValidationRule; + + /** + * Validates that a string has at least the specified minimum length. Takes a number as the first parameter in the validation rule. + * + * ```tsx + * minLength: [6, 'Must be at least 6 characters long'] + * ``` + */ + minLength?: ValidationRule; + + /** + * Validates that a string doesn't exceed the specified maximum length. Takes a number as the first parameter in the validation rule. + * + * ```tsx + * maxLength: [100, 'Cannot exceed 100 characters'] + * ``` + */ + maxLength?: ValidationRule; + + /** + * Validates that a string's length falls within the specified range. Takes an array of two numbers [min, max] as the first parameter. + * + * ```tsx + * rangeLength: [[8, 16], 'Must be between 8 and 16 characters'] + * ``` + */ + rangeLength?: ValidationRule; + + /** + * Validates that a numeric value is at least the specified minimum. Takes a number as the first parameter in the validation rule. + * + * ```tsx + * min: [18, 'Value must be at least 18'] + * ``` + */ + min?: ValidationRule; + + /** + * Validates that a numeric value doesn't exceed the specified maximum. Takes a number as the first parameter in the validation rule. + * + * ```tsx + * max: [100, 'Value cannot exceed 100'] + * ``` + */ + max?: ValidationRule; + + /** + * Validates that a numeric value falls within the specified range. Takes an array of two numbers [min, max] as the first parameter. + * + * ```tsx + * range: [[1, 10], 'Value must be between 1 and 10'] + * ``` + */ + range?: ValidationRule; + + /** + * Validates that the input matches the specified regular expression pattern. Takes a RegExp object or a string pattern as the first parameter. + * + * ```tsx + * regex: [/^[A-Z][a-z]+$/, 'Must start with capital letter followed by lowercase letters'] + * ``` + */ + regex?: ValidationRule; + + /** + * Validates that the input conforms to a standard telephone number format. Checks for proper formatting of phone numbers with optional country code. + * + * ```tsx + * tel: [true, 'Please enter a valid phone number'] + * ``` + */ + tel?: ValidationRule; + + /** + * Validates that the field's value exactly matches another field's value. Takes the name of another field as the first parameter. + * + * ```tsx + * equalTo: ['password', 'Passwords must match'] + * ``` + */ + equalTo?: ValidationRule; + + /** + * Allows for completely custom validation logic as a function. + * + * This function receives the field value and should return either: + * - A string containing an error message if validation fails + * - null if validation passes + * + * ```tsx + * customValidator: (value) => { + * if (typeof value === 'string' && !value.includes('@company.com')) { + * return 'Email must be a company email'; + * } + * return null; + * } + * ``` + */ + customValidator?: (value: FormValueType) => string | null; +} + +/** + * Defines the complete validation schema for a form by mapping field names to their validation rules. + * + * This interface creates a dictionary where each key is a field name and each value + * is an object containing all validation rules that apply to that field. The ValidationRules + * object is passed to the Form component to establish the validation criteria for the entire form. + * + * ```tsx + * const validationSchema: ValidationRules = { + * username: { + * required: [true, 'Username is required'], + * minLength: [3, 'Username must be at least 3 characters'] + * }, + * + * email: { + * required: [true, 'Email is required'], + * email: [true, 'Please enter a valid email address'] + * } + * }; + * ``` + */ +export interface ValidationRules { + [fieldName: string]: FieldValidationRules; +} + +/** + * Specifies the state and callback properties provided by the Form component. + * + * The FormState interface provides comprehensive access to the form's state + * and behavior through the onFormStateChange callback. It allows parent components + * to build custom form UIs with full access to validation state, field values, and form event handlers. + */ +export interface FormState { + /** + * Specifies the current values for all form fields indexed by field name. + * Access individual values using: values.fieldName + * + * ```tsx + * const username = formState.values.username; + * ``` + */ + values: Record; + + /** + * Specifies the current validation errors for all fields indexed by field name. + * Fields without errors won't appear in this object. + * + */ + errors: Record; + + /** + * Specifies which fields in the form are valid (have no validation errors). + * + */ + valid: Record; + + /** + * Specifies if the form can be submitted. True when all fields are valid; + * otherwise, all fields are marked as touched when attempted to submit. + */ + allowSubmit: boolean; + + /** + * Specifies if the form has been submitted at least once. + * True after a submission attempt regardless of validation result. + */ + submitted: boolean; + + /** + * Specifies which fields in the form have been modified from their initial values. + * + */ + modified: Record; + + /** + * Specifies which fields have been touched (blurred after interaction). + * Useful for determining when to display validation messages. + * + */ + touched: Record; + + /** + * Specifies which fields have been visited (received focus). + * + */ + visited: Record; + + /** + * Specifies a dictionary of field names for easy access to form fields. + * + */ + fieldNames: Record; + + /** + * Specifies the callback function to handle field value changes. + * Use this instead of directly modifying input elements. + * + * @param name - The name of the field to update + * @param options - Object containing the new value for the field. + * @event onChange + */ + onChange(name: string, options: { value: FormValueType }): void; + + /** + * Specifies the callback function for when a field loses focus. + * + * @param fieldName - The name of the field that lost focus. + * @event onBlur + */ + onBlur(fieldName: string): void; + + /** + * Specifies the callback function for when a field receives focus. + * Records the field as visited. + * + * @param fieldName - The name of the field that received focus. + * @event onFocus + */ + onFocus(fieldName: string): void; + + /** + * Specifies the callback function to reset the form to its initial state. + * Clears all values, errors, and interaction states. + * + * @event onFormReset + */ + onFormReset(): void; + + /** + * Specifies the callback function to submit the form. + * Can be used with a submit button's onClick event. + * + * @param event - The synthetic event from the form submission. + * @event onSubmit + */ + onSubmit(event: React.SyntheticEvent): void; +} + +/** + * Specifies initial values for form fields. Maps field names to their default values when the form loads. + * + * ```tsx + * const initialValues: FormInitialValues = { + * username: 'john_doe' + * }; + * ``` + */ +export interface FormInitialValues { + [fieldName: string]: FormValueType; +} + +/** + * Specifies the props interface for the Form component. + */ +export interface FormProps { + /** + * Specifies the validation rules for each form field. This object defines constraints that form fields must satisfy. + */ + rules: ValidationRules; + + /** + * Specifies the callback fired when form is submitted and all validation rules pass. + * + * @param {Record} data - Specifies the form data containing all field values + * @event onSubmit + */ + onSubmit?: (data: Record) => void; + + /** + * Specifies the callback fired when form state changes. + * This can be used to access form state from parent components for custom UI rendering. + * + * @param {FormState} formState - The current state of the form + * @event onFormStateChange + */ + onFormStateChange?: (formState: FormState) => void; + + /** + * Specifies the initial values for form fields. These values populate the form. + * + * @default - + */ + initialValues?: FormInitialValues; + + /** + * Specifies whether to trigger validation on every input change. When true, validation occurs on each keystroke, providing immediate feedback. + * + * @default false + */ + validateOnChange?: boolean; +} + +/** + * Specifies the FormValidator interface for imperative methods. + */ +export interface IFormValidator extends FormProps { + /** + * Validates the entire form against all defined rules. + * + * @returns {boolean} - Returns true if the form is valid, false otherwise. + */ + validate(): boolean; + + /** + * Resets the form to its initial state, clearing all values, errors, and interaction states. + * + * @returns {void} + */ + reset(): void; + + /** + * Validates a specific field against its defined rules. + * + * @param {string} fieldName - Specifies the name of the field to validate + * @returns {boolean} - Returns true if the field is valid, false otherwise + */ + validateField(fieldName: string): boolean; + + /** + * Provides access to the underlying HTML form element. + * + * @private + */ + element?: HTMLFormElement; +} + +interface FormData { + values: Record; + errors: Record; + touched: Record; + visited: Record; + modified: Record; + submitted: boolean; + validated: Record; +} + +type FormComponentProps = FormProps & Omit, 'onSubmit'>; + +/** + * Provides a form component with built-in validation functionality. Manages form state tracking, + * field validation, and submission handling. + * + * ```typescript + * import { Form, FormField, FormState } from '@syncfusion/react-inputs'; + * + * const [formState, setFormState] = useState(); + * + *
console.log(data)} + * onFormStateChange={setFormState} > + * + * formState?.onChange('username', { value: e.target.value })} + * onBlur={() => formState?.onBlur('username')} + * onFocus={() => formState?.onFocus('username')} + * /> + * {formState?.errors?.username && (
{formState.errors.username}
)} + *
+ * + *
+ * ``` + */ +export const Form: React.ForwardRefExoticComponent> = +forwardRef((props: FormComponentProps, ref: Ref) => { + const { + rules, + onSubmit, + onReset, + children, + onFormStateChange, + initialValues = {}, + validateOnChange = false, + className = '', + ...otherProps + } = props; + const formRef: React.RefObject = useRef(null); + const { locale, dir } = useProviderContext(); + const stateRef: React.RefObject = useRef({ + values: { ...initialValues }, + errors: {}, + touched: {}, + visited: {}, + modified: {}, + submitted: false, + validated: {} + }); + + const notifyStateChange: () => void = useCallback(() => { + if (onFormStateChange) { + formStateRef.current = getFormState(); + onFormStateChange(formStateRef.current); + } + }, [onFormStateChange]); + + const setFieldValue: (field: string, value: FormValueType) => void = useCallback((field: string, value: FormValueType) => { + stateRef.current = { + ...stateRef.current, + values: { + ...stateRef.current.values, + [field]: value + }, + modified: { + ...stateRef.current.modified, + [field]: true + } + }; + }, []); + + const setFieldTouched: (field: string) => void = useCallback((field: string) => { + stateRef.current = { + ...stateRef.current, + touched: { + ...stateRef.current.touched, + [field]: true + } + }; + }, []); + + const setFieldVisited: (field: string) => void = useCallback((field: string) => { + stateRef.current = { + ...stateRef.current, + visited: { + ...stateRef.current.visited, + [field]: true + } + }; + }, []); + + const setFieldError: (field: string, error: string | null) => void = useCallback((field: string, error: string | null) => { + const errors: Record = { ...stateRef.current.errors }; + if (error) { + errors[field as string] = error; + } else { + delete errors[field as string]; + } + stateRef.current = { + ...stateRef.current, + errors + }; + }, []); + + const setSubmitted: (value: boolean) => void = useCallback((value: boolean) => { + stateRef.current = { + ...stateRef.current, + submitted: value + }; + }, []); + + const resetForm: (values: Record) => void = useCallback((values: Record) => { + stateRef.current = { + values, + errors: {}, + touched: {}, + visited: {}, + modified: {}, + submitted: false, + validated: {} + }; + notifyStateChange(); + }, [notifyStateChange]); + + const touchAllFields: () => void = useCallback(() => { + const allTouched: Record = {}; + Object.keys(stateRef.current.values).forEach((field: string) => { + allTouched[field as string] = true; + }); + stateRef.current = { + ...stateRef.current, + touched: allTouched + }; + }, []); + + const visitAllFields: () => void = useCallback(() => { + const allVisited: Record = {}; + Object.keys(stateRef.current.values).forEach((field: string) => { + allVisited[field as string] = true; + }); + stateRef.current = { + ...stateRef.current, + visited: allVisited + }; + }, []); + + const setBulkErrors: (errors: Record) => void = useCallback((errors: Record) => { + const newErrors: { [x: string]: string; } = { ...stateRef.current.errors }; + Object.entries(errors).forEach(([field, error]: [string, string | null]) => { + if (error) { + newErrors[field as string] = error; + } else { + delete newErrors[field as string]; + } + }); + stateRef.current = { + ...stateRef.current, + errors: newErrors + }; + notifyStateChange(); + }, [notifyStateChange]); + const rulesRef: React.RefObject = useRef(rules); + const formStateRef: React.RefObject = useRef(null); + const l10nRef: React.RefObject = useRef(null); + const defaultErrorMessages: { [rule: string]: string } = { + required: 'This field is required.', + email: 'Please enter a valid email address.', + url: 'Please enter a valid URL.', + date: 'Please enter a valid date.', + dateIso: 'Please enter a valid date (ISO).', + creditCard: 'Please enter valid card number.', + number: 'Please enter a valid number.', + digits: 'Please enter only digits.', + maxLength: 'Please enter no more than {0} characters.', + minLength: 'Please enter at least {0} characters.', + rangeLength: 'Please enter a value between {0} and {1} characters long.', + range: 'Please enter a value between {0} and {1}.', + max: 'Please enter a value less than or equal to {0}.', + min: 'Please enter a value greater than or equal to {0}.', + regex: 'Please enter a correct value.', + tel: 'Please enter a valid phone number.', + equalTo: 'Please enter the same value again.' + }; + const registeredFields: React.RefObject> = useRef>({}); + const registerField: (fieldName: string) => void = (fieldName: string) => { + registeredFields.current[fieldName as string] = true; + }; + + useEffect(() => { + l10nRef.current = L10n('formValidator', defaultErrorMessages, locale); + return () => { + l10nRef.current = null; + }; + }, [locale]); + + useEffect(() => { + notifyStateChange(); + validateInitialValues(); + preRender('formValidator'); + return () => { + l10nRef.current = null; + rulesRef.current = {}; + registeredFields.current = {}; + if (formRef.current) { + formRef.current = null; + } + formStateRef.current = null; + if (onFormStateChange) { + onFormStateChange(formStateRef.current as unknown as FormState ); + } + }; + }, []); + + useEffect(() => { + rulesRef.current = rules; + }, [rules]); + + const validateInitialValues: () => void = (): void => { + if (Object.keys(initialValues).length > 0) { + const errors: Record = {}; + + for (const fieldName in initialValues) { + if (Object.prototype.hasOwnProperty.call(initialValues, fieldName) && + Object.prototype.hasOwnProperty.call(rulesRef.current, fieldName)) { + const error: string | null = validateFieldValue(fieldName, initialValues[fieldName as string]); + if (error) { + errors[fieldName as string] = error; + } else { + errors[fieldName as string] = null; + } + } + } + if (Object.keys(errors).length > 0) { + setBulkErrors(errors); + } + } + }; + + const formatErrorMessage: (ruleName: string, params: unknown) => string = (ruleName: string, params: unknown): string => { + let formattedMessage: string = l10nRef.current?.getConstant(ruleName) as string; + if (Array.isArray(params)) { + params.forEach((value: unknown, index: number) => { + const placeholder: string = `{${index}}`; + if (formattedMessage.includes(placeholder)) { + formattedMessage = formattedMessage.replace(placeholder, String(value)); + } + }); + } else { + formattedMessage = formattedMessage.replace('{0}', String(params)); + } + + return formattedMessage; + }; + + const validateCreditCard: (value: string) => boolean = (value: string): boolean => { + if (!VALIDATION_REGEX.CREDIT_CARD.test(value)) { + return false; + } + const cardNumber: string = value.replace(/[\s-]/g, ''); + let sum: number = 0; + let shouldDouble: boolean = false; + for (let i: number = cardNumber.length - 1; i >= 0; i--) { + let digit: number = parseInt(cardNumber.charAt(i), 10); + if (shouldDouble) { + digit *= 2; + if (digit > 9) { + digit -= 9; + } + } + sum += digit; + shouldDouble = !shouldDouble; + } + return (sum % 10) === 0; + }; + + const validateFieldValue: (fieldName: string, value: FormValueType) => string | null = + (fieldName: string, value: FormValueType): string | null => { + const fieldRules: FieldValidationRules = rulesRef.current[fieldName as string]; + if (!fieldRules || !registeredFields.current[fieldName as string]) {return null; } + const isValueEmpty: boolean = value === undefined || value === null || value.toString().trim() === ''; + const isRequired: boolean = fieldRules.required != null && fieldRules.required[0] !== false; + if (isValueEmpty && !isRequired) { + return null; + } + + for (const ruleName in fieldRules) { + if (Object.prototype.hasOwnProperty.call(fieldRules, ruleName)) { + const ruleValue: ValidationRule | ((value: FormValueType) => string | null) | undefined = + fieldRules[ruleName as keyof typeof fieldRules]; + if (!ruleValue) {continue; } + let isValid: boolean = true; + let param: unknown = null; + let errorMessage: string | undefined; + + if (ruleName === 'customValidator' && typeof ruleValue === 'function') { + const customError: string | null = ruleValue(value); + if (customError) { + return customError; + } + continue; + } + + if (Array.isArray(ruleValue)) { + param = ruleValue[0]; + errorMessage = ruleValue[1]; + + if (ruleName === 'required' && param === false) { + continue; + } + } + + if (ruleName !== 'required' && (value === '' || value === null || value === undefined)) { + continue; + } + + switch (ruleName) { + case 'required': + isValid = !isValueEmpty; + break; + case 'email': + isValid = VALIDATION_REGEX.EMAIL.test(value as string); + break; + case 'url': + isValid = VALIDATION_REGEX.URL.test(value as string); + break; + case 'date': + isValid = !isNaN(Date.parse(value as string)); + break; + case 'dateIso': + isValid = VALIDATION_REGEX.DATE_ISO.test(value as string); + break; + case 'number': + isValid = !isNaN(Number(value)) && String(value).indexOf(' ') === -1; + break; + case 'digits': + isValid = VALIDATION_REGEX.DIGITS.test(value as string); + break; + case 'creditCard': + isValid = validateCreditCard(value as string); + break; + case 'minLength': + isValid = String(value).length >= Number(param); + break; + case 'maxLength': + isValid = String(value).length <= Number(param); + break; + case 'rangeLength': + if (Array.isArray(param)) { + isValid = String(value).length >= param[0] && String(value).length <= param[1]; + } + break; + case 'min': + isValid = Number(value) >= Number(param); + break; + case 'max': + isValid = Number(value) <= Number(param); + break; + case 'range': + if (Array.isArray(param)) { + isValid = Number(value) >= param[0] && Number(value) <= param[1]; + } + break; + case 'regex': + if (param instanceof RegExp) { + isValid = param.test(value as string); + } else if (typeof param === 'string') { + // eslint-disable-next-line security/detect-non-literal-regexp + isValid = new RegExp(param).test(value as string); + } + break; + case 'tel': + isValid = VALIDATION_REGEX.PHONE.test(value as string); + break; + case 'equalTo': + if (typeof param === 'string') { + isValid = value === stateRef.current.values[param as string]; + } + break; + } + + if (!isValid) { + if (errorMessage) { + return errorMessage; + } else { + return formatErrorMessage(ruleName, param); + } + } + } + } + + return null; + }; + + const validateForm: () => Record = (): Record => { + const errors: Record = {}; + const fields: string[] = Object.keys(rulesRef.current); + + for (const field of fields) { + const error: string | null = validateFieldValue(field, stateRef.current.values[field as string]); + if (!stateRef.current.values[field as string]) { + setFieldValue(field, stateRef.current.values[field as string]); + } + if (error) { + errors[field as string] = error; + } + } + + return errors; + }; + + const validate: () => boolean = (): boolean => { + const formErrors: Record = validateForm(); + for (const field in formErrors) { + if (Object.prototype.hasOwnProperty.call(formErrors, field)) { + setFieldError(field, formErrors[field as string]); + } + } + const fields: string[] = Object.keys(rulesRef.current); + for (const field of fields) { + if (!formErrors[field as string]) { + setFieldError(field, null); + } + } + if (Object.keys(formErrors).length === 0) { + stateRef.current = { + ...stateRef.current, + errors: {}, + touched: {}, + visited: {}, + modified: {}, + submitted: false, + validated: {} + }; + } + notifyStateChange(); + + return Object.keys(formErrors).length === 0; + }; + + const handleSubmit: (event: FormEvent | React.SyntheticEvent) => void = + (event: FormEvent | React.SyntheticEvent): void => { + event?.preventDefault(); + const isValid: boolean = validate(); + touchAllFields(); + visitAllFields(); + setSubmitted(true); + notifyStateChange(); + if (isValid) { + onSubmit?.(stateRef.current.values); + } + }; + + const handleChange: (name: string, { value }: { value: FormValueType; }) => void = + (name: string, { value }: { value: FormValueType }): void => { + setFieldValue(name, value); + if (validateOnChange) { + const error: string | null = validateFieldValue(name, value); + setFieldError(name, error); + } + notifyStateChange(); + }; + + const handleBlur: (fieldName: string) => void = (fieldName: string): void => { + setFieldTouched(fieldName); + const error: string | null = validateFieldValue(fieldName, stateRef.current.values[fieldName as string]); + setFieldError(fieldName, error); + notifyStateChange(); + }; + + const handleFormReset: (args?: FormEvent) => void = + (args?: FormEvent): void => { + resetForm({ ...initialValues }); + onReset?.(args as FormEvent); + }; + + const reset: () => void = (): void => { + handleFormReset(); + }; + + const getFormState: () => FormState = (): FormState => { + const state: FormData = stateRef.current; + return { + values: state.values, + errors: state.errors, + submitted: state.submitted, + touched: state.touched, + visited: state.visited, + modified: state.modified, + valid: Object.keys(rulesRef.current).reduce((acc: Record, fieldName: string) => { + acc[fieldName as string] = !state.errors[fieldName as string]; + return acc; + }, {} as Record), + allowSubmit: Object.keys(state.errors).length === 0, + onChange: handleChange, + onBlur: handleBlur, + onFocus: handleFocus, + onFormReset: handleFormReset, + onSubmit: handleSubmit, + fieldNames: Object.keys(registeredFields.current).reduce((acc: Record, fieldName: string) => { + acc[fieldName as string] = fieldName; + return acc; + }, {} as Record) + }; + }; + + const validateField: (fieldName: string) => boolean = (fieldName: string): boolean => { + const error: string | null = validateFieldValue(fieldName, stateRef.current.values[fieldName as string]); + setFieldError(fieldName, error); + notifyStateChange(); + return !error; + }; + const publicAPI: Partial = React.useMemo(() => ({ + rules, + initialValues, + validateOnChange + }), [rules, initialValues, validateOnChange]); + + useImperativeHandle(ref, () => ({ + ...publicAPI as IFormValidator, + validate, + reset, + validateField, + element: formRef.current as HTMLFormElement + }), [publicAPI]); + + const formClassName: string = React.useMemo(() => { + return [ + 'sf-control sf-form-validator', + dir === 'rtl' ? 'sf-rtl' : '', + className + ].filter(Boolean).join(' '); + }, [dir, className]); + + const handleFocus: (fieldName: string) => void = (fieldName: string): void => { + setFieldVisited(fieldName); + notifyStateChange(); + }; + const formContextValue: FormContextProps = { + registerField: registerField + }; + + return ( + +
+ {children} +
+
+ ); +} +); + +Form.displayName = 'Form'; + +/** + * Specifies the properties for the FormField component. + */ +export interface FormFieldProps { + /** + * Specifies the name of the field that must match a key in the rules object. This is required for proper validation. + */ + name: string; + + /** + * Specifies the children content for the form field. Children should include the actual form control elements like inputs, textarea, etc. + */ + children: React.ReactNode; +} + +/** + * Specifies a component that connects form inputs with validation rules. The FormField component provides + * an easy way to integrate form controls with the Form validation system, handling state management and + * validation automatically. + * + * ```typescript + * const [formState, setFormState] = useState(); + * + *
console.log(data)} + * onFormStateChange={setFormState} + * > + * + * formState?.onChange('username', { value: e.target.value })} + * onBlur={() => formState?.onBlur('username')} + * onFocus={() => formState?.onFocus('username')} + * /> + * {formState?.touched?.username && formState?.errors?.username && ( + *
{formState.errors.username}
+ * )} + *
+ * + *
+ * ``` + * + * @param {IFormFieldProps} props - Specifies the form field configuration properties + * @returns {React.ReactNode} - Returns the children with access to form validation context + */ +export const FormField: React.FC = (props: FormFieldProps): React.ReactNode => { + const { name, children } = props; + if (!name) { + return null; + } + + const formContext: FormContextProps | null = React.useContext(FormContext); + if (!formContext) { + return null; + } + if (formContext.registerField) { + formContext.registerField(name); + } + return <>{children}; +}; + +FormField.displayName = 'FormField'; diff --git a/components/inputs/src/form-validator/index.ts b/components/inputs/src/form-validator/index.ts new file mode 100644 index 0000000..2c95566 --- /dev/null +++ b/components/inputs/src/form-validator/index.ts @@ -0,0 +1,4 @@ +/** + * FormValidator modules + */ +export * from './form-validator'; diff --git a/components/inputs/src/index.ts b/components/inputs/src/index.ts deleted file mode 100644 index adc54f2..0000000 --- a/components/inputs/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * NumericTextBox all modules - */ -export * from './numerictextbox/index'; -export * from './textbox/index'; -export * from './textarea/index'; -export * from './common/index'; diff --git a/components/buttons/styles/check-box/_all.scss b/components/inputs/src/inputs/input/_all.scss similarity index 100% rename from components/buttons/styles/check-box/_all.scss rename to components/inputs/src/inputs/input/_all.scss diff --git a/components/inputs/styles/input/_layout.scss b/components/inputs/src/inputs/input/_layout.scss similarity index 56% rename from components/inputs/styles/input/_layout.scss rename to components/inputs/src/inputs/input/_layout.scss index e309ed7..bf64c96 100644 --- a/components/inputs/styles/input/_layout.scss +++ b/components/inputs/src/inputs/input/_layout.scss @@ -35,19 +35,16 @@ font-family: $input-font-family; font-size: $input-font-size; font-weight: normal; - @if ($input-skin-name == 'tailwind' or $input-skin-name == 'tailwind3') { - font-feature-settings: 'calt' 0; - } } .sf-input-group input.sf-input, .sf-input-group.sf-control-wrapper input.sf-input, .sf-input-group textarea.sf-input, .sf-input-group.sf-control-wrapper textarea.sf-input, - .sf-input-group.sf-small .sf-input, - .sf-input-group.sf-small.sf-control-wrapper .sf-input, - .sf-small .sf-input-group .sf-input, - .sf-small .sf-input-group.sf-control-wrapper .sf-input { + .sf-input-group.sf-medium .sf-input, + .sf-input-group.sf-medium.sf-control-wrapper .sf-input, + .sf-medium .sf-input-group .sf-input, + .sf-medium .sf-input-group.sf-control-wrapper .sf-input { font: inherit; } @@ -109,42 +106,7 @@ .sf-float-input.sf-input-group textarea, .sf-float-input.sf-control-wrapper textarea, .sf-float-input.sf-control-wrapper.sf-input-group textarea { - @if $skin-name == 'fluent2' { - border-radius: 4px; - } - @else { - border-radius: $input-box-border-radius; - } - } - - .sf-input.sf-small, - .sf-input-group.sf-small, - .sf-input-group.sf-control-wrapper.sf-small, - .sf-input-group.sf-small .sf-input, - .sf-input-group.sf-small input, - .sf-input-group.sf-control-wrapper.sf-small .sf-input, - .sf-input-group.sf-control-wrapper.sf-small input, - .sf-float-input.sf-small input, - .sf-float-input.sf-input-group.sf-small input, - .sf-float-input.sf-control-wrapper.sf-small input, - .sf-float-input.sf-control-wrapper.sf-input-group.sf-small input, - .sf-float-input.sf-small, - .sf-float-input.sf-control-wrapper.sf-small, - .sf-small .sf-input-group, - .sf-small .sf-input-group.sf-control-wrapper, - .sf-small .sf-input-group .sf-input, - .sf-small .sf-input-group input, - .sf-small .sf-input-group.sf-control-wrapper .sf-input, - .sf-small .sf-input-group.sf-control-wrapper input, - .sf-small .sf-float-input input, - .sf-small .sf-float-input.sf-input-group input, - .sf-small .sf-float-input.sf-control-wrapper input, - .sf-small .sf-float-input.sf-control-wrapper.sf-input-group input, - .sf-small .sf-float-input, - .sf-small .sf-float-input.sf-control-wrapper { - @if ($input-skin-name == 'tailwind3' or $input-skin-name == 'bootstrap5.3') { - border-radius: $input-small-border-radius; - } + border-radius: $input-box-border-radius; } .sf-input#{$css}:focus { @@ -152,21 +114,17 @@ padding-bottom: $input-padding-bottom; } - .sf-input.sf-small#{$css}:focus { + .sf-input.sf-medium#{$css}:focus { border-width: $input-focus-border-width; - padding-bottom: $input-small-padding-bottom; + padding-bottom: $input-medium-padding-bottom; } .sf-input#{$css}:focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - padding-bottom: $input-focus-padding-bottom; - } + padding-bottom: $input-focus-padding-bottom; } - .sf-input.sf-small#{$css}:focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - padding-bottom: $input-focus-small-padding-bottom; - } + .sf-input.sf-medium#{$css}:focus { + padding-bottom: $input-focus-medium-padding-bottom; } .sf-input-group input.sf-input:focus, @@ -198,98 +156,47 @@ min-width: $input-child-min-width; padding: $input-child-padding; text-align: center; - @if ($input-skin-name == 'bootstrap5.3') { - border: 1px solid; - border-bottom: $zero-value; - border-collapse: collapse; - border-top: $zero-value; - } - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - border-radius: $input-child-icon-border-radius; - } - } - - .sf-input-group .sf-input-group-icon:first-child, - .sf-input-group.sf-control-wrapper .sf-input-group-icon:first-child { - @if ($input-skin-name == 'bootstrap5.3' or $input-skin-name == 'tailwind3') { - border-left-width: $zero-value; - } - } - - .sf-input-group .sf-input-group-icon:last-child, - .sf-input-group.sf-control-wrapper .sf-input-group-icon:last-child { - @if ($input-skin-name == 'bootstrap5.3' or $input-skin-name == 'tailwind3') { - border-bottom-right-radius: $input-group-icon-border-radius; - border-top-right-radius: $input-group-icon-border-radius; - } - } - - .sf-input-group .sf-input-group-icon:first-child, - .sf-input-group.sf-control-wrapper .sf-input-group-icon:first-child { - @if ($input-skin-name == 'bootstrap5.3' or $input-skin-name == 'tailwind3') { - border-bottom-left-radius: $input-group-icon-border-radius; - border-top-left-radius: $input-group-icon-border-radius; - } - } - - .sf-input-group.sf-rtl .sf-input-group-icon:last-child, - .sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon:last-child, - .sf-rtl .sf-input-group .sf-input-group-icon:last-child, - .sf-rtl .sf-input-group.sf-control-wrapper .sf-input-group-icon:last-child { - @if ($input-skin-name == 'bootstrap5.3' or $input-skin-name == 'tailwind3') { - border-bottom-left-radius: $input-group-icon-border-radius; - border-bottom-right-radius: $zero-value; - border-top-left-radius: $input-group-icon-border-radius; - border-top-right-radius: $zero-value; - } + border-radius: $input-child-icon-border-radius; } .sf-input-group.sf-float-icon-left > .sf-input-group-icon, .sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon, .sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon, .sf-float-input.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { font-size: $input-left-icon-font-size; margin: $zero-value; min-height: $input-left-child-min-height; min-width: $input-left-child-min-width; padding: $zero-value; - } } - .sf-input-group.sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-input-group.sf-control-wrapper.sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-small .sf-input-group.sf-float-icon-left > .sf-input-group-icon, - .sf-small .sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon, - .sf-float-input.sf-input-group.sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-small .sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon, - .sf-float-input.sf-control-wrapper.sf-input-group.sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-small .sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - font-size: $input-small-left-icon-font-size; - margin: $zero-value; - min-height: $input-small-left-child-min-height; - min-width: $input-small-left-child-min-width; - padding: $zero-value; - } + .sf-input-group.sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-input-group.sf-control-wrapper.sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-medium .sf-input-group.sf-float-icon-left > .sf-input-group-icon, + .sf-medium .sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon, + .sf-float-input.sf-input-group.sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-medium .sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon, + .sf-float-input.sf-control-wrapper.sf-input-group.sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-medium .sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon { + font-size: $input-medium-left-icon-font-size; + margin: $zero-value; + min-height: $input-medium-left-child-min-height; + min-width: $input-medium-left-child-min-width; + padding: $zero-value; } .sf-input-group.sf-float-icon-left:not(.sf-disabled) > .sf-input-group-icon:active, .sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-disabled) > .sf-input-group-icon:active { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - background: transparent; - } + background: transparent; } .sf-input-group.sf-float-icon-left > .sf-input-group-icon, .sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - cursor: auto; - } + cursor: auto; } .sf-input#{$css}[disabled], @@ -330,27 +237,6 @@ cursor: not-allowed; } - .sf-input#{$css}[disabled], - .sf-input-group.sf-disabled, - .sf-input-group.sf-control-wrapper.sf-disabled, - .sf-float-input input[disabled], - .sf-float-input input.sf-disabled, - .sf-float-input.sf-control-wrapper input[disabled], - .sf-float-input.sf-control-wrapper input.sf-disabled { - @if $input-skin-name != 'Material3' { - border-color: $input-disable-border-color; - border-style: $input-disable-border-type; - } - } - - .sf-input-group.sf-disabled, - .sf-input-group.sf-control-wrapper.sf-disabled { - @if $input-skin-name != 'Material3' { - border-bottom-style: $input-disable-border-type; - border-width: $input-disable-group-border-width; - } - } - .sf-input#{$css}[disabled], .sf-input-group.sf-disabled, .sf-input-group.sf-control-wrapper.sf-disabled, @@ -405,9 +291,7 @@ .sf-input-group .sf-input-group-icon:first-child, .sf-input-group.sf-control-wrapper .sf-input-group-icon:first-child { - @if ($input-skin-name != 'tailwind3' and $input-skin-name != 'bootstrap5.3') { border-left-width: $input-border-left-width; - } } .sf-input-group .sf-input-group-icon, @@ -417,17 +301,13 @@ .sf-input-group .sf-input-group-icon:not(:last-child), .sf-input-group.sf-control-wrapper .sf-input-group-icon:not(:last-child) { - @if ($input-skin-name != 'bootstrap5.3') { border-right-width: $zero-value; - } } .sf-input + .sf-input-group-icon, .sf-input-group .sf-input + .sf-input-group-icon, .sf-input-group.sf-control-wrapper .sf-input + .sf-input-group-icon { - @if ($input-skin-name != 'bootstrap5.3') { border-left-width: $zero-value; - } } .sf-input-group.sf-corner .sf-input:first-child, @@ -448,74 +328,68 @@ .sf-input-group.sf-rtl .sf-input-group-icon:first-child, .sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon:first-child { - @if ($input-skin-name != 'bootstrap5.3') { border-left-width: $zero-value; border-right-width: $input-border-left-width; - } } .sf-input-group.sf-rtl .sf-input-group-icon:last-child, .sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon:last-child { - @if $input-skin-name != 'tailwind3' and $input-skin-name != 'bootstrap5.3' { border-left-width: $input-border-left-width; border-right-width: $zero-value; - } } .sf-input-group.sf-rtl .sf-input-group-icon:not(:last-child), .sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon:not(:last-child) { - @if ( $input-skin-name != 'bootstrap5.3' and $input-skin-name != 'tailwind3') { border-left-width: $input-border-left-width; - } } .sf-input-group.sf-rtl .sf-input-group-icon + .sf-input, .sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon + .sf-input { - @if ( $input-skin-name != 'bootstrap5.3') { border-right-width: $zero-value; - } } - input.sf-input.sf-small#{$css}, - textarea.sf-input.sf-small#{$css}, - .sf-small input.sf-input#{$css}, - .sf-small textarea.sf-input#{$css}, - .sf-input-group.sf-small, - .sf-small .sf-input-group, - .sf-input-group.sf-control-wrapper.sf-small, - .sf-small .sf-input-group.sf-control-wrapper, - .sf-input-group.sf-small.sf-disabled, - .sf-small .sf-input-group.sf-disabled, - .sf-input-group.sf-control-wrapper.sf-small.sf-disabled, - .sf-small .sf-input-group.sf-control-wrapper.sf-disabled { - font-size: $input-small-font-size; - } - - .sf-input#{$css}.sf-small, - .sf-input-group.sf-small .sf-input, - .sf-input-group.sf-control-wrapper.sf-small .sf-input { + input.sf-input.sf-medium#{$css}, + textarea.sf-input.sf-medium#{$css}, + .sf-medium input.sf-input#{$css}, + .sf-medium textarea.sf-input#{$css}, + .sf-input-group.sf-medium, + .sf-medium .sf-input-group, + .sf-input-group.sf-control-wrapper.sf-medium, + .sf-medium .sf-input-group.sf-control-wrapper, + .sf-input-group.sf-medium.sf-disabled, + .sf-medium .sf-input-group.sf-disabled, + .sf-input-group.sf-control-wrapper.sf-medium.sf-disabled, + .sf-medium .sf-input-group.sf-control-wrapper.sf-disabled { + font-size: $input-medium-font-size; + } + + .sf-input#{$css}.sf-medium, + .sf-input-group.sf-medium .sf-input, + .sf-input-group.sf-control-wrapper.sf-medium .sf-input { line-height: inherit; - padding: $input-small-padding; - } - - .sf-input-group.sf-small .sf-input:focus, - .sf-input-group.sf-control-wrapper.sf-small .sf-input:focus, - .sf-input-group.sf-small.sf-input-focus .sf-input, - .sf-input-group.sf-control-wrapper.sf-small.sf-input-focus .sf-input { - padding: $input-small-padding; - } - - .sf-input-group.sf-small .sf-input-group-icon, - .sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-small .sf-input-group .sf-input-group-icon, - .sf-small .sf-input-group.sf-control-wrapper .sf-input-group-icon { - font-size: $input-small-icon-font-size; - min-height: $input-small-child-min-height; - min-width: $input-small-child-min-width; - padding: $input-small-child-padding; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - border-radius: $input-small-child-border-radius; - } + padding: $input-medium-padding; + } + + .sf-input-group.sf-medium .sf-input:focus, + .sf-input-group.sf-control-wrapper.sf-medium .sf-input:focus, + .sf-input-group.sf-medium.sf-input-focus .sf-input, + .sf-input-group.sf-control-wrapper.sf-medium.sf-input-focus .sf-input { + padding: $input-medium-padding; + } + + .sf-input-group.sf-medium .sf-input-group-icon, + .sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-medium .sf-input-group .sf-input-group-icon, + .sf-medium .sf-input-group.sf-control-wrapper .sf-input-group-icon { + font-size: $input-medium-icon-font-size; + min-height: $input-medium-child-min-height; + min-width: $input-medium-child-min-width; + padding: $input-medium-child-padding; + border-radius: $input-medium-child-border-radius; + + svg { + font-size: $input-medium-left-icon-font-size; + } } label.sf-float-text, @@ -535,7 +409,7 @@ top: -11px; transform: translate3d(0, 16px, 0) scale(1); transform-origin: left top; - transition: .25s cubic-bezier(.25, .8, .25, 1); + transition: $transition-duration-standard $transition-standard-curve; user-select: none; white-space: nowrap; width: 100%; @@ -553,34 +427,30 @@ .sf-float-input label.sf-float-text, .sf-float-input.sf-control-wrapper label.sf-float-text, .sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-float-input.sf-medium:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { + .sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { @include float-label-alignment; font-style: $input-font-style; } - .sf-float-input.sf-small label.sf-float-text, - .sf-float-input.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-control-wrapper.sf-small label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - font-size: $float-placeholder-small-font-size; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -11px; - } + .sf-float-input.sf-medium label.sf-float-text, + .sf-float-input.sf-medium:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-float-input.sf-control-wrapper.sf-medium label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { + font-size: $float-placeholder-medium-font-size; + top: -6px; } .sf-float-input .sf-input-in-wrap label.sf-float-text, .sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -11px; - } + top: -11px; } .sf-float-input input:focus ~ label.sf-float-text, @@ -598,73 +468,37 @@ .sf-float-input.sf-control-wrapper.sf-input-focus input ~ label.sf-float-text, .sf-float-input.sf-input-focus input ~ label.sf-float-text { font-size: $float-label-font-size; - @if $input-skin-name == 'Material3' { top: -9px; transform: translate3d(0, -6px, 0) scale(.92); - } - @else if $input-skin-name == 'fluent2' { - padding: 0; - top: -15px; - transform: translate3d(0, -6px, 0) scale(.92); - } - @else if ($input-skin-name == 'tailwind3') { - font-weight: 500; - padding-right: 0; - transform: translate3d(-10px, -35px, 0) scale(1); - padding-left: 0; - left: 12px; - top: 40%; - } - @else if ($input-skin-name == 'bootstrap5.3') { - font-weight: 500; - padding-right: 0; - transform: translate3d(-10px, -36px, 0) scale(1); - } } - .sf-float-input.sf-small input:focus ~ label.sf-float-text, - .sf-float-input.sf-small input:valid ~ label.sf-float-text, - .sf-float-input.sf-small input ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small input label.sf-float-text.sf-label-top, - .sf-small .sf-float-input input:focus ~ label.sf-float-text, - .sf-small .sf-float-input input:valid ~ label.sf-float-text, - .sf-small .sf-float-input input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input input label.sf-float-text.sf-label-top, - .sf-float-input.sf-control-wrapper.sf-small input:focus ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input:valid ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input label.sf-float-text.sf-label-top, - .sf-small .sf-float-input.sf-control-wrapper input:focus ~ label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper input:valid ~ label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper input label.sf-float-text.sf-label-top { + .sf-float-input.sf-medium input:focus ~ label.sf-float-text, + .sf-float-input.sf-medium input:valid ~ label.sf-float-text, + .sf-float-input.sf-medium input ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium input label.sf-float-text.sf-label-top, + .sf-medium .sf-float-input input:focus ~ label.sf-float-text, + .sf-medium .sf-float-input input:valid ~ label.sf-float-text, + .sf-medium .sf-float-input input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input input label.sf-float-text.sf-label-top, + .sf-float-input.sf-control-wrapper.sf-medium input:focus ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input:valid ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input label.sf-float-text.sf-label-top, + .sf-medium .sf-float-input.sf-control-wrapper input:focus ~ label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper input:valid ~ label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper input label.sf-float-text.sf-label-top { font-size: $float-label-font-size; - @if $input-skin-name == 'fluent2' { - top: -9px; - transform: translate3d(0, -6px, 0) scale(.92); - } - @else if $input-skin-name == 'Material3' { - top: -7px; - transform: translate3d(0, 0, 0) scale(1); - } - @else if ( $input-skin-name == 'tailwind3') { - font-weight: 500; - padding-right: 0; - transform: translate3d(-10px, -34px, 0) scale(1); - } - @else if ( $input-skin-name == 'bootstrap5.3') { - font-weight: 500; - padding-right: 0; - transform: translate3d(-10px, -35px, 0) scale(1); - } + top: -7px; + transform: translate3d(0, 0, 0) scale(1); } .sf-float-input .sf-input-in-wrap input:focus ~ label.sf-float-text, @@ -679,60 +513,48 @@ .sf-float-input.sf-control-wrapper .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper .sf-input-in-wrap input label.sf-float-text.sf-label-top { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { top: -9px; - } } - .sf-float-input.sf-small.sf-outline input:valid ~ label.sf-float-text, - .sf-float-input.sf-small.sf-outline input ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small.sf-outline input:valid ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input.sf-outline ~ label.sf-label-top.sf-float-text { - font-size: $float-label-small-font-size; - @if $input-skin-name == 'Material3' { - top: -6px; - } - } - - .sf-float-input.sf-small input:focus ~ label.sf-float-text, - .sf-float-input.sf-small input:valid ~ label.sf-float-text, - .sf-float-input.sf-small input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input input ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input:focus ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input:valid ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper input ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small.sf-input-focus input-group-animation ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small.sf-input-focus input ~ label.sf-float-text { - font-size: $float-label-small-font-size; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -12px; - } - @if $input-skin-name == 'bootstrap5.3' or $input-skin-name == 'tailwind3' { - padding-right: 0; - top: 17px; - } - } - - .sf-float-input.sf-small .sf-input-in-wrap input:focus ~ label.sf-float-text, - .sf-float-input.sf-small .sf-input-in-wrap input:valid ~ label.sf-float-text, - .sf-float-input.sf-small .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input:focus ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input:valid ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - top: -7px; - } + .sf-float-input.sf-medium.sf-outline input:valid ~ label.sf-float-text, + .sf-float-input.sf-medium.sf-outline input ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium.sf-outline input:valid ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input.sf-outline ~ label.sf-label-top.sf-float-text { + font-size: $float-label-medium-font-size; + top: -6px; + } + + .sf-float-input.sf-medium input:focus ~ label.sf-float-text, + .sf-float-input.sf-medium input:valid ~ label.sf-float-text, + .sf-float-input.sf-medium input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input input ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input:focus ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input:valid ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper input ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium.sf-input-focus input-group-animation ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium.sf-input-focus input ~ label.sf-float-text { + font-size: $float-label-medium-font-size; + top: -12px; + } + + .sf-float-input.sf-medium .sf-input-in-wrap input:focus ~ label.sf-float-text, + .sf-float-input.sf-medium .sf-input-in-wrap input:valid ~ label.sf-float-text, + .sf-float-input.sf-medium .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input:focus ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input:valid ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text { + top: -7px; } .sf-float-input, @@ -745,28 +567,28 @@ width: 100%; } - .sf-float-input.sf-small, - .sf-float-input.sf-control-wrapper.sf-small, - .sf-small .sf-float-input.sf-control-wrapper { - line-height: 1.35; - margin-bottom: $input-small-margin-bottom; - margin-top: $input-small-margin-top; - padding-top: $float-input-small-wrap-padding-top; + .sf-float-input.sf-medium, + .sf-float-input.sf-control-wrapper.sf-medium, + .sf-medium .sf-float-input.sf-control-wrapper { + line-height: 24px; + margin-bottom: $input-medium-margin-bottom; + margin-top: $input-medium-margin-top; + padding-top: $float-input-medium-wrap-padding-top; } - .sf-input-group.sf-small, - .sf-input-group.sf-control-wrapper.sf-small, - .sf-small .sf-input-group, - .sf-small .sf-input-group.sf-control-wrapper { - line-height: normal; + .sf-input-group.sf-medium, + .sf-input-group.sf-control-wrapper.sf-medium, + .sf-medium .sf-input-group, + .sf-medium .sf-input-group.sf-control-wrapper { + line-height: 24px; } .sf-float-input.sf-no-float-label, - .sf-float-input.sf-small.sf-no-float-label, - .sf-small .sf-float-input.sf-no-float-label, + .sf-float-input.sf-medium.sf-no-float-label, + .sf-medium .sf-float-input.sf-no-float-label, .sf-float-input.sf-control-wrapper.sf-no-float-label, - .sf-float-input.sf-control-wrapper.sf-small.sf-no-float-label, - .sf-small .sf-float-input.sf-control-wrapper.sf-no-float-label { + .sf-float-input.sf-control-wrapper.sf-medium.sf-no-float-label, + .sf-medium .sf-float-input.sf-control-wrapper.sf-no-float-label { margin-top: $zero-value; } @@ -809,26 +631,26 @@ text-indent: $input-text-indent; } - .sf-float-input.sf-small.sf-disabled, - .sf-small .sf-float-input.sf-disabled, - .sf-float-input.sf-control-wrapper.sf-small.sf-disabled, - .sf-small .sf-float-input.sf-control-wrapper.sf-disabled, - .sf-float-input.sf-input-group.sf-small.sf-disabled, - .sf-small .sf-float-input.sf-input-group.sf-disabled, - .sf-float-input.sf-input-group.sf-control-wrapper.sf-small.sf-disabled, - .sf-small .sf-float-input.sf-input-group.sf-control-wrapper.sf-disabled, - .sf-float-input.sf-small, - .sf-small .sf-float-input, - .sf-float-input.sf-control-wrapper.sf-small, - .sf-small .sf-float-input.sf-control-wrapper { - font-size: $input-small-font-size; + .sf-float-input.sf-medium.sf-disabled, + .sf-medium .sf-float-input.sf-disabled, + .sf-float-input.sf-control-wrapper.sf-medium.sf-disabled, + .sf-medium .sf-float-input.sf-control-wrapper.sf-disabled, + .sf-float-input.sf-input-group.sf-medium.sf-disabled, + .sf-medium .sf-float-input.sf-input-group.sf-disabled, + .sf-float-input.sf-input-group.sf-control-wrapper.sf-medium.sf-disabled, + .sf-medium .sf-float-input.sf-input-group.sf-control-wrapper.sf-disabled, + .sf-float-input.sf-medium, + .sf-medium .sf-float-input, + .sf-float-input.sf-control-wrapper.sf-medium, + .sf-medium .sf-float-input.sf-control-wrapper { + font-size: $input-medium-font-size; } - .sf-float-input.sf-small input, - .sf-float-input.sf-control-wrapper.sf-small input { + .sf-float-input.sf-medium input, + .sf-float-input.sf-control-wrapper.sf-medium input { font: inherit; line-height: inherit; - padding: $float-input-small-padding; + padding: $float-input-medium-padding; } .sf-float-input input:focus, @@ -874,9 +696,6 @@ .sf-float-input.sf-control-wrapper textarea:focus ~ label.sf-float-text, .sf-float-input.sf-control-wrapper textarea:valid ~ label.sf-float-text.sf-label-top, .sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text.sf-label-top { - @if ($input-skin-name == 'bootstrap5.3') { - font-weight: $input-float-font-weight; - } user-select: text; } @@ -885,18 +704,18 @@ .sf-float-input.sf-control-wrapper label.sf-float-text, .sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-small:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-float-input.sf-medium:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-control-wrapper.sf-small:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-small:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-float-input.sf-medium:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { + .sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { font-weight: normal; } @@ -960,9 +779,6 @@ .sf-rtl .sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { right: 0; transform-origin: right top; - @if $input-skin-name == 'fluent2' { - padding-right: 8px; - } } .sf-float-input.sf-rtl:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, @@ -977,30 +793,30 @@ .sf-rtl .sf-float-input.sf-control-wrapper input:not(:focus):not(:valid) label.sf-float-text, .sf-float-input.sf-rtl.sf-control-wrapper input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, .sf-rtl .sf-float-input.sf-control-wrapper input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-rtl.sf-small:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-rtl.sf-control-wrapper.sf-small:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-float-input.sf-small:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input.sf-rtl:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input.sf-rtl.sf-control-wrapper:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, - .sf-small.sf-rtl .sf-float-input:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, - .sf-small.sf-rtl .sf-float-input.sf-control-wrapper:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-small.sf-rtl input:not(:focus):not(:valid) label.sf-float-text, - .sf-float-input.sf-rtl.sf-small input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-float-input.sf-small input:not(:focus):not(:valid) label.sf-float-text, - .sf-rtl .sf-float-input.sf-small input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input.sf-rtl input:not(:focus):not(:valid) label.sf-float-text, - .sf-small .sf-float-input.sf-rtl input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, - .sf-small.sf-rtl .sf-float-input input:not(:focus):not(:valid) label.sf-float-text, - .sf-small.sf-rtl .sf-float-input input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-control-wrapper.sf-small.sf-rtl input:not(:focus):not(:valid) label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-rtl.sf-small input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-float-input.sf-control-wrapper.sf-small input:not(:focus):not(:valid) label.sf-float-text, - .sf-rtl .sf-float-input.sf-control-wrapper.sf-small input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input.sf-control-wrapper.sf-rtl input:not(:focus):not(:valid) label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper.sf-rtl input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, - .sf-small.sf-rtl .sf-float-input.sf-control-wrapper input:not(:focus):not(:valid) label.sf-float-text, - .sf-small.sf-rtl .sf-float-input.sf-control-wrapper input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom { + .sf-float-input.sf-rtl.sf-medium:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, + .sf-float-input.sf-rtl.sf-control-wrapper.sf-medium:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-float-input.sf-medium:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input.sf-rtl:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input.sf-rtl.sf-control-wrapper:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, + .sf-medium.sf-rtl .sf-float-input:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, + .sf-medium.sf-rtl .sf-float-input.sf-control-wrapper:not(.sf-input-focus) label.sf-float-text.sf-label-bottom, + .sf-float-input.sf-medium.sf-rtl input:not(:focus):not(:valid) label.sf-float-text, + .sf-float-input.sf-rtl.sf-medium input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-float-input.sf-medium input:not(:focus):not(:valid) label.sf-float-text, + .sf-rtl .sf-float-input.sf-medium input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input.sf-rtl input:not(:focus):not(:valid) label.sf-float-text, + .sf-medium .sf-float-input.sf-rtl input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, + .sf-medium.sf-rtl .sf-float-input input:not(:focus):not(:valid) label.sf-float-text, + .sf-medium.sf-rtl .sf-float-input input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, + .sf-float-input.sf-control-wrapper.sf-medium.sf-rtl input:not(:focus):not(:valid) label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-rtl.sf-medium input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-float-input.sf-control-wrapper.sf-medium input:not(:focus):not(:valid) label.sf-float-text, + .sf-rtl .sf-float-input.sf-control-wrapper.sf-medium input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input.sf-control-wrapper.sf-rtl input:not(:focus):not(:valid) label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper.sf-rtl input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom, + .sf-medium.sf-rtl .sf-float-input.sf-control-wrapper input:not(:focus):not(:valid) label.sf-float-text, + .sf-medium.sf-rtl .sf-float-input.sf-control-wrapper input:not(:focus):not(:valid) label.sf-float-text.sf-label-bottom { padding-right: $float-label-padding; } @@ -1090,18 +906,7 @@ .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left .sf-float-line::after, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left .sf-float-line::before, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - @include input-group-animation; - } - } - - .sf-input-group::before, - .sf-input-group::after, - .sf-input-group.sf-control-wrapper::before, - .sf-input-group.sf-control-wrapper::after { - @if $input-skin-name != 'Material3' { - @include input-group-animation; - } + @include input-group-animation; } .sf-input-group:not(.sf-float-icon-left):not(.sf-float-input)::before, @@ -1116,16 +921,7 @@ .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left::before, .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left .sf-float-line::before, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left .sf-float-line::before { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - @include input-group-animation-left; - } - } - - .sf-input-group::before, - .sf-input-group.sf-control-wrapper::before { - @if $input-skin-name != 'Material3' { - @include input-group-animation-left; - } + @include input-group-animation-left; } .sf-input-group:not(.sf-float-icon-left):not(.sf-float-input).sf-input-focus::before, @@ -1148,18 +944,7 @@ .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left.sf-input-focus .sf-float-line::after, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left.sf-input-focus .sf-float-line::before, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left.sf-input-focus .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - @include input-group-animation-width; - } - } - - .sf-input-group.sf-input-focus::before, - .sf-input-group.sf-input-focus::after, - .sf-input-group.sf-control-wrapper.sf-input-focus::before, - .sf-input-group.sf-control-wrapper.sf-input-focus::after { - @if $input-skin-name != 'Material3' { - @include input-group-animation-width; - } + @include input-group-animation-width; } .sf-input-group:not(.sf-float-icon-left):not(.sf-float-input)::after, @@ -1174,16 +959,7 @@ .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-float-input)::after, .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left .sf-float-line::after, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - @include input-group-animation-right; - } - } - - .sf-input-group::after, - .sf-input-group.sf-control-wrapper::after { - @if $input-skin-name != 'Material3' { - @include input-group-animation-right; - } + @include input-group-animation-right; } .sf-input-group, @@ -1192,19 +968,10 @@ width: 100%; } - .sf-input-group .sf-input-group-icon:hover, - .sf-input-group.sf-rtl.sf-corner .sf-input-group-icon:hover, - .sf-input-group.sf-control-wrapper .sf-input-group-icon:hover, - .sf-input-group.sf-control-wrapper.sf-rtl.sf-corner .sf-input-group-icon:hover { - @if ( $input-skin-name != 'bootstrap5.3' and $input-skin-name != 'Material3') { - border-radius: $input-group-border-radius; - } - } - - .sf-input#{$css}.sf-small, - .sf-input-group.sf-small, - .sf-input-group.sf-control-wrapper.sf-small { - margin-bottom: $input-small-margin-bottom; + .sf-input#{$css}.sf-medium, + .sf-input-group.sf-medium, + .sf-input-group.sf-control-wrapper.sf-medium { + margin-bottom: $input-medium-margin-bottom; } .sf-input-group .sf-input-group-icon, @@ -1214,80 +981,18 @@ margin-top: $input-child-margin-top; } - .sf-float-input.sf-input-group .sf-input-group-icon, - .sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { - @if $input-skin-name != 'Material3' { - margin-top: $float-input-child-margin-top; - } - } - - .sf-input-group.sf-small .sf-input-group-icon, - .sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-small .sf-input-group .sf-input-group-icon, - .sf-small .sf-input-group.sf-control-wrapper .sf-input-group-icon { - @if ($input-skin-name != 'Material3') { - margin-bottom: $input-child-small-margin-bottom; - margin-right: $input-child-small-margin-right; - margin-top: $input-child-small-margin-top; - } - @else { - margin: $input-child-small-margin; - } - } - - .sf-float-input.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-float-input.sf-input-group .sf-input-group-icon, - .sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { - @if $input-skin-name != 'Material3' and $input-skin-name != 'fluent2' { - margin-top: $float-input-child-small-margin-top; - } - } - - .sf-input-group, - .sf-input-group.sf-control-wrapper { - @if $input-skin-name != 'Material3' { - border-bottom: $input-group-border; - } + .sf-input-group.sf-medium .sf-input-group-icon, + .sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-medium .sf-input-group .sf-input-group-icon, + .sf-medium .sf-input-group.sf-control-wrapper .sf-input-group-icon { + margin: $input-child-medium-margin; } .sf-input-group:not(.sf-float-icon-left), .sf-input-group.sf-control-wrapper:not(.sf-float-icon-left), .sf-filled.sf-input-group.sf-float-icon-left, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - border-bottom: $input-group-border; - } - } - - .sf-underline.sf-input-group:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-success:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-warning:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-error:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-control-wrapper:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-control-wrapper.sf-success:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-control-wrapper.sf-warning:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-control-wrapper.sf-error:not(.sf-float-icon-left) { - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - border: $input-group-full-border; - border-width: $input-group-full-border-width; - padding-top: 1px; - border-radius: 0; - } - } - - .sf-input-group, - .sf-input-group.sf-success, - .sf-input-group.sf-warning, - .sf-input-group.sf-error, - .sf-input-group.sf-control-wrapper, - .sf-input-group.sf-control-wrapper.sf-success, - .sf-input-group.sf-control-wrapper.sf-warning, - .sf-input-group.sf-control-wrapper.sf-error { - @if $input-skin-name != 'Material3' { - border: $input-group-full-border; - border-width: $input-group-full-border-width; - } + border-bottom: $input-group-border; } .sf-input-group.sf-rtl.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input:not(:first-child):focus, @@ -1299,54 +1004,29 @@ .sf-input-group.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error).sf-disabled .sf-input-in-wrap, .sf-input-group.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-float-icon-left).sf-disabled, .sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error).sf-disabled .sf-input-in-wrap { - @if ($input-skin-name == 'tailwind3') - { - background: $input-readonly-bg-color; - } - @if $input-skin-name != 'tailwind3' - { - background: $input-disable-bg-color; - } + background: $input-disable-bg-color; + color: $input-disable-font-color; + background-image: linear-gradient(90deg, $input-disable-border-color 0, $input-disable-border-color 33%, transparent 0); + background-position: bottom -1px left 0; + background-repeat: repeat-x; + background-size: 4px 1px; + border-bottom-color: transparent; color: $input-disable-font-color; - @if $input-skin-name == 'Material3' { - background-image: linear-gradient(90deg, $input-disable-border-color 0, $input-disable-border-color 33%, transparent 0); - background-position: bottom -1px left 0; - background-repeat: repeat-x; - background-size: 4px 1px; - border-bottom-color: transparent; - color: $input-disable-font-color; - } - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - border-color: $input-disable-border-color; - } - } - - .sf-input-group:not(.sf-success):not(.sf-warning):not(.sf-error).sf-disabled, - .sf-input-group.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error).sf-disabled { - @if $input-skin-name != 'Material3' { - border-style: $input-disable-border-type; - } } .sf-input-group .sf-input-group-icon, .sf-input-group.sf-control-wrapper .sf-input-group-icon { - @if $input-skin-name != 'fluent2' { - @include input-ripple-parent; - } + @include input-ripple-parent; } .sf-input-group:not(.sf-filled) .sf-input-group-icon::after, .sf-input-group.sf-control-wrapper:not(.sf-filled) .sf-input-group-icon::after { - @if $input-skin-name != 'fluent2' { - @include input-ripple-style; - } + @include input-ripple-style; } .sf-input-group .sf-input-group-icon.sf-input-btn-ripple::after, .sf-input-group.sf-control-wrapper .sf-input-group-icon.sf-input-btn-ripple::after { - @if $input-skin-name != 'fluent2' { - @include input-ripple-animation; - } + @include input-ripple-animation; } input.sf-input#{$css}::-ms-clear, @@ -1388,14 +1068,8 @@ .sf-float-input.sf-input-group.sf-control-wrapper .sf-float-line, .sf-float-input.sf-control-wrapper.sf-input-group .sf-float-line, .sf-float-input.sf-control-wrapper.sf-input-group.sf-control-wrapper .sf-float-line { - @if $input-skin-name == 'fluent2' { - bottom: -.1px; - position: absolute; - } - @else { - bottom: -1px; - position: absolute; - } + bottom: -1px; + position: absolute; } .sf-float-input.sf-input-group input, @@ -1429,31 +1103,31 @@ font-style: $input-font-style; } - .sf-small input.sf-input#{$css}::-webkit-input-placeholder, - input.sf-small.sf-input#{$css}::-webkit-input-placeholder, - .sf-small input.sf-input#{$css}:-moz-placeholder, - input.sf-small.sf-input#{$css}:-moz-placeholder, - .sf-small input.sf-input#{$css}:-ms-input-placeholder, - input.sf-small.sf-input#{$css}:-ms-input-placeholder, - .sf-small input.sf-input#{$css}::-moz-placeholder, - input.sf-small.sf-input#{$css}::-moz-placeholder, - .sf-small textarea.sf-input#{$css}::-webkit-input-placeholder, - textarea.sf-small.sf-input#{$css}::-webkit-input-placeholder, - .sf-small textarea.sf-input#{$css}:-moz-placeholder, - textarea.sf-small.sf-input#{$css}:-moz-placeholder, - .sf-small textarea.sf-input#{$css}:-ms-input-placeholder, - textarea.sf-small.sf-input#{$css}:-ms-input-placeholder, - .sf-small textarea.sf-input#{$css}::-moz-placeholder, - textarea.sf-small.sf-input#{$css}::-moz-placeholder, - .sf-small textarea.sf-input#{$css}::-webkit-textarea-placeholder, - textarea.sf-small.sf-input#{$css}::-webkit-textarea-placeholder, - .sf-small textarea.sf-input#{$css}:-moz-placeholder, - textarea.sf-small.sf-input#{$css}:-moz-placeholder, - .sf-small textarea.sf-input#{$css}:-ms-input-placeholder, - textarea.sf-small.sf-input#{$css}:-ms-input-placeholder, - .sf-small textarea.sf-input#{$css}::-moz-placeholder, - textarea.sf-small.sf-input#{$css}::-moz-placeholder { - font-size: $input-small-font-size; + .sf-medium input.sf-input#{$css}::-webkit-input-placeholder, + input.sf-medium.sf-input#{$css}::-webkit-input-placeholder, + .sf-medium input.sf-input#{$css}:-moz-placeholder, + input.sf-medium.sf-input#{$css}:-moz-placeholder, + .sf-medium input.sf-input#{$css}:-ms-input-placeholder, + input.sf-medium.sf-input#{$css}:-ms-input-placeholder, + .sf-medium input.sf-input#{$css}::-moz-placeholder, + input.sf-medium.sf-input#{$css}::-moz-placeholder, + .sf-medium textarea.sf-input#{$css}::-webkit-input-placeholder, + textarea.sf-medium.sf-input#{$css}::-webkit-input-placeholder, + .sf-medium textarea.sf-input#{$css}:-moz-placeholder, + textarea.sf-medium.sf-input#{$css}:-moz-placeholder, + .sf-medium textarea.sf-input#{$css}:-ms-input-placeholder, + textarea.sf-medium.sf-input#{$css}:-ms-input-placeholder, + .sf-medium textarea.sf-input#{$css}::-moz-placeholder, + textarea.sf-medium.sf-input#{$css}::-moz-placeholder, + .sf-medium textarea.sf-input#{$css}::-webkit-textarea-placeholder, + textarea.sf-medium.sf-input#{$css}::-webkit-textarea-placeholder, + .sf-medium textarea.sf-input#{$css}:-moz-placeholder, + textarea.sf-medium.sf-input#{$css}:-moz-placeholder, + .sf-medium textarea.sf-input#{$css}:-ms-input-placeholder, + textarea.sf-medium.sf-input#{$css}:-ms-input-placeholder, + .sf-medium textarea.sf-input#{$css}::-moz-placeholder, + textarea.sf-medium.sf-input#{$css}::-moz-placeholder { + font-size: $input-medium-font-size; font-style: $input-font-style; } @@ -1508,41 +1182,39 @@ .sf-control.sf-input-group.sf-control-wrapper input.sf-input, .sf-control.sf-float-input input, .sf-control.sf-float-input.sf-control-wrapper input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - @include input-sizing; - } + @include input-sizing; } - input.sf-input.sf-small#{$css}, - .sf-input-group.sf-small input, - .sf-input-group.sf-small input.sf-input, - .sf-small .sf-input-group input, - .sf-small .sf-input-group input.sf-input, - .sf-input-group.sf-control-wrapper.sf-small input, - .sf-input-group.sf-control-wrapper.sf-small input.sf-input, - .sf-small .sf-input-group.sf-control-wrapper input, - .sf-small .sf-input-group.sf-control-wrapper input.sf-input, - .sf-float-input.sf-small input, - .sf-float-input.sf-small input.sf-input, - .sf-small .sf-float-input input, - .sf-small .sf-float-input input.sf-input, - .sf-float-input.sf-control-wrapper.sf-small input, - .sf-float-input.sf-control-wrapper.sf-small input.sf-input, - .sf-small .sf-float-input.sf-control-wrapper input, - .sf-small .sf-float-input.sf-control-wrapper input.sf-input { + input.sf-input.sf-medium#{$css}, + .sf-input-group.sf-medium input, + .sf-input-group.sf-medium input.sf-input, + .sf-medium .sf-input-group input, + .sf-medium .sf-input-group input.sf-input, + .sf-input-group.sf-control-wrapper.sf-medium input, + .sf-input-group.sf-control-wrapper.sf-medium input.sf-input, + .sf-medium .sf-input-group.sf-control-wrapper input, + .sf-medium .sf-input-group.sf-control-wrapper input.sf-input, + .sf-float-input.sf-medium input, + .sf-float-input.sf-medium input.sf-input, + .sf-medium .sf-float-input input, + .sf-medium .sf-float-input input.sf-input, + .sf-float-input.sf-control-wrapper.sf-medium input, + .sf-float-input.sf-control-wrapper.sf-medium input.sf-input, + .sf-medium .sf-float-input.sf-control-wrapper input, + .sf-medium .sf-float-input.sf-control-wrapper input.sf-input { @include input-sizing; - @include input-height ($input-small-height); + @include input-height ($input-medium-height); } - .sf-float-input.sf-small:not(.sf-input-group) input, - .sf-float-input.sf-small:not(.sf-input-group) input.sf-input, - .sf-small .sf-float-input:not(.sf-input-group) input, - .sf-small .sf-float-input:not(.sf-input-group) input.sf-input .sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-group) input, - .sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-group) input.sf-input, - .sf-small .sf-float-input.sf-control-wrapper:not(.sf-input-group) input, - .sf-small .sf-float-input.sf-control-wrapper:not(.sf-input-group) input.sf-input { + .sf-float-input.sf-medium:not(.sf-input-group) input, + .sf-float-input.sf-medium:not(.sf-input-group) input.sf-input, + .sf-medium .sf-float-input:not(.sf-input-group) input, + .sf-medium .sf-float-input:not(.sf-input-group) input.sf-input .sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-group) input, + .sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-group) input.sf-input, + .sf-medium .sf-float-input.sf-control-wrapper:not(.sf-input-group) input, + .sf-medium .sf-float-input.sf-control-wrapper:not(.sf-input-group) input.sf-input { @include input-sizing; - @include input-height ($float-input-small-height); + @include input-height ($float-input-medium-height); } textarea.sf-input#{$css}, @@ -1554,52 +1226,52 @@ @include input-height ($textarea-normal-height); } - textarea.sf-input.sf-small#{$css}, - .sf-input-group.sf-small textarea, - .sf-input-group.sf-small textarea.sf-input, - .sf-small .sf-input-group textarea, - .sf-small .sf-input-group textarea.sf-input, - .sf-input-group.sf-control-wrapper.sf-small textarea, - .sf-input-group.sf-control-wrapper.sf-small textarea.sf-input, - .sf-small .sf-input-group.sf-control-wrapper textarea, - .sf-small .sf-input-group.sf-control-wrapper textarea.sf-input, - .sf-float-input.sf-small textarea, - .sf-float-input.sf-small textarea.sf-input, - .sf-small .sf-float-input textarea, - .sf-small .sf-float-input textarea.sf-input, - .sf-float-input.sf-control-wrapper.sf-small textarea, - .sf-float-input.sf-control-wrapper.sf-small textarea.sf-input, - .sf-small .sf-float-input.sf-control-wrapper textarea, - .sf-small .sf-float-input.sf-control-wrapper textarea.sf-input { + textarea.sf-input.sf-medium#{$css}, + .sf-input-group.sf-medium textarea, + .sf-input-group.sf-medium textarea.sf-input, + .sf-medium .sf-input-group textarea, + .sf-medium .sf-input-group textarea.sf-input, + .sf-input-group.sf-control-wrapper.sf-medium textarea, + .sf-input-group.sf-control-wrapper.sf-medium textarea.sf-input, + .sf-medium .sf-input-group.sf-control-wrapper textarea, + .sf-medium .sf-input-group.sf-control-wrapper textarea.sf-input, + .sf-float-input.sf-medium textarea, + .sf-float-input.sf-medium textarea.sf-input, + .sf-medium .sf-float-input textarea, + .sf-medium .sf-float-input textarea.sf-input, + .sf-float-input.sf-control-wrapper.sf-medium textarea, + .sf-float-input.sf-control-wrapper.sf-medium textarea.sf-input, + .sf-medium .sf-float-input.sf-control-wrapper textarea, + .sf-medium .sf-float-input.sf-control-wrapper textarea.sf-input { @include input-sizing; - @include input-height ($textarea-small-height); - } - - input.sf-input#{$css}.sf-small, - .sf-input-group input.sf-input.sf-small, - .sf-input-group.sf-control-wrapper input.sf-input.sf-small, - .sf-input-group.sf-small .sf-input, - .sf-input-group.sf-control-wrapper.sf-small .sf-input, - .sf-small input.sf-input#{$css}, - .sf-small .sf-input-group .sf-input, - .sf-small .sf-input-group.sf-control-wrapper .sf-input, - .sf-float-input.sf-small input, - .sf-float-input input.sf-small, - .sf-small .sf-float-input input, - .sf-float-input.sf-control-wrapper.sf-small input, - .sf-float-input.sf-control-wrapper input.sf-small, - .sf-small .sf-float-input.sf-control-wrapper input, - textarea.sf-input#{$css}.sf-small, - .sf-input-group textarea.sf-input.sf-small, - .sf-input-group.sf-control-wrapper input.sf-input-group textarea.sf-input.sf-small, - .sf-small input.sf-input#{$css}, - .sf-float-input.sf-small textarea, - .sf-float-input textarea.sf-small, - .sf-small .sf-float-input textarea, - .sf-float-input.sf-control-wrapper.sf-small textarea, - .sf-float-input.sf-control-wrapper textarea.sf-small, - .sf-small .sf-float-input.sf-control-wrapper textarea { - text-indent: $input-small-text-indent; + @include input-height ($textarea-medium-height); + } + + input.sf-input#{$css}.sf-medium, + .sf-input-group input.sf-input.sf-medium, + .sf-input-group.sf-control-wrapper input.sf-input.sf-medium, + .sf-input-group.sf-medium .sf-input, + .sf-input-group.sf-control-wrapper.sf-medium .sf-input, + .sf-medium input.sf-input#{$css}, + .sf-medium .sf-input-group .sf-input, + .sf-medium .sf-input-group.sf-control-wrapper .sf-input, + .sf-float-input.sf-medium input, + .sf-float-input input.sf-medium, + .sf-medium .sf-float-input input, + .sf-float-input.sf-control-wrapper.sf-medium input, + .sf-float-input.sf-control-wrapper input.sf-medium, + .sf-medium .sf-float-input.sf-control-wrapper input, + textarea.sf-input#{$css}.sf-medium, + .sf-input-group textarea.sf-input.sf-medium, + .sf-input-group.sf-control-wrapper input.sf-input-group textarea.sf-input.sf-medium, + .sf-medium input.sf-input#{$css}, + .sf-float-input.sf-medium textarea, + .sf-float-input textarea.sf-medium, + .sf-medium .sf-float-input textarea, + .sf-float-input.sf-control-wrapper.sf-medium textarea, + .sf-float-input.sf-control-wrapper textarea.sf-medium, + .sf-medium .sf-float-input.sf-control-wrapper textarea { + text-indent: $input-medium-text-indent; } input.sf-input#{$css}, @@ -1636,12 +1308,7 @@ .sf-input-group.sf-control-wrapper textarea.sf-input:focus, .sf-float-input.sf-control-wrapper textarea:focus, .sf-float-input textarea:focus { - @if $input-skin-name == 'fluent2' { - padding-left: 12px; - } - @else { - padding-left: $input-text-indent; - } + padding-left: $input-text-indent; text-indent: 0; } @@ -1713,148 +1380,148 @@ text-indent: 0; } - input.sf-input.sf-small#{$css}, - .sf-small input.sf-input#{$css}, - .sf-input-group.sf-small input.sf-input, - .sf-input-group.sf-control-wrapper.sf-small input.sf-input, - .sf-float-input.sf-small input, - .sf-float-input.sf-control-wrapper input.sf-small, - .sf-float-input.sf-small input, - .sf-float-input.sf-control-wrapper input.sf-small, - .sf-input-group input.sf-input.sf-small, - .sf-input-group.sf-control-wrapper input.sf-input.sf-small, - .sf-small .sf-float-input input, - .sf-small .sf-float-input.sf-control-wrapper input, - .sf-small .sf-input-group input.sf-input, - .sf-small .sf-input-group.sf-control-wrapper input.sf-input, - .sf-input-group.sf-small input.sf-input:focus, - .sf-input-group.sf-control-wrapper.sf-small input.sf-input:focus, - .sf-float-input.sf-small input:focus, - .sf-float-input.sf-control-wrapper.sf-small input:focus, - .sf-small .sf-input-group.sf-control-wrapper input.sf-input:focus, - .sf-small .sf-input-group input.sf-input:focus, - .sf-small .sf-float-input input:focus, - .sf-small .sf-float-input.sf-control-wrapper input:focus, - .sf-input-group.sf-small.sf-input-focus input.sf-input, - .sf-input-group.sf-control-wrapper.sf-small.sf-input-focus input.sf-input, - .sf-small .sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input, - .sf-small .sf-input-group.sf-input-focus input.sf-input, - .sf-float-input.sf-small.sf-input-focus input, - .sf-float-input.sf-control-wrapper.sf-input-focus.sf-small input, - .sf-small .sf-float-input.sf-input-focus input, - .sf-small .sf-float-input.sf-control-wrapper.sf-input-focus input, - textarea.sf-input.sf-small#{$css}, - .sf-small textarea.sf-input#{$css}, - .sf-input-group.sf-small textarea.sf-input, - .sf-input-group.sf-control-wrapper.sf-small textarea.sf-input, - .sf-float-input.sf-control-wrapper.sf-small textarea, - .sf-float-input.sf-control-wrapper textarea.sf-small, - .sf-float-input.sf-small textarea, - .sf-float-input textarea.sf-small, - .sf-input-group textarea.sf-input.sf-small, - .sf-input-group.sf-control-wrapper textarea.sf-input.sf-small, - .sf-small .sf-float-input.sf-control-wrapper textarea, - .sf-small .sf-float-input textarea, - .sf-small .sf-input-group textarea.sf-input, - .sf-small .sf-input-group.sf-control-wrapper textarea.sf-input, - .sf-input-group.sf-small textarea.sf-input:focus, - .sf-input-group.sf-control-wrapper.sf-small textarea.sf-input:focus, - .sf-float-input.sf-small textarea:focus, - .sf-float-input.sf-control-wrapper.sf-small textarea:focus, - .sf-small .sf-input-group textarea.sf-input:focus, - .sf-small .sf-input-group.sf-control-wrapper textarea.sf-input:focus, - .sf-small .sf-float-input.sf-control-wrapper textarea:focus, - .sf-small .sf-float-input textarea:focus { - padding-left: $input-small-text-indent; + input.sf-input.sf-medium#{$css}, + .sf-medium input.sf-input#{$css}, + .sf-input-group.sf-medium input.sf-input, + .sf-input-group.sf-control-wrapper.sf-medium input.sf-input, + .sf-float-input.sf-medium input, + .sf-float-input.sf-control-wrapper input.sf-medium, + .sf-float-input.sf-medium input, + .sf-float-input.sf-control-wrapper input.sf-medium, + .sf-input-group input.sf-input.sf-medium, + .sf-input-group.sf-control-wrapper input.sf-input.sf-medium, + .sf-medium .sf-float-input input, + .sf-medium .sf-float-input.sf-control-wrapper input, + .sf-medium .sf-input-group input.sf-input, + .sf-medium .sf-input-group.sf-control-wrapper input.sf-input, + .sf-input-group.sf-medium input.sf-input:focus, + .sf-input-group.sf-control-wrapper.sf-medium input.sf-input:focus, + .sf-float-input.sf-medium input:focus, + .sf-float-input.sf-control-wrapper.sf-medium input:focus, + .sf-medium .sf-input-group.sf-control-wrapper input.sf-input:focus, + .sf-medium .sf-input-group input.sf-input:focus, + .sf-medium .sf-float-input input:focus, + .sf-medium .sf-float-input.sf-control-wrapper input:focus, + .sf-input-group.sf-medium.sf-input-focus input.sf-input, + .sf-input-group.sf-control-wrapper.sf-medium.sf-input-focus input.sf-input, + .sf-medium .sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input, + .sf-medium .sf-input-group.sf-input-focus input.sf-input, + .sf-float-input.sf-medium.sf-input-focus input, + .sf-float-input.sf-control-wrapper.sf-input-focus.sf-medium input, + .sf-medium .sf-float-input.sf-input-focus input, + .sf-medium .sf-float-input.sf-control-wrapper.sf-input-focus input, + textarea.sf-input.sf-medium#{$css}, + .sf-medium textarea.sf-input#{$css}, + .sf-input-group.sf-medium textarea.sf-input, + .sf-input-group.sf-control-wrapper.sf-medium textarea.sf-input, + .sf-float-input.sf-control-wrapper.sf-medium textarea, + .sf-float-input.sf-control-wrapper textarea.sf-medium, + .sf-float-input.sf-medium textarea, + .sf-float-input textarea.sf-medium, + .sf-input-group textarea.sf-input.sf-medium, + .sf-input-group.sf-control-wrapper textarea.sf-input.sf-medium, + .sf-medium .sf-float-input.sf-control-wrapper textarea, + .sf-medium .sf-float-input textarea, + .sf-medium .sf-input-group textarea.sf-input, + .sf-medium .sf-input-group.sf-control-wrapper textarea.sf-input, + .sf-input-group.sf-medium textarea.sf-input:focus, + .sf-input-group.sf-control-wrapper.sf-medium textarea.sf-input:focus, + .sf-float-input.sf-medium textarea:focus, + .sf-float-input.sf-control-wrapper.sf-medium textarea:focus, + .sf-medium .sf-input-group textarea.sf-input:focus, + .sf-medium .sf-input-group.sf-control-wrapper textarea.sf-input:focus, + .sf-medium .sf-float-input.sf-control-wrapper textarea:focus, + .sf-medium .sf-float-input textarea:focus { + padding-left: $input-medium-text-indent; text-indent: 0; } - .sf-rtl input.sf-input.sf-small#{$css}, - input.sf-input#{$css}.sf-small.sf-rtl, - .sf-small.sf-rtl input.sf-input#{$css}, - .sf-small input.sf-input.sf-rtl#{$css}, - .sf-float-input.sf-control-wrapper.sf-small.sf-rtl input, - .sf-float-input.sf-small.sf-rtl input, - .sf-input-group.sf-small.sf-rtl input.sf-input, - .sf-input-group.sf-control-wrapper.sf-small.sf-rtl input.sf-input, - .sf-rtl .sf-float-input.sf-small input, - .sf-rtl .sf-float-input.sf-control-wrapper.sf-small input, - .sf-rtl .sf-input-group.sf-small input.sf-input, - .sf-rtl .sf-input-group.sf-control-wrapper.sf-small input.sf-input, - .sf-float-input.sf-rtl input.sf-small, - .sf-float-input.sf-control-wrapper.sf-rtl input.sf-small, - .sf-input-group.sf-rtl input.sf-input.sf-small, - .sf-input-group.sf-control-wrapper.sf-rtl input.sf-input.sf-small, - .sf-rtl .sf-float-input input.sf-small, - .sf-rtl .sf-float-input.sf-control-wrapper input.sf-small, - .sf-rtl .sf-input-group input.sf-input.sf-small, - .sf-rtl .sf-input-group.sf-control-wrapper input.sf-input.sf-small, - .sf-small .sf-float-input.sf-rtl input, - .sf-small .sf-float-input.sf-control-wrapper.sf-rtl input, - .sf-small .sf-input-group.sf-rtl input.sf-input, - .sf-small .sf-input-group.sf-control-wrapper.sf-rtl input.sf-input, - .sf-small.sf-rtl .sf-float-input.sf-control-wrapper input, - .sf-small.sf-rtl .sf-float-input input, - .sf-small.sf-rtl .sf-input-group.sf-control-wrapper input.sf-input, - .sf-small.sf-rtl .sf-input-group input.sf-input, - .sf-small.sf-rtl .sf-input-group.sf-control-wrapper input.sf-input:focus, - .sf-small.sf-rtl .sf-input-group input.sf-input:focus, - .sf-small.sf-rtl .sf-float-input.sf-control-wrapper input:focus, - .sf-small.sf-rtl .sf-float-input input:focus, - .sf-small .sf-input-group.sf-control-wrapper.sf-rtl input.sf-input:focus, - .sf-small .sf-input-group.sf-rtl input.sf-input:focus, - .sf-small .sf-float-input.sf-control-wrapper.sf-rtl input:focus, - .sf-small .sf-float-input.sf-rtl input:focus, - .sf-small.sf-rtl .sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input, - .sf-small.sf-rtl .sf-input-group.sf-input-focus input.sf-input, - .sf-small .sf-input-group.sf-control-wrapper.sf-rtl.sf-input-focus input.sf-input, - .sf-small .sf-input-group.sf-rtl.sf-input-focus input.sf-input, - .sf-small.sf-rtl .sf-float-input.sf-control-wrapper.sf-input-focus input, - .sf-small.sf-rtl .sf-float-input.sf-input-focus input, - .sf-small .sf-float-input.sf-control-wrapper.sf-rtl.sf-input-focus input, - .sf-small .sf-float-input.sf-rtl.sf-input-focus input { + .sf-rtl input.sf-input.sf-medium#{$css}, + input.sf-input#{$css}.sf-medium.sf-rtl, + .sf-medium.sf-rtl input.sf-input#{$css}, + .sf-medium input.sf-input.sf-rtl#{$css}, + .sf-float-input.sf-control-wrapper.sf-medium.sf-rtl input, + .sf-float-input.sf-medium.sf-rtl input, + .sf-input-group.sf-medium.sf-rtl input.sf-input, + .sf-input-group.sf-control-wrapper.sf-medium.sf-rtl input.sf-input, + .sf-rtl .sf-float-input.sf-medium input, + .sf-rtl .sf-float-input.sf-control-wrapper.sf-medium input, + .sf-rtl .sf-input-group.sf-medium input.sf-input, + .sf-rtl .sf-input-group.sf-control-wrapper.sf-medium input.sf-input, + .sf-float-input.sf-rtl input.sf-medium, + .sf-float-input.sf-control-wrapper.sf-rtl input.sf-medium, + .sf-input-group.sf-rtl input.sf-input.sf-medium, + .sf-input-group.sf-control-wrapper.sf-rtl input.sf-input.sf-medium, + .sf-rtl .sf-float-input input.sf-medium, + .sf-rtl .sf-float-input.sf-control-wrapper input.sf-medium, + .sf-rtl .sf-input-group input.sf-input.sf-medium, + .sf-rtl .sf-input-group.sf-control-wrapper input.sf-input.sf-medium, + .sf-medium .sf-float-input.sf-rtl input, + .sf-medium .sf-float-input.sf-control-wrapper.sf-rtl input, + .sf-medium .sf-input-group.sf-rtl input.sf-input, + .sf-medium .sf-input-group.sf-control-wrapper.sf-rtl input.sf-input, + .sf-medium.sf-rtl .sf-float-input.sf-control-wrapper input, + .sf-medium.sf-rtl .sf-float-input input, + .sf-medium.sf-rtl .sf-input-group.sf-control-wrapper input.sf-input, + .sf-medium.sf-rtl .sf-input-group input.sf-input, + .sf-medium.sf-rtl .sf-input-group.sf-control-wrapper input.sf-input:focus, + .sf-medium.sf-rtl .sf-input-group input.sf-input:focus, + .sf-medium.sf-rtl .sf-float-input.sf-control-wrapper input:focus, + .sf-medium.sf-rtl .sf-float-input input:focus, + .sf-medium .sf-input-group.sf-control-wrapper.sf-rtl input.sf-input:focus, + .sf-medium .sf-input-group.sf-rtl input.sf-input:focus, + .sf-medium .sf-float-input.sf-control-wrapper.sf-rtl input:focus, + .sf-medium .sf-float-input.sf-rtl input:focus, + .sf-medium.sf-rtl .sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input, + .sf-medium.sf-rtl .sf-input-group.sf-input-focus input.sf-input, + .sf-medium .sf-input-group.sf-control-wrapper.sf-rtl.sf-input-focus input.sf-input, + .sf-medium .sf-input-group.sf-rtl.sf-input-focus input.sf-input, + .sf-medium.sf-rtl .sf-float-input.sf-control-wrapper.sf-input-focus input, + .sf-medium.sf-rtl .sf-float-input.sf-input-focus input, + .sf-medium .sf-float-input.sf-control-wrapper.sf-rtl.sf-input-focus input, + .sf-medium .sf-float-input.sf-rtl.sf-input-focus input { padding-left: 0; - padding-right: $input-small-text-indent; + padding-right: $input-medium-text-indent; text-indent: 0; } - .sf-rtl textarea.sf-input.sf-small#{$css}, - textarea.sf-input.sf-small.sf-rtl#{$css}, - .sf-small.sf-rtl textarea.sf-input#{$css}, - .sf-small textarea.sf-input.sf-rtl#{$css}, - .sf-float-input:not(.sf-outline).sf-small.sf-rtl textarea, - .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-small.sf-rtl textarea, - .sf-input-group:not(.sf-outline).sf-small.sf-rtl textarea.sf-input, - .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-small.sf-rtl textarea.sf-input, - .sf-rtl .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-small textarea, - .sf-rtl .sf-float-input:not(.sf-outline).sf-small textarea, - .sf-rtl .sf-input-group:not(.sf-outline).sf-small textarea.sf-input, - .sf-rtl .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-small textarea.sf-input, - .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-rtl textarea.sf-small, - .sf-float-input:not(.sf-outline).sf-rtl textarea.sf-small, - .sf-input-group:not(.sf-outline).sf-rtl textarea.sf-input.sf-small, - .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-rtl textarea.sf-input.sf-small, - .sf-rtl .sf-float-input:not(.sf-outline).sf-control-wrapper textarea.sf-small, - .sf-rtl .sf-float-input:not(.sf-outline) textarea.sf-small, - .sf-rtl .sf-input-group:not(.sf-outline) textarea.sf-input.sf-small, - .sf-rtl .sf-input-group:not(.sf-outline).sf-control-wrapper textarea.sf-input.sf-small, - .sf-small .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-rtl textarea, - .sf-small .sf-float-input:not(.sf-outline).sf-rtl textarea, - .sf-small .sf-input-group:not(.sf-outline).sf-rtl textarea.sf-input, - .sf-small .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-rtl textarea.sf-input, - .sf-small.sf-rtl .sf-float-input:not(.sf-outline).sf-control-wrapper textarea, - .sf-small.sf-rtl .sf-float-input:not(.sf-outline) textarea, - .sf-small.sf-rtl .sf-input-group:not(.sf-outline) textarea.sf-input, - .sf-small.sf-rtl .sf-input-group:not(.sf-outline).sf-control-wrapper textarea.sf-input, - .sf-small.sf-rtl .sf-input-group:not(.sf-outline) textarea.sf-input:focus, - .sf-small.sf-rtl .sf-input-group:not(.sf-outline).sf-control-wrapper textarea.sf-input:focus, - .sf-small.sf-rtl .sf-float-input:not(.sf-outline).sf-control-wrapper textarea:focus, - .sf-small.sf-rtl .sf-float-input:not(.sf-outline) textarea:focus, - .sf-small .sf-input-group:not(.sf-outline).sf-rtl textarea.sf-input:focus, - .sf-small .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-rtl textarea.sf-input:focus, - .sf-small .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-rtl textarea:focus, - .sf-small .sf-float-input:not(.sf-outline).sf-rtl textarea:focus { - padding-right: $input-small-text-indent; + .sf-rtl textarea.sf-input.sf-medium#{$css}, + textarea.sf-input.sf-medium.sf-rtl#{$css}, + .sf-medium.sf-rtl textarea.sf-input#{$css}, + .sf-medium textarea.sf-input.sf-rtl#{$css}, + .sf-float-input:not(.sf-outline).sf-medium.sf-rtl textarea, + .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-medium.sf-rtl textarea, + .sf-input-group:not(.sf-outline).sf-medium.sf-rtl textarea.sf-input, + .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-medium.sf-rtl textarea.sf-input, + .sf-rtl .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-medium textarea, + .sf-rtl .sf-float-input:not(.sf-outline).sf-medium textarea, + .sf-rtl .sf-input-group:not(.sf-outline).sf-medium textarea.sf-input, + .sf-rtl .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-medium textarea.sf-input, + .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-rtl textarea.sf-medium, + .sf-float-input:not(.sf-outline).sf-rtl textarea.sf-medium, + .sf-input-group:not(.sf-outline).sf-rtl textarea.sf-input.sf-medium, + .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-rtl textarea.sf-input.sf-medium, + .sf-rtl .sf-float-input:not(.sf-outline).sf-control-wrapper textarea.sf-medium, + .sf-rtl .sf-float-input:not(.sf-outline) textarea.sf-medium, + .sf-rtl .sf-input-group:not(.sf-outline) textarea.sf-input.sf-medium, + .sf-rtl .sf-input-group:not(.sf-outline).sf-control-wrapper textarea.sf-input.sf-medium, + .sf-medium .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-rtl textarea, + .sf-medium .sf-float-input:not(.sf-outline).sf-rtl textarea, + .sf-medium .sf-input-group:not(.sf-outline).sf-rtl textarea.sf-input, + .sf-medium .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-rtl textarea.sf-input, + .sf-medium.sf-rtl .sf-float-input:not(.sf-outline).sf-control-wrapper textarea, + .sf-medium.sf-rtl .sf-float-input:not(.sf-outline) textarea, + .sf-medium.sf-rtl .sf-input-group:not(.sf-outline) textarea.sf-input, + .sf-medium.sf-rtl .sf-input-group:not(.sf-outline).sf-control-wrapper textarea.sf-input, + .sf-medium.sf-rtl .sf-input-group:not(.sf-outline) textarea.sf-input:focus, + .sf-medium.sf-rtl .sf-input-group:not(.sf-outline).sf-control-wrapper textarea.sf-input:focus, + .sf-medium.sf-rtl .sf-float-input:not(.sf-outline).sf-control-wrapper textarea:focus, + .sf-medium.sf-rtl .sf-float-input:not(.sf-outline) textarea:focus, + .sf-medium .sf-input-group:not(.sf-outline).sf-rtl textarea.sf-input:focus, + .sf-medium .sf-input-group:not(.sf-outline).sf-control-wrapper.sf-rtl textarea.sf-input:focus, + .sf-medium .sf-float-input:not(.sf-outline).sf-control-wrapper.sf-rtl textarea:focus, + .sf-medium .sf-float-input:not(.sf-outline).sf-rtl textarea:focus { + padding-right: $input-medium-text-indent; text-indent: 0; } @@ -1903,83 +1570,41 @@ .sf-input-group.sf-control-wrapper .sf-clear-icon { min-height: $input-clear-icon-min-height; min-width: $input-clear-icon-min-width; - @if $input-skin-name != 'Material3' and $input-skin-name != 'fluent2' { - padding-bottom: $input-clear-icon-padding-bottom; - padding-left: $input-clear-icon-padding-left; - padding-right: $input-clear-icon-padding-right; - padding-top: $input-clear-icon-padding-top; - } - @else { - padding: $input-clear-icon-padding; - margin: $input-clear-icon-margin; - border-radius: $input-clear-icon-hover-border-radius; - } + padding: $input-clear-icon-padding; + margin: $input-clear-icon-margin; + border-radius: $input-clear-icon-hover-border-radius; } - .sf-float-input.sf-input-group .sf-clear-icon, - .sf-float-input.sf-input-group.sf-control-wrapper .sf-clear-icon { - @if $input-skin-name != 'Material3' and $input-skin-name != 'fluent2' and $input-skin-name != 'tailwind3' { - padding-right: $input-clear-icon-padding-right; - padding-top: $float-input-clear-icon-padding-top; - } - } + .sf-input-group.sf-medium .sf-clear-icon, + .sf-input-group .sf-clear-icon.sf-medium, + .sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon, + .sf-input-group.sf-control-wrapper .sf-clear-icon.sf-medium { + min-height: $input-bigger-medium-clear-icon-min-height; + min-width: $input-bigger-medium-clear-icon-min-width; + padding: $input-clear-icon-padding; + margin: $input-clear-icon-margin; + border-radius: $input-bigger-clear-icon-border-radius; - .sf-input-group.sf-small .sf-clear-icon, - .sf-input-group .sf-clear-icon.sf-small, - .sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, - .sf-input-group.sf-control-wrapper .sf-clear-icon.sf-small { - @if $input-skin-name != 'Material3' and $input-skin-name != 'fluent2' { - min-height: $input-bigger-clear-icon-min-height; - min-width: $input-bigger-clear-icon-min-width; - padding-bottom: $input-clear-icon-bigger-padding-bottom; - padding-right: $input-clear-icon-bigger-padding-right; - padding-top: $input-clear-icon-bigger-padding-top; - } - @else { - min-height: $input-bigger-small-clear-icon-min-height; - min-width: $input-bigger-small-clear-icon-min-width; - padding: $input-clear-icon-padding; - margin: $input-clear-icon-margin; - border-radius: $input-bigger-clear-icon-border-radius; - } + svg { + font-size: $outline-input-font-size; + } } - .sf-input-group.sf-small .sf-clear-icon, - .sf-input-group .sf-clear-icon.sf-small, - .sf-small .sf-input-group .sf-clear-icon, - .sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, - .sf-input-group.sf-control-wrapper .sf-clear-icon.sf-small, - .sf-small .sf-input-group.sf-control-wrapper .sf-clear-icon { - min-height: $input-small-clear-icon-min-height; - min-width: $input-small-clear-icon-min-width; - @if $input-skin-name != 'Material3' and $input-skin-name != 'fluent2' { - padding-bottom: $input-clear-icon-small-padding-bottom; - padding-right: $input-clear-icon-small-padding-right; - padding-top: $input-clear-icon-small-padding-top; - } - @else { + .sf-input-group.sf-medium.sf-outline .sf-clear-icon, + .sf-input-group.sf-outline .sf-clear-icon.sf-medium, + .sf-medium .sf-input-group.sf-outline .sf-clear-icon, + .sf-input-group.sf-control-wrapper.sf-medium.sf-outline .sf-clear-icon, + .sf-input-group.sf-control-wrapper.sf-outline .sf-clear-icon.sf-medium, + .sf-medium .sf-input-group.sf-control-wrapper.sf-outline .sf-clear-icon { + min-height: $input-medium-clear-icon-min-height; + min-width: $input-medium-clear-icon-min-width; padding: $input-clear-icon-padding; margin: $input-clear-icon-margin; - border-radius: $input-small-clear-icon-border-radius; - } - } + border-radius: $input-medium-clear-icon-border-radius; - .sf-input-group.sf-float-input.sf-small .sf-clear-icon, - .sf-input-group.sf-float-input .sf-clear-icon.sf-small, - .sf-small .sf-input-group.sf-float-input .sf-clear-icon, - .sf-input-group.sf-control-wrapper.sf-float-input.sf-small .sf-clear-icon, - .sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon.sf-small, - .sf-small .sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon, - .sf-input-group.sf-float-input.sf-control-wrapper.sf-small .sf-clear-icon, - .sf-input-group.sf-float-input.sf-control-wrapper .sf-clear-icon.sf-small, - .sf-small .sf-input-group.sf-float-input.sf-control-wrapper .sf-clear-icon, - .sf-input-group.sf-control-wrapper.sf-float-input.sf-small .sf-clear-icon, - .sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon.sf-small, - .sf-small .sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon { - @if $input-skin-name != 'Material3' and $input-skin-name != 'fluent2' and $input-skin-name != 'tailwind3' { - padding-right: $input-clear-icon-small-padding-right; - padding-top: $float-input-clear-icon-small-padding-top; - } + svg { + font-size: $input-medium-left-icon-font-size; + } } .sf-input#{$css}:not(:valid), @@ -2008,47 +1633,29 @@ .sf-float-input .sf-input-in-wrap, .sf-float-input.sf-control-wrapper .sf-input-in-wrap { width: 100%; - @if $input-skin-name == 'bootstrap5.3' $input-skin-name == 'tailwind3' { - display: flex; - position: relative; - } } .sf-float-input .sf-input-in-wrap label.sf-float-text, .sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text { right: 0; - @if $input-skin-name == 'bootstrap5.3' or $input-skin-name == 'tailwind3' { - overflow: hidden; - text-overflow: ellipsis; - vertical-align: middle; - white-space: nowrap; - } } .sf-input-group .sf-input-in-wrap, .sf-input-group.sf-control-wrapper .sf-input-in-wrap, .sf-float-input .sf-input-in-wrap, .sf-float-input.sf-control-wrapper .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - display: flex; - position: relative; - width: 100%; - } + display: flex; + position: relative; + width: 100%; } .sf-float-input.sf-float-icon-left .sf-input-in-wrap, .sf-float-input.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, .sf-input-group.sf-float-icon-left .sf-input-in-wrap, .sf-input-group.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap { - @if $input-skin-name == 'Material3' { - border: $input-group-full-border; - border-width: $input-group-full-border-width; - margin-left: $input-inner-wrap-margin-left; - } - @if $input-skin-name == 'fluent2' { - border-width: $input-group-full-border-width; - margin-left: $input-inner-wrap-margin-left; - } + border: $input-group-full-border; + border-width: $input-group-full-border-width; + margin-left: $input-inner-wrap-margin-left; } .sf-rtl .sf-float-input.sf-float-icon-left .sf-input-in-wrap, @@ -2058,23 +1665,15 @@ .sf-float-input.sf-float-icon-left.sf-rtl .sf-input-in-wrap, .sf-input-group.sf-float-icon-left.sf-rtl .sf-input-in-wrap, .sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - margin-left: $zero-value; - margin-right: $input-inner-wrap-margin-left; - } + margin-left: $zero-value; + margin-right: $input-inner-wrap-margin-left; } .sf-float-input label.sf-float-text.sf-label-bottom, .sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' { - transform: translate3d(0, 16px, 0) scale(1); - } - @if $input-skin-name == 'fluent2' { - transform: translate3d(0, 16px, 0) scale(1); - padding-left: 7px; - } + transform: translate3d(0, 16px, 0) scale(1); } .sf-float-input textarea:focus ~ label.sf-float-text, @@ -2090,100 +1689,64 @@ .sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper textarea label.sf-float-text.sf-label-top { font-size: $float-label-font-size; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - top: $textarea-float-top; - transform: translate3d(0, 6px, 0) scale(.92); - @if $input-skin-name == 'tailwind3' { - left: 2px; - } - } - @else if ($input-skin-name == 'bootstrap5.3') { - font-weight: 500; - padding-right: 0; - transform: translate3d(-10px, -36px, 0) scale(1); - } + top: $textarea-float-top; + transform: translate3d(0, 6px, 0) scale(.92); } - .sf-float-input.sf-small textarea:focus ~ label.sf-float-text, - .sf-float-input.sf-small textarea:valid ~ label.sf-float-text, - .sf-float-input.sf-small textarea ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input textarea ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small textarea:focus ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small textarea:valid ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small textarea ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper textarea ~ label.sf-label-top.sf-float-text { - font-size: $float-label-small-font-size; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -21px; - } - @if $input-skin-name == 'tailwind3' { - top: -21px; - left: 2px; - } - @else if $input-skin-name == 'bootstrap5.3' or $input-skin-name == 'tailwind3' { - top: 16px; - } + .sf-float-input.sf-medium textarea:focus ~ label.sf-float-text, + .sf-float-input.sf-medium textarea:valid ~ label.sf-float-text, + .sf-float-input.sf-medium textarea ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input textarea ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium textarea:focus ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium textarea:valid ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium textarea ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper textarea ~ label.sf-label-top.sf-float-text { + font-size: $float-label-medium-font-size; + top: -21px; } .sf-float-input textarea ~ .sf-float-text, .sf-float-input.sf-control-wrapper textarea ~ .sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -11px; - } - @else if $input-skin-name == 'bootstrap5.3' { - top: 15px; - } - @else if $input-skin-name == 'tailwind3' { - top: 13px; - } - } - - .sf-float-input.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-float-input.sf-control-wrapper.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - font-size: $float-placeholder-small-font-size; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -11px; - } - } - - .sf-float-input.sf-small textarea ~ label.sf-float-text, - .sf-float-input textarea ~ label.sf-float-text.sf-small, - .sf-float-input textarea.sf-small ~ label.sf-float-text, - .sf-small .sf-float-input textarea ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small textarea ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text.sf-small, - .sf-float-input.sf-control-wrapper textarea.sf-small ~ label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text { - font-size: $float-placeholder-small-font-size; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -11px; - } - @else if $input-skin-name == 'bootstrap5.3' or $input-skin-name == 'tailwind3' { - top: 15px; - } + top: -11px; } - .sf-input-group.sf-small:not(.sf-float-input) .sf-input, - .sf-small .sf-input-group:not(.sf-float-input) .sf-input, - .sf-input-group.sf-control-wrapper.sf-small:not(.sf-float-input) .sf-input, - .sf-small .sf-input-group.sf-control-wrapper:not(.sf-float-input) .sf-input, - .sf-float-input.sf-small input, - .sf-small .sf-float-input input, - .sf-float-input.sf-input-group.sf-small input, - .sf-small .sf-float-input.sf-input-group input, - .sf-float-input.sf-input-group.sf-control-wrapper.sf-small input, - .sf-small .sf-float-input.sf-input-group.sf-control-wrapper input, - .sf-float-input.sf-control-wrapper.sf-small input, - .sf-small .sf-float-input.sf-control-wrapper input, - .sf-float-input.sf-control-wrapper.sf-input-group.sf-small input, - .sf-small .sf-float-input.sf-control-wrapper.sf-input-group input, - .sf-float-input.sf-control-wrapper.sf-input-group.sf-small input, - .sf-small .sf-float-input.sf-control-wrapper.sf-input-group input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - min-height: 18px; - } + .sf-float-input.sf-medium textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-float-input.sf-control-wrapper.sf-medium textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { + font-size: $float-placeholder-medium-font-size; + top: -6px; + } + + .sf-float-input.sf-medium textarea ~ label.sf-float-text, + .sf-float-input textarea ~ label.sf-float-text.sf-medium, + .sf-float-input textarea.sf-medium ~ label.sf-float-text, + .sf-medium .sf-float-input textarea ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium textarea ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text.sf-medium, + .sf-float-input.sf-control-wrapper textarea.sf-medium ~ label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text { + font-size: $float-placeholder-medium-font-size; + top: -6px; + } + + .sf-input-group.sf-medium:not(.sf-float-input) .sf-input, + .sf-medium .sf-input-group:not(.sf-float-input) .sf-input, + .sf-input-group.sf-control-wrapper.sf-medium:not(.sf-float-input) .sf-input, + .sf-medium .sf-input-group.sf-control-wrapper:not(.sf-float-input) .sf-input, + .sf-float-input.sf-medium input, + .sf-medium .sf-float-input input, + .sf-float-input.sf-input-group.sf-medium input, + .sf-medium .sf-float-input.sf-input-group input, + .sf-float-input.sf-input-group.sf-control-wrapper.sf-medium input, + .sf-medium .sf-float-input.sf-input-group.sf-control-wrapper input, + .sf-float-input.sf-control-wrapper.sf-medium input, + .sf-medium .sf-float-input.sf-control-wrapper input, + .sf-float-input.sf-control-wrapper.sf-input-group.sf-medium input, + .sf-medium .sf-float-input.sf-control-wrapper.sf-input-group input, + .sf-float-input.sf-control-wrapper.sf-input-group.sf-medium input, + .sf-medium .sf-float-input.sf-control-wrapper.sf-input-group input { + min-height: 18px; } .sf-input-group input.sf-input, @@ -2192,9 +1755,7 @@ .sf-float-input.sf-input-group.sf-control-wrapper input, .sf-float-input input, .sf-float-input.sf-control-wrapper input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - min-height: 22px; - } + min-height: 22px; } .sf-input-group:hover:not(.sf-disabled):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-float-icon-left), @@ -2209,39 +1770,14 @@ .sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-disabled):not(.sf-success):not(.sf-warning):not(.sf-error) textarea:not([disabled]), .sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-disabled):not(.sf-success):not(.sf-warning):not(.sf-error) input:not([disabled]), .sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-disabled):not(.sf-success):not(.sf-warning):not(.sf-error) textarea:not([disabled]) { - @if $input-skin-name == 'Material3' { - border-bottom-width: $input-group-border-width-hover; - } - @else if $input-skin-name == 'fluent2' { - border-bottom-width: $input-group-border-width-hover; - border-color: $input-group-border-color-hover; - } - } - - .sf-input-group:hover:not(.sf-disabled), - .sf-input-group.sf-control-wrapper:hover:not(.sf-disabled), - .sf-float-input:hover:not(.sf-disabled), - .sf-float-input:hover:not(.sf-input-group):not(.sf-disabled) input:not([disabled]), - .sf-float-input:hover:not(.sf-input-group):not(.sf-disabled) textarea:not([disabled]), - .sf-float-input:hover:not(.sf-input-group):not(.sf-disabled) input:not([disabled]), - .sf-float-input:hover:not(.sf-input-group):not(.sf-disabled) textarea:not([disabled]), - .sf-float-input.sf-control-wrapper:hover:not(.sf-disabled), - .sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-disabled) input:not([disabled]), - .sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-disabled) textarea:not([disabled]), - .sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-disabled) input:not([disabled]), - .sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-disabled) textarea:not([disabled]) { - @if $input-skin-name != 'Material3' { - border-bottom-width: $input-group-border-width-hover; - } + border-bottom-width: $input-group-border-width-hover; } .sf-input-group.sf-float-icon-left:not(.sf-disabled):not(.sf-input-focus) .sf-input-in-wrap:hover, .sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-disabled):not(.sf-input-focus) .sf-input-in-wrap:hover, .sf-float-input.sf-float-icon-left:not(.sf-disabled):not(.sf-input-focus) .sf-input-in-wrap:hover, .sf-float-input.sf-control-wrapper.sf-float-icon-left:not(.sf-disabled):not(.sf-input-focus) .sf-input-in-wrap:hover { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - border-bottom-width: $input-group-border-width-hover; - } + border-bottom-width: $input-group-border-width-hover; } .sf-input-group:not(.sf-disabled):not(.sf-float-icon-left)::before, @@ -2252,55 +1788,33 @@ .sf-input-group.sf-control-wrapper:not(.sf-disabled):not(.sf-float-icon-left)::after, .sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-disabled) .sf-input-in-wrap::before, .sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-disabled) .sf-input-in-wrap::after { - @if $input-skin-name == 'Material3' { - bottom: -2px; - } - @else if $input-skin-name == 'fluent2' { - bottom: -.3px; - } + bottom: -2px; } .sf-float-input:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus):not(.sf-outline) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { font-size: $float-label-font-size; - @if $input-skin-name == 'Material3' { - top: -9px; - transform: translate3d(0, -6px, 0) scale(.92); - } - @else if $input-skin-name == 'bootstrap5.3' { - font-weight: bold; - padding-right: 0; - transform: translate3d(-10px, -39px, 0) scale(1); - } + top: -9px; + transform: translate3d(0, -6px, 0) scale(.92); user-select: text; } - .sf-small .sf-float-input:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, - .sf-small.sf-float-input:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, - .sf-small.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus):not(.sf-outline) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, - .sf-small.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus):not(.sf-outline) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { - font-size: $float-label-small-font-size; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -8px; - transform: translate3d(0, -6px, 0) scale(.92); - } - @else if $input-skin-name == 'bootstrap5.3' { - font-weight: bold; - padding-right: 0; - top: 17px; - transform: translate3d(-10px, -39px, 0) scale(1); - } + .sf-medium .sf-float-input:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, + .sf-medium.sf-float-input:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, + .sf-medium.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-outline) input:-webkit-autofill ~ label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus):not(.sf-outline) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, + .sf-medium.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus):not(.sf-outline) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { + font-size: $float-label-medium-font-size; + top: -8px; + transform: translate3d(0, -6px, 0) scale(.92); user-select: text; } .sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - transform: translate3d(0, 16px, 0) scale(1); - } + transform: translate3d(0, 16px, 0) scale(1); } .sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, @@ -2317,7 +1831,7 @@ top: -11px; transform: translate3d(0, 16px, 0) scale(1); transform-origin: left top; - transition: .25s cubic-bezier(.25, .8, .25, 1); + transition: $transition-duration-standard $transition-standard-curve; user-select: none; white-space: nowrap; width: 100%; @@ -2327,74 +1841,48 @@ .sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { font-size: $float-label-font-size; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -9px; - transform: translate3d(0, -6px, 0) scale(.92); - } - @else if ($input-skin-name == 'tailwind3' or $input-skin-name == 'bootstrap5.3') { - font-weight: 500; - padding-right: 0; - transform: translate3d(-10px, -40px, 0) scale(1); - } + top: -9px; + transform: translate3d(0, -6px, 0) scale(.92); user-select: text; } - .sf-small .sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, - .sf-small.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, - .sf-small.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, - .sf-small.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { - font-size: $float-label-small-font-size; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - top: -8px; - transform: translate3d(0, -6px, 0) scale(.92); - } + .sf-medium .sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, + .sf-medium.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, + .sf-medium.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, + .sf-medium.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { + font-size: $float-label-medium-font-size; + top: -8px; + transform: translate3d(0, -6px, 0) scale(.92); user-select: text; } - .sf-float-input.sf-small textarea:focus ~ label.sf-float-text, - .sf-float-input.sf-small textarea:valid ~ label.sf-float-text, - .sf-float-input.sf-small textarea ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small textarea[readonly] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small textarea[disabled] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-small textarea label.sf-float-text.sf-label-top, - .sf-small .sf-float-input textarea:focus ~ label.sf-float-text, - .sf-small .sf-float-input textarea:valid ~ label.sf-float-text, - .sf-small .sf-float-input textarea ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input textarea[readonly] ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input textarea[disabled] ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input textarea label.sf-float-text.sf-label-top, - .sf-float-input.sf-control-wrapper.sf-small textarea:focus ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small textarea:valid ~ label.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small textarea ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small textarea[readonly] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small textarea[disabled] ~ label.sf-label-top.sf-float-text, - .sf-float-input.sf-control-wrapper.sf-small textarea label.sf-float-text.sf-label-top, - .sf-small .sf-float-input.sf-control-wrapper textarea:focus ~ label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper textarea:valid ~ label.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper textarea ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper textarea[readonly] ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-label-top.sf-float-text, - .sf-small .sf-float-input.sf-control-wrapper textarea label.sf-float-text.sf-label-top { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - transform: translate3d(0, 6px, 0) scale(.86); - } - @else if ( $input-skin-name == 'bootstrap5.3') { - font-weight: 500; - padding-right: 0; - transform: translate3d(-10px, -35px, 0) scale(1); - } - } - - .sf-float-input textarea[disabled], - .sf-float-input textarea.sf-disabled, - .sf-float-input.sf-control-wrapper textarea[disabled], - .sf-float-input.sf-control-wrapper textarea.sf-disabled { - @if $input-skin-name != 'Material3' { - border-color: $input-disable-border-color; - border-style: $input-disable-border-type; - } + .sf-float-input.sf-medium textarea:focus ~ label.sf-float-text, + .sf-float-input.sf-medium textarea:valid ~ label.sf-float-text, + .sf-float-input.sf-medium textarea ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium textarea[readonly] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium textarea[disabled] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-medium textarea label.sf-float-text.sf-label-top, + .sf-medium .sf-float-input textarea:focus ~ label.sf-float-text, + .sf-medium .sf-float-input textarea:valid ~ label.sf-float-text, + .sf-medium .sf-float-input textarea ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input textarea[readonly] ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input textarea[disabled] ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input textarea label.sf-float-text.sf-label-top, + .sf-float-input.sf-control-wrapper.sf-medium textarea:focus ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium textarea:valid ~ label.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium textarea ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium textarea[readonly] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium textarea[disabled] ~ label.sf-label-top.sf-float-text, + .sf-float-input.sf-control-wrapper.sf-medium textarea label.sf-float-text.sf-label-top, + .sf-medium .sf-float-input.sf-control-wrapper textarea:focus ~ label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper textarea:valid ~ label.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper textarea ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper textarea[readonly] ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-float-input.sf-control-wrapper textarea label.sf-float-text.sf-label-top { + transform: translate3d(0, 6px, 0) scale(1); } .sf-float-input textarea[disabled], @@ -2423,91 +1911,66 @@ padding: $textarea-padding; } - .sf-input-group.sf-small textarea, - .sf-input-group.sf-small textarea.sf-input, - .sf-input-group textarea.sf-small, - .sf-input-group textarea.sf-input.sf-small, - .sf-input-group.sf-control-wrapper.sf-small textarea, - .sf-input-group.sf-control-wrapper.sf-small textarea.sf-input, - .sf-small .sf-input-group textarea, - .sf-small .sf-input-group textarea.sf-input, - .sf-float-input.sf-small textarea, - .sf-float-input textarea.sf-small, - .sf-float-input.sf-control-wrapper.sf-small textarea, - .sf-float-input.sf-control-wrapper textarea.sf-small, - .sf-small .sf-float-input textarea, - .sf-small .sf-float-input.sf-control-wrapper textarea { + .sf-input-group.sf-medium textarea, + .sf-input-group.sf-medium textarea.sf-input, + .sf-input-group textarea.sf-medium, + .sf-input-group textarea.sf-input.sf-medium, + .sf-input-group.sf-control-wrapper.sf-medium textarea, + .sf-input-group.sf-control-wrapper.sf-medium textarea.sf-input, + .sf-medium .sf-input-group textarea, + .sf-medium .sf-input-group textarea.sf-input, + .sf-float-input.sf-medium textarea, + .sf-float-input textarea.sf-medium, + .sf-float-input.sf-control-wrapper.sf-medium textarea, + .sf-float-input.sf-control-wrapper textarea.sf-medium, + .sf-medium .sf-float-input textarea, + .sf-medium .sf-float-input.sf-control-wrapper textarea { font: inherit; - min-height: $textarea-small-min-height; - padding: $textarea-small-padding; - } - - .sf-input-group.sf-input-focus.sf-small textarea, - .sf-input-group.sf-input-focus.sf-small textarea.sf-input, - .sf-input-group.sf-input-focus textarea.sf-small, - .sf-input-group.sf-input-focus textarea.sf-input.sf-small, - .sf-input-group.sf-input-focus textarea.sf-input.sf-small, - .sf-input-group.sf-control-wrapper.sf-input-focus.sf-small textarea, - .sf-input-group.sf-control-wrapper.sf-input-focus.sf-small textarea.sf-input, - .sf-small .sf-input-group.sf-input-focus textarea, - .sf-small .sf-input-group.sf-input-focus textarea.sf-input { + min-height: $textarea-medium-min-height; + padding: $textarea-medium-padding; + } + + .sf-input-group.sf-input-focus.sf-medium textarea, + .sf-input-group.sf-input-focus.sf-medium textarea.sf-input, + .sf-input-group.sf-input-focus textarea.sf-medium, + .sf-input-group.sf-input-focus textarea.sf-input.sf-medium, + .sf-input-group.sf-input-focus textarea.sf-input.sf-medium, + .sf-input-group.sf-control-wrapper.sf-input-focus.sf-medium textarea, + .sf-input-group.sf-control-wrapper.sf-input-focus.sf-medium textarea.sf-input, + .sf-medium .sf-input-group.sf-input-focus textarea, + .sf-medium .sf-input-group.sf-input-focus textarea.sf-input { font: inherit; - min-height: $textarea-small-min-height; - padding: $textarea-small-padding; - } - - .sf-input-group.sf-small textarea:focus, - .sf-input-group.sf-small textarea.sf-input:focus, - .sf-input-group textarea.sf-small:focus, - .sf-input-group textarea.sf-input.sf-small:focus, - .sf-input-group.sf-control-wrapper.sf-small textarea:focus, - .sf-input-group.sf-control-wrapper.sf-small textarea.sf-input:focus, - .sf-small .sf-input-group textarea:focus, - .sf-small .sf-input-group textarea.sf-input:focus, - .sf-float-input.sf-small textarea:focus, - .sf-float-input textarea.sf-small:focus, - .sf-float-input.sf-control-wrapper.sf-small textarea:focus, - .sf-float-input.sf-control-wrapper textarea.sf-small:focus, - .sf-small .sf-float-input textarea:focus, - .sf-small .sf-float-input.sf-control-wrapper textarea:focus { - padding: $textarea-small-padding; - } - - input.sf-input.sf-small#{$css}, - textarea.sf-input.sf-small#{$css}, - .sf-small input.sf-input#{$css}, - .sf-small textarea.sf-input #{$css} { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - min-height: $input-smaller-min-height; - } + min-height: $textarea-medium-min-height; + padding: $textarea-medium-padding; + } + + .sf-input-group.sf-medium textarea:focus, + .sf-input-group.sf-medium textarea.sf-input:focus, + .sf-input-group textarea.sf-medium:focus, + .sf-input-group textarea.sf-input.sf-medium:focus, + .sf-input-group.sf-control-wrapper.sf-medium textarea:focus, + .sf-input-group.sf-control-wrapper.sf-medium textarea.sf-input:focus, + .sf-medium .sf-input-group textarea:focus, + .sf-medium .sf-input-group textarea.sf-input:focus, + .sf-float-input.sf-medium textarea:focus, + .sf-float-input textarea.sf-medium:focus, + .sf-float-input.sf-control-wrapper.sf-medium textarea:focus, + .sf-float-input.sf-control-wrapper textarea.sf-medium:focus, + .sf-medium .sf-float-input textarea:focus, + .sf-medium .sf-float-input.sf-control-wrapper textarea:focus { + padding: $textarea-medium-padding; + } + + input.sf-input.sf-medium#{$css}, + textarea.sf-input.sf-medium#{$css}, + .sf-medium input.sf-input#{$css}, + .sf-medium textarea.sf-input #{$css} { + min-height: $input-smaller-min-height; } input.sf-input#{$css}, textarea.sf-input#{$css} { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { - min-height: $input-min-height; - } - } - - .sf-underline.sf-input-group.sf-control-wrapper, - .sf-underline.sf-input-group, - .sf-underline.sf-input-group:not(.sf-float-icon-left), - .sf-underline.sf-float-input, - .sf-underline.sf-float-input.sf-control-wrapper, - .sf-underline.sf-input-group:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-success:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-warning:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-error:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-control-wrapper:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-control-wrapper.sf-success:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-control-wrapper.sf-warning:not(.sf-float-icon-left), - .sf-underline.sf-input-group.sf-control-wrapper.sf-error:not(.sf-float-icon-left) { - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - border: 1px solid $underline-border-color; - border-width: $input-group-full-border-width; - padding-top: 1px; - border-radius: 0; - } + min-height: $input-min-height; } textarea.sf-outline.sf-input, @@ -2521,42 +1984,38 @@ .sf-outline.sf-float-input.sf-control-wrapper textarea, .sf-outline.sf-input-group:not(.sf-float-icon-left) textarea.sf-input:focus, .sf-outline.sf-input-group.sf-control-wrapper:not(.sf-float-icon-left) textarea.sf-input:focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'tailwind3' { - box-sizing: border-box; - margin: $outline-textarea-margin-top; - padding: $zero-value $outline-input-padding-left $outline-input-padding-left; - } - } - - .sf-outline.sf-float-input.sf-small input:focus ~ label.sf-float-text, - .sf-outline.sf-float-input.sf-small input:valid ~ label.sf-float-text, - .sf-outline.sf-float-input.sf-small input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-outline.sf-float-input input ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-small input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-small input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small input:focus ~ label.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small input:valid ~ label.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-outline.sf-float-input.sf-control-wrapper input ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-small textarea:focus ~ label.sf-float-text, - .sf-outline.sf-float-input.sf-small textarea:valid ~ label.sf-float-text, - .sf-outline.sf-float-input.sf-small textarea ~ label.sf-label-top.sf-float-text, - .sf-small .sf-outline.sf-float-input textarea ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-small textarea[readonly] ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-small textarea[disabled] ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea:focus ~ label.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea:valid ~ label.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea ~ label.sf-label-top.sf-float-text, - .sf-small .sf-outline.sf-float-input.sf-control-wrapper textarea ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea[readonly] ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea[disabled] ~ label.sf-label-top.sf-float-text, - .sf-outline.sf-float-input.sf-small.sf-input-focus input ~ label.sf-float-text, - .sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-input-focus input ~ label.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'tailwind3' { - font-size: $outline-small-label-font-size; - } + box-sizing: border-box; + margin: $outline-textarea-margin-top; + padding: $zero-value $outline-input-padding-left $outline-input-padding-left; + } + + .sf-outline.sf-float-input.sf-medium input:focus ~ label.sf-float-text, + .sf-outline.sf-float-input.sf-medium input:valid ~ label.sf-float-text, + .sf-outline.sf-float-input.sf-medium input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-outline.sf-float-input input ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-medium input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-medium input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium input:focus ~ label.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium input:valid ~ label.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-outline.sf-float-input.sf-control-wrapper input ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-medium textarea:focus ~ label.sf-float-text, + .sf-outline.sf-float-input.sf-medium textarea:valid ~ label.sf-float-text, + .sf-outline.sf-float-input.sf-medium textarea ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-outline.sf-float-input textarea ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-medium textarea[readonly] ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-medium textarea[disabled] ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea:focus ~ label.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea:valid ~ label.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-outline.sf-float-input.sf-control-wrapper textarea ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea[readonly] ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea[disabled] ~ label.sf-label-top.sf-float-text, + .sf-outline.sf-float-input.sf-medium.sf-input-focus input ~ label.sf-float-text, + .sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-input-focus input ~ label.sf-float-text { + font-size: $outline-medium-label-font-size; } .sf-outline.sf-float-input textarea:focus ~ label.sf-float-text, @@ -2571,13 +2030,8 @@ .sf-outline.sf-float-input.sf-control-wrapper textarea[readonly] ~ label.sf-label-top.sf-float-text, .sf-outline.sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-outline.sf-float-input.sf-control-wrapper textarea label.sf-float-text.sf-label-top { - @if $input-skin-name == 'Material3' or $input-skin-name == 'tailwind3' { - top: $outline-float-label-top; - transform: translate3d($zero-value, $zero-value, $zero-value) scale(1); - } - @if $input-skin-name == 'tailwind3' { - left: 2px; - } + top: $outline-float-label-top; + transform: translate3d($zero-value, $zero-value, $zero-value) scale(1); } .sf-filled.sf-input-group, @@ -2607,65 +2061,61 @@ .sf-control.sf-filled.sf-input-group.sf-control-wrapper input.sf-input, .sf-control.sf-filled.sf-float-input input, .sf-control.sf-filled.sf-float-input.sf-control-wrapper input, - .sf-filled input.sf-input.sf-small#{$css}, - .sf-filled.sf-input-group.sf-small input, - .sf-filled.sf-input-group.sf-small input.sf-input, - .sf-small .sf-filled.sf-input-group input, - .sf-small .sf-filled.sf-input-group input.sf-input, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small input, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small input.sf-input, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper input, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper input.sf-input, - .sf-filled.sf-float-input.sf-small input, - .sf-filled.sf-float-input.sf-small input.sf-input, - .sf-small .sf-filled.sf-float-input input, - .sf-small .sf-filled.sf-float-input input.sf-input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small input.sf-input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper input.sf-input, - .sf-filled.sf-float-input.sf-small:not(.sf-input-group) input, - .sf-filled.sf-float-input.sf-small:not(.sf-input-group) input.sf-input, - .sf-small .sf-filled.sf-float-input:not(.sf-input-group) input, - .sf-small .sf-filled.sf-float-input:not(.sf-input-group) input.sf-input .sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-group) input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-group) input.sf-input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-group) input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-group) input.sf-input, + .sf-filled input.sf-input.sf-medium#{$css}, + .sf-filled.sf-input-group.sf-medium input, + .sf-filled.sf-input-group.sf-medium input.sf-input, + .sf-medium .sf-filled.sf-input-group input, + .sf-medium .sf-filled.sf-input-group input.sf-input, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium input, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium input.sf-input, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper input, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper input.sf-input, + .sf-filled.sf-float-input.sf-medium input, + .sf-filled.sf-float-input.sf-medium input.sf-input, + .sf-medium .sf-filled.sf-float-input input, + .sf-medium .sf-filled.sf-float-input input.sf-input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input.sf-input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper input.sf-input, + .sf-filled.sf-float-input.sf-medium:not(.sf-input-group) input, + .sf-filled.sf-float-input.sf-medium:not(.sf-input-group) input.sf-input, + .sf-medium .sf-filled.sf-float-input:not(.sf-input-group) input, + .sf-medium .sf-filled.sf-float-input:not(.sf-input-group) input.sf-input .sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-group) input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-group) input.sf-input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-group) input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-group) input.sf-input, .sf-filled textarea.sf-input#{$css}, .sf-filled.sf-input-group textarea, .sf-filled.sf-input-group.sf-control-wrapper textarea, .sf-filled.sf-float-input textarea, .sf-filled.sf-float-input.sf-control-wrapper textarea, - .sf-filled textarea.sf-input.sf-small#{$css}, - .sf-filled.sf-input-group.sf-small textarea, - .sf-filled.sf-input-group.sf-small textarea.sf-input, - .sf-small .sf-filled.sf-input-group textarea, - .sf-small .sf-filled.sf-input-group textarea.sf-input, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small textarea, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small textarea.sf-input, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper textarea, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper textarea.sf-input, - .sf-filled.sf-float-input.sf-small textarea, - .sf-filled.sf-float-input.sf-small textarea.sf-input, - .sf-small .sf-filled.sf-float-input textarea, - .sf-small .sf-filled.sf-float-input textarea.sf-input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small textarea, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small textarea.sf-input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper textarea, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper textarea.sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled textarea.sf-input.sf-medium#{$css}, + .sf-filled.sf-input-group.sf-medium textarea, + .sf-filled.sf-input-group.sf-medium textarea.sf-input, + .sf-medium .sf-filled.sf-input-group textarea, + .sf-medium .sf-filled.sf-input-group textarea.sf-input, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium textarea, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium textarea.sf-input, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper textarea, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper textarea.sf-input, + .sf-filled.sf-float-input.sf-medium textarea, + .sf-filled.sf-float-input.sf-medium textarea.sf-input, + .sf-medium .sf-filled.sf-float-input textarea, + .sf-medium .sf-filled.sf-float-input textarea.sf-input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium textarea, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium textarea.sf-input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper textarea, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper textarea.sf-input { box-sizing: border-box; - } } .sf-filled.sf-float-input input, .sf-filled.sf-float-input textarea, .sf-filled.sf-float-input.sf-control-wrapper input, .sf-filled.sf-float-input.sf-control-wrapper textarea { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border: $float-input-border; border-width: $zero-value; - } } .sf-filled.sf-float-input:hover:not(.sf-input-group):not(.sf-disabled) input:not([disabled]), @@ -2680,9 +2130,7 @@ .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-disabled):not(.sf-input-focus) .sf-input-in-wrap:hover, .sf-filled.sf-float-input.sf-float-icon-left:not(.sf-disabled):not(.sf-input-focus) .sf-input-in-wrap:hover, .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left:not(.sf-disabled):not(.sf-input-focus) .sf-input-in-wrap:hover { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border-bottom-width: $zero-value; - } } .sf-filled.sf-float-input.sf-control-wrapper.sf-input-focus:not(.sf-input-group):not(.sf-float-icon-left):not(.sf-disabled):not(.sf-success):not(.sf-warning):not(.sf-error) input, @@ -2709,86 +2157,69 @@ .sf-filled.sf-float-input.sf-control-wrapper.sf-input-focus:not(.sf-input-group):not(.sf-float-icon-left).sf-success:not(.sf-warning):not(.sf-error) input, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-focus:not(.sf-input-group):not(.sf-float-icon-left):not(.sf-success).sf-warning:not(.sf-error) input, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-focus:not(.sf-input-group):not(.sf-float-icon-left):not(.sf-success):not(.sf-warning).sf-error input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border-width: $zero-value; - } } .sf-filled.sf-input-group, .sf-filled.sf-input-group.sf-control-wrapper { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border-radius: 4px 4px $zero-value $zero-value; padding: $filled-wrapper-padding; - } } - .sf-filled.sf-input-group.sf-small, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-input-group.sf-medium, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper { margin-top: $filled-wrapper-margin; - padding: $small-filled-wrapper-padding; - } + padding: $medium-filled-wrapper-padding; + line-height: 20px; } .sf-filled.sf-float-input, .sf-filled.sf-float-input.sf-control-wrapper { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border: $input-group-full-border; border-radius: 4px 4px $zero-value $zero-value; border-width: $input-group-full-border-width; margin-top: $filled-wrapper-margin; padding: $filled-float-input-wrapper-padding; - } } - .sf-filled.sf-float-input.sf-small, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-float-input.sf-medium, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper { margin-top: $filled-wrapper-margin; - padding: $small-filled-float-input-wrapper-padding; - } + padding: $medium-filled-float-input-wrapper-padding; } .sf-rtl.sf-filled.sf-input-group, .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper, .sf-rtl .sf-filled.sf-input-group, .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-wrapper-rtl-padding; - } } - .sf-rtl.sf-filled.sf-input-group.sf-small, - .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-small, - .sf-small .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper, - .sf-rtl .sf-filled.sf-input-group.sf-small, - .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-small, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $small-filled-wrapper-rtl-padding; - } + .sf-rtl.sf-filled.sf-input-group.sf-medium, + .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-medium, + .sf-medium .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper, + .sf-rtl .sf-filled.sf-input-group.sf-medium, + .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-medium, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper { + padding: $medium-filled-wrapper-rtl-padding; } .sf-rtl.sf-filled.sf-float-input, .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper, .sf-rtl .sf-filled.sf-float-input, .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-float-input-wrapper-rtl-padding; - } } - .sf-rtl.sf-filled.sf-float-input.sf-small, - .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-small, - .sf-small .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper, - .sf-rtl .sf-filled.sf-float-input.sf-small, - .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-small, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $small-filled-float-input-wrapper-rtl-padding; - } + .sf-rtl.sf-filled.sf-float-input.sf-medium, + .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-medium, + .sf-medium .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper, + .sf-rtl .sf-filled.sf-float-input.sf-medium, + .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-medium, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper { + padding: $medium-filled-float-input-wrapper-rtl-padding; } .sf-filled input.sf-input#{$css}, @@ -2803,47 +2234,35 @@ .sf-filled.sf-input-group.sf-control-wrapper textarea.sf-input:focus, .sf-filled.sf-input-group.sf-input-focus input.sf-input, .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-input-padding; - } } .sf-filled .sf-input#{$css}:focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding-bottom: $filled-input-padding-bottom; - } } - .sf-filled .sf-input.sf-small#{$css}:focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding-bottom: $filled-small-input-padding-bottom; - } + .sf-filled .sf-input.sf-medium#{$css}:focus { + padding-bottom: $filled-medium-input-padding-bottom; } - .sf-filled .sf-input#{$css}.sf-small, - .sf-filled.sf-input-group.sf-small .sf-input, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input, - .sf-filled.sf-input-group.sf-small .sf-input:focus, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input:focus, - .sf-filled.sf-input-group.sf-small.sf-input-focus .sf-input, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small.sf-input-focus .sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $filled-small-input-padding; - } + .sf-filled .sf-input#{$css}.sf-medium, + .sf-filled.sf-input-group.sf-medium .sf-input, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input, + .sf-filled.sf-input-group.sf-medium .sf-input:focus, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input:focus, + .sf-filled.sf-input-group.sf-medium.sf-input-focus .sf-input, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium.sf-input-focus .sf-input { + padding: $filled-medium-input-padding; } .sf-filled.sf-float-input input, .sf-filled.sf-float-input.sf-control-wrapper input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-float-input-padding; - } } - .sf-filled.sf-float-input.sf-small input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $filled-small-float-input-padding; - } + .sf-filled.sf-float-input.sf-medium input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input { + padding: $filled-medium-float-input-padding; } .sf-filled input.sf-input.sf-rtl#{$css}, @@ -2863,10 +2282,8 @@ .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl.sf-input-focus input.sf-input, .sf-rtl .sf-filled.sf-input-group.sf-input-focus input.sf-input, .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-input-rtl-padding; text-indent: 0; - } } .sf-filled.sf-float-input.sf-rtl input, @@ -2885,66 +2302,60 @@ .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl.sf-input-focus input, .sf-rtl .sf-filled.sf-float-input.sf-input-focus input, .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-focus input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-float-input-rtl-padding; text-indent: 0; - } } - .sf-rtl .sf-filled input.sf-input.sf-small#{$css}, - .sf-filled input.sf-input#{$css}.sf-small.sf-rtl, - .sf-small.sf-rtl .sf-filled input.sf-input#{$css}, - .sf-small .sf-filled input.sf-input.sf-rtl#{$css}, - .sf-filled.sf-input-group.sf-small.sf-rtl input.sf-input, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small.sf-rtl input.sf-input, - .sf-rtl .sf-filled.sf-input-group.sf-small input.sf-input, - .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-small input.sf-input, - .sf-filled.sf-input-group.sf-rtl input.sf-input.sf-small, - .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl input.sf-input.sf-small, - .sf-rtl .sf-filled.sf-input-group input.sf-input.sf-small, - .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper input.sf-input.sf-small, - .sf-small .sf-filled.sf-input-group.sf-rtl input.sf-input, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl input.sf-input, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper input.sf-input, - .sf-small.sf-rtl .sf-filled.sf-input-group input.sf-input, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper input.sf-input:focus, - .sf-small.sf-rtl .sf-filled.sf-input-group input.sf-input:focus, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl input.sf-input:focus, - .sf-small .sf-filled.sf-input-group.sf-rtl input.sf-input:focus, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-input-focus input.sf-input, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl.sf-input-focus input.sf-input, - .sf-small .sf-filled.sf-input-group.sf-rtl.sf-input-focus input.sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $filled-small-input-rtl-padding; + .sf-rtl .sf-filled input.sf-input.sf-medium#{$css}, + .sf-filled input.sf-input#{$css}.sf-medium.sf-rtl, + .sf-medium.sf-rtl .sf-filled input.sf-input#{$css}, + .sf-medium .sf-filled input.sf-input.sf-rtl#{$css}, + .sf-filled.sf-input-group.sf-medium.sf-rtl input.sf-input, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium.sf-rtl input.sf-input, + .sf-rtl .sf-filled.sf-input-group.sf-medium input.sf-input, + .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-medium input.sf-input, + .sf-filled.sf-input-group.sf-rtl input.sf-input.sf-medium, + .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl input.sf-input.sf-medium, + .sf-rtl .sf-filled.sf-input-group input.sf-input.sf-medium, + .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper input.sf-input.sf-medium, + .sf-medium .sf-filled.sf-input-group.sf-rtl input.sf-input, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl input.sf-input, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper input.sf-input, + .sf-medium.sf-rtl .sf-filled.sf-input-group input.sf-input, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper input.sf-input:focus, + .sf-medium.sf-rtl .sf-filled.sf-input-group input.sf-input:focus, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl input.sf-input:focus, + .sf-medium .sf-filled.sf-input-group.sf-rtl input.sf-input:focus, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-input-focus input.sf-input, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl.sf-input-focus input.sf-input, + .sf-medium .sf-filled.sf-input-group.sf-rtl.sf-input-focus input.sf-input { + padding: $filled-medium-input-rtl-padding; text-indent: 0; - } } - .sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-rtl input, - .sf-filled.sf-float-input.sf-small.sf-rtl input, - .sf-rtl .sf-filled.sf-float-input.sf-small input, - .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-small input, - .sf-filled.sf-float-input.sf-rtl input.sf-small, - .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl input.sf-small, - .sf-rtl .sf-filled.sf-float-input input.sf-small, - .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper input.sf-small, - .sf-small .sf-filled.sf-float-input.sf-rtl input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl input, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper input, - .sf-small.sf-rtl .sf-filled.sf-float-input input, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper input:focus, - .sf-small.sf-rtl .sf-filled.sf-float-input input:focus, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl input:focus, - .sf-small .sf-filled.sf-float-input.sf-rtl input:focus, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-focus input, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-input-focus input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl.sf-input-focus input, - .sf-small .sf-filled.sf-float-input.sf-rtl.sf-input-focus input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $filled-small-float-input-rtl-padding; + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-rtl input, + .sf-filled.sf-float-input.sf-medium.sf-rtl input, + .sf-rtl .sf-filled.sf-float-input.sf-medium input, + .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input, + .sf-filled.sf-float-input.sf-rtl input.sf-medium, + .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl input.sf-medium, + .sf-rtl .sf-filled.sf-float-input input.sf-medium, + .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper input.sf-medium, + .sf-medium .sf-filled.sf-float-input.sf-rtl input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl input, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper input, + .sf-medium.sf-rtl .sf-filled.sf-float-input input, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper input:focus, + .sf-medium.sf-rtl .sf-filled.sf-float-input input:focus, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl input:focus, + .sf-medium .sf-filled.sf-float-input.sf-rtl input:focus, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-focus input, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-input-focus input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl.sf-input-focus input, + .sf-medium .sf-filled.sf-float-input.sf-rtl.sf-input-focus input { + padding: $filled-medium-float-input-rtl-padding; text-indent: 0; - } } .sf-filled.sf-float-input, @@ -2953,69 +2364,57 @@ .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled, .sf-filled.sf-float-input.sf-input-group.sf-disabled, .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-disabled { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { font-size: $filled-input-font-size; - } - } - - .sf-filled.sf-float-input.sf-small, - .sf-small .sf-filled.sf-float-input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper, - .sf-filled.sf-float-input.sf-small.sf-disabled, - .sf-small .sf-filled.sf-float-input.sf-disabled, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-disabled, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled, - .sf-filled.sf-float-input.sf-input-group.sf-small.sf-disabled, - .sf-small .sf-filled.sf-float-input.sf-input-group.sf-disabled, - .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-small.sf-disabled, - .sf-small .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-disabled { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $small-filled-input-font-size; - } - } - - .sf-filled.sf-input-group.sf-small:not(.sf-float-input) .sf-input, - .sf-small .sf-filled.sf-input-group:not(.sf-float-input) .sf-input, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small:not(.sf-float-input) .sf-input, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper:not(.sf-float-input) .sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - min-height: $small-filled-default-input-min-height; - } } - .sf-filled.sf-float-input.sf-small input, - .sf-small .sf-filled.sf-float-input input, - .sf-filled.sf-float-input.sf-input-group.sf-small input, - .sf-small .sf-filled.sf-float-input.sf-input-group input, - .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-small input, - .sf-small .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - min-height: $small-filled-input-min-height; - } + .sf-filled.sf-float-input.sf-medium, + .sf-medium .sf-filled.sf-float-input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper, + .sf-filled.sf-float-input.sf-medium.sf-disabled, + .sf-medium .sf-filled.sf-float-input.sf-disabled, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-disabled, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled, + .sf-filled.sf-float-input.sf-input-group.sf-medium.sf-disabled, + .sf-medium .sf-filled.sf-float-input.sf-input-group.sf-disabled, + .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-medium.sf-disabled, + .sf-medium .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-disabled { + font-size: $medium-filled-input-font-size; + } + + .sf-filled.sf-input-group.sf-medium:not(.sf-float-input) .sf-input, + .sf-medium .sf-filled.sf-input-group:not(.sf-float-input) .sf-input, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium:not(.sf-float-input) .sf-input, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper:not(.sf-float-input) .sf-input { + min-height: $medium-filled-default-input-min-height; + } + + .sf-filled.sf-float-input.sf-medium input, + .sf-medium .sf-filled.sf-float-input input, + .sf-filled.sf-float-input.sf-input-group.sf-medium input, + .sf-medium .sf-filled.sf-float-input.sf-input-group input, + .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-medium input, + .sf-medium .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group input { + min-height: $medium-filled-input-min-height; } .sf-filled.sf-input-group input.sf-input, .sf-filled.sf-input-group.sf-control-wrapper input.sf-input, .sf-filled.sf-input-group:not(.sf-float-input) input.sf-input, .sf-filled.sf-input-group:not(.sf-float-input).sf-control-wrapper input.sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { min-height: $filled-default-input-min-height; - } } .sf-float-input.sf-filled.sf-input-group.sf-control-wrapper input, .sf-float-input.sf-filled input, .sf-float-input.sf-filled.sf-control-wrapper input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { min-height: $filled-input-min-height; - } } .sf-filled label.sf-float-text, @@ -3023,7 +2422,6 @@ .sf-filled.sf-float-input.sf-control-wrapper label.sf-float-text, .sf-filled.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { font-size: $filled-float-label-font-size; left: $filled-input-label-left; letter-spacing: .009375em; @@ -3035,18 +2433,15 @@ transform: none; transition: transform 150ms cubic-bezier(.4, $zero-value, .2, 1), color 150ms cubic-bezier(.4, $zero-value, .2, 1); width: 100%; - } } .sf-filled.sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { font-size: $filled-float-label-font-size; padding-left: $float-label-padding; top: $filled-input-label-top; transform: none; width: 100%; - } } .sf-filled.sf-float-input input:focus ~ label.sf-float-text, @@ -3063,19 +2458,9 @@ .sf-filled.sf-float-input.sf-control-wrapper input label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-input-focus input ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-focus input ~ label.sf-float-text { - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $filled-float-label-font-size; - top: $filled-input-label-top-after-floating; - transform: translateY(-50%) scale(.75); - } - @if $input-skin-name == 'Material3' { font-size: 16px; top: 14px; transform: translateY(-50%) scale(.75); - } - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - left: 1px; - } } .sf-filled.sf-float-input textarea:focus ~ label.sf-float-text, @@ -3090,11 +2475,9 @@ .sf-filled.sf-float-input.sf-control-wrapper textarea[readonly] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper textarea label.sf-float-text.sf-label-top { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { font-size: $filled-float-label-font-size; top: $filled-input-label-top-after-floating; transform: translateY(-50%) scale(.75); - } } .sf-filled.sf-float-input input:-webkit-autofill ~ label.sf-float-text, @@ -3103,84 +2486,77 @@ .sf-filled.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { font-size: $filled-float-label-font-size; top: $filled-input-label-top-after-floating; transform: translateY(-50%) scale(.75); user-select: text; - } - } - - .sf-filled.sf-float-input.sf-small input:focus ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-small input:valid ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-small input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-filled.sf-float-input input ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-small input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-small input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small input:focus ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small input:valid ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper input ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-small.sf-input-focus input ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-input-focus input ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-small textarea:focus ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-small textarea:valid ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-small textarea ~ label.sf-label-top.sf-float-text, - .sf-small .sf-filled.sf-float-input textarea ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small textarea:focus ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small textarea:valid ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small textarea ~ label.sf-label-top.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper textarea ~ label.sf-label-top.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $small-filled-float-label-font-size; - top: $small-filled-input-label-top-after-floating; - } } - .sf-small .sf-filled.sf-float-input input:-webkit-autofill ~ label.sf-float-text, - .sf-small.sf-filled.sf-float-input input:-webkit-autofill ~ label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill ~ label.sf-float-text, - .sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill ~ label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, - .sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, - .sf-small.sf-filled.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, - .sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, - .sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $small-filled-float-label-font-size; - top: $small-filled-input-label-top-after-floating; - transform: translateY(-50%) scale(.75); + .sf-filled.sf-float-input.sf-medium input:focus ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-medium input:valid ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-medium input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-filled.sf-float-input input ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-medium input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-medium input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input:focus ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input:valid ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper input ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-medium.sf-input-focus input ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-input-focus input ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-medium textarea:focus ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-medium textarea:valid ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-medium textarea ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-filled.sf-float-input textarea ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium textarea:focus ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium textarea:valid ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium textarea ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper textarea ~ label.sf-label-top.sf-float-text { + font-size: $medium-filled-float-label-font-size; + top: $medium-filled-input-label-top-after-floating; + transform: translateY(-16px) scale(.75); + } + + .sf-medium .sf-filled.sf-float-input input:-webkit-autofill ~ label.sf-float-text, + .sf-medium.sf-filled.sf-float-input input:-webkit-autofill ~ label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill ~ label.sf-float-text, + .sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill ~ label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, + .sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, + .sf-medium.sf-filled.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, + .sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, + .sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { + font-size: $medium-filled-float-label-font-size; + top: $medium-filled-input-label-top-after-floating; + transform: translateY(-16px) scale(.75); user-select: text; - } } - .sf-filled.sf-float-input.sf-small label.sf-float-text, - .sf-filled.sf-float-input.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-small textarea ~ label.sf-float-text, - .sf-filled.sf-float-input textarea ~ label.sf-float-text.sf-small, - .sf-filled.sf-float-input textarea.sf-small ~ label.sf-float-text, - .sf-small .sf-filled.sf-float-input textarea ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small textarea ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text.sf-small, - .sf-filled.sf-float-input.sf-control-wrapper textarea.sf-small ~ label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $small-filled-float-label-font-size; - top: $small-filled-input-label-top; - } + .sf-filled.sf-float-input.sf-medium label.sf-float-text, + .sf-filled.sf-float-input.sf-medium:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-medium textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-medium textarea ~ label.sf-float-text, + .sf-filled.sf-float-input textarea ~ label.sf-float-text.sf-medium, + .sf-filled.sf-float-input textarea.sf-medium ~ label.sf-float-text, + .sf-medium .sf-filled.sf-float-input textarea ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium textarea ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text.sf-medium, + .sf-filled.sf-float-input.sf-control-wrapper textarea.sf-medium ~ label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text { + font-size: $medium-filled-float-label-font-size; + top: $medium-filled-input-label-top; } .sf-filled.sf-float-input label.sf-float-text, @@ -3191,9 +2567,7 @@ .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { left: $filled-input-label-left; - } } .sf-filled.sf-float-input.sf-rtl label.sf-float-text, @@ -3209,68 +2583,60 @@ .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-rtl .sf-filled.sf-float-input:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { right: $filled-input-label-left; - } - } - - .sf-filled.sf-float-input.sf-small label.sf-float-text, - .sf-filled.sf-float-input.sf-small label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-small:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - left: $small-filled-input-label-left; - } } - .sf-filled.sf-float-input.sf-small.sf-rtl label.sf-float-text, - .sf-filled.sf-float-input.sf-rtl.sf-small label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-filled.sf-float-input.sf-small label.sf-float-text .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl.sf-small label.sf-float-text, - .sf-filled.sf-float-input.sf-rtl.sf-control-wrapper.sf-small label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-small label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-rtl label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-rtl label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input label.sf-float-text .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-rtl.sf-control-wrapper label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input.sf-control-wrapper label.sf-float-text, - .sf-small.sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-small.sf-filled.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-small.sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small.sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-small.sf-filled.sf-float-input:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-small.sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - right: $small-filled-input-label-left; - } + .sf-filled.sf-float-input.sf-medium label.sf-float-text, + .sf-filled.sf-float-input.sf-medium label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-medium:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-medium:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { + left: $medium-filled-input-label-left; + } + + .sf-filled.sf-float-input.sf-medium.sf-rtl label.sf-float-text, + .sf-filled.sf-float-input.sf-rtl.sf-medium label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-filled.sf-float-input.sf-medium label.sf-float-text .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl.sf-medium label.sf-float-text, + .sf-filled.sf-float-input.sf-rtl.sf-control-wrapper.sf-medium label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-medium label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-rtl label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-rtl label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input label.sf-float-text .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-rtl.sf-control-wrapper label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input.sf-control-wrapper label.sf-float-text, + .sf-medium.sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-medium.sf-filled.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-medium.sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium.sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-medium.sf-filled.sf-float-input:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-medium.sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { + right: $medium-filled-input-label-left; } .sf-filled.sf-float-input .sf-float-line, .sf-float-input.sf-filled.sf-control-wrapper .sf-float-line { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { bottom: -1px; position: absolute; - } } .sf-float-input.sf-filled .sf-float-line, @@ -3286,209 +2652,168 @@ .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-rtl .sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-small.sf-rtl .sf-input-in-wrap label.sf-float-text, - .sf-filled.sf-float-input.sf-rtl.sf-small .sf-input-in-wrap label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-filled.sf-float-input.sf-small .sf-input-in-wrap label.sf-float-text .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl.sf-small .sf-input-in-wrap label.sf-float-text, - .sf-filled.sf-float-input.sf-rtl.sf-control-wrapper.sf-small .sf-input-in-wrap label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-rtl .sf-input-in-wrap label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-rtl .sf-input-in-wrap label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input .sf-input-in-wrap label.sf-float-text .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl .sf-input-in-wrap label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-rtl.sf-control-wrapper .sf-input-in-wrap label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text, - .sf-small.sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-small.sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl .sf-small.sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-rtl.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-float-input.sf-medium.sf-rtl .sf-input-in-wrap label.sf-float-text, + .sf-filled.sf-float-input.sf-rtl.sf-medium .sf-input-in-wrap label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-filled.sf-float-input.sf-medium .sf-input-in-wrap label.sf-float-text .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl.sf-medium .sf-input-in-wrap label.sf-float-text, + .sf-filled.sf-float-input.sf-rtl.sf-control-wrapper.sf-medium .sf-input-in-wrap label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-rtl .sf-input-in-wrap label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-rtl .sf-input-in-wrap label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input .sf-input-in-wrap label.sf-float-text .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl .sf-input-in-wrap label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-rtl.sf-control-wrapper .sf-input-in-wrap label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text, + .sf-medium.sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-medium.sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl .sf-medium.sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-rtl:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-rtl:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-rtl.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { right: $zero-value; - } } .sf-filled.sf-input-group:not(.sf-disabled):not(.sf-float-icon-left)::before, .sf-filled.sf-filled.sf-input-group:not(.sf-disabled):not(.sf-float-icon-left)::after, .sf-filled.sf-input-group.sf-control-wrapper:not(.sf-disabled):not(.sf-float-icon-left)::before, .sf-filled.sf-input-group.sf-control-wrapper:not(.sf-disabled):not(.sf-float-icon-left)::after { - @if $input-skin-name == 'Material3' { bottom: -1px; - } - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - bottom: -.1px; - } } .sf-filled.sf-input-group .sf-input-group-icon, .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, - .sf-filled.sf-input-group.sf-small .sf-input-group-icon, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-input-group.sf-medium .sf-input-group-icon, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon { font-size: $filled-input-icon-size; margin-bottom: $zero-value; margin-top: $zero-value; min-height: $filled-input-icon-min-height; min-width: $filled-input-icon-min-height; padding: $zero-value $zero-value $zero-value 8px; - } } .sf-rtl.sf-filled.sf-input-group .sf-input-group-icon, .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, - .sf-rtl.sf-filled.sf-input-group.sf-small .sf-input-group-icon, - .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-small .sf-rtl.sf-filled.sf-input-group .sf-input-group-icon, - .sf-small .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, + .sf-rtl.sf-filled.sf-input-group.sf-medium .sf-input-group-icon, + .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-medium .sf-rtl.sf-filled.sf-input-group .sf-input-group-icon, + .sf-medium .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, .sf-rtl .sf-filled.sf-input-group .sf-input-group-icon, .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, - .sf-rtl .sf-filled.sf-input-group.sf-small .sf-input-group-icon, - .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-small.sf-rtl .sf-filled.sf-input-group .sf-input-group-icon, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, + .sf-rtl .sf-filled.sf-input-group.sf-medium .sf-input-group-icon, + .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-medium.sf-rtl .sf-filled.sf-input-group .sf-input-group-icon, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon, - .sf-rtl.sf-filled.sf-float-input.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, - .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon, + .sf-rtl.sf-filled.sf-float-input.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, + .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon, .sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon, - .sf-rtl .sf-filled.sf-float-input.sf-input-group.sf-small .sf-input-group-icon, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, - .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-input-group-icon, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-rtl .sf-filled.sf-float-input.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, + .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { padding: $zero-value 8px $zero-value $zero-value; - } } - .sf-filled.sf-input-group.sf-small .sf-input-group-icon, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $small-filled-input-icon-size; - min-height: $small-filled-input-icon-min-height; - min-width: $small-filled-input-icon-min-height; - padding: $zero-value $zero-value $zero-value 4px; - } - } - - .sf-rtl.sf-filled.sf-input-group.sf-small .sf-input-group-icon, - .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-small .sf-rtl.sf-filled.sf-input-group .sf-input-group-icon, - .sf-small .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, - .sf-rtl .sf-filled.sf-input-group.sf-small .sf-input-group-icon, - .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-small.sf-rtl .sf-filled.sf-input-group .sf-input-group-icon, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, - .sf-rtl.sf-filled.sf-float-input.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, - .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon, - .sf-rtl .sf-filled.sf-float-input.sf-input-group.sf-small .sf-input-group-icon, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, - .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-input-group-icon, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $zero-value 4px $zero-value $zero-value; - } + .sf-filled.sf-input-group.sf-medium .sf-input-group-icon, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon { + font-size: $medium-filled-input-icon-size; + min-height: $medium-filled-input-icon-min-height; + min-width: $medium-filled-input-icon-min-height; + padding: 4px $zero-value 4px $zero-value; + } + + .sf-rtl.sf-filled.sf-input-group.sf-medium .sf-input-group-icon, + .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-medium .sf-rtl.sf-filled.sf-input-group .sf-input-group-icon, + .sf-medium .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, + .sf-rtl .sf-filled.sf-input-group.sf-medium .sf-input-group-icon, + .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-medium.sf-rtl .sf-filled.sf-input-group .sf-input-group-icon, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, + .sf-rtl.sf-filled.sf-float-input.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, + .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon, + .sf-rtl .sf-filled.sf-float-input.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, + .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { + padding: 4px $zero-value 4px $zero-value; } .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon, - .sf-filled.sf-float-input.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, - .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-float-input.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, + .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { font-size: $filled-input-icon-size; margin-bottom: $zero-value; margin-top: $zero-value; padding: 8px; margin: 9px 12px 9px 0; - } } - .sf-filled.sf-float-input.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, - .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-input-group-icon, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $small-filled-input-icon-size; - padding: $zero-value $zero-value $zero-value 4px; - } + .sf-filled.sf-float-input.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium .sf-filled.sf-float-input.sf-input-group .sf-input-group-icon, + .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-input-group-icon, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { + font-size: $medium-filled-input-icon-size; + padding: 4px $zero-value 4px $zero-value; } .sf-filled.sf-float-input .sf-clear-icon, .sf-filled.sf-float-input.sf-control-wrapper .sf-clear-icon, .sf-filled.sf-input-group .sf-clear-icon, .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon { - @if $input-skin-name == 'Material3' { font-size: $filled-input-clear-icon-size; padding: $zero-value $zero-value $zero-value 8px; - } - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $filled-input-clear-icon-size; - padding: $input-clear-icon-padding; - } } .sf-filled.sf-input-group .sf-clear-icon, .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon { - @if $input-skin-name == 'Material3' { min-height: $filled-input-icon-min-height; min-width: $filled-input-icon-min-height; padding: $zero-value $zero-value $zero-value 8px; - } - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - min-height: $filled-input-icon-min-height; - min-width: $filled-input-icon-min-height; - padding: $input-clear-icon-padding; - } } .sf-filled.sf-float-input.sf-input-group .sf-clear-icon, .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper .sf-clear-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $zero-value; margin: 2px; height: 32px; - } } - .sf-filled.sf-input-group.sf-small .sf-clear-icon, - .sf-filled.sf-input-group .sf-clear-icon.sf-small, - .sf-small .sf-filled.sf-input-group .sf-clear-icon, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, - .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon.sf-small, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $small-filled-input-clear-icon-size; - min-height: $small-filled-input-icon-min-height; - min-width: $small-filled-input-icon-min-height; + .sf-filled.sf-input-group.sf-medium .sf-clear-icon, + .sf-filled.sf-input-group .sf-clear-icon.sf-medium, + .sf-medium .sf-filled.sf-input-group .sf-clear-icon, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon, + .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon.sf-medium, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon { + font-size: $medium-filled-input-clear-icon-size; + min-height: $medium-filled-input-icon-min-height; + min-width: $medium-filled-input-icon-min-height; padding: $zero-value; - } } - .sf-rtl.sf-filled.sf-float-input.sf-input-group.sf-small .sf-clear-icon, - .sf-small .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-clear-icon, - .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-clear-icon, - .sf-small .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon, - .sf-rtl .sf-filled.sf-float-input.sf-input-group.sf-small .sf-clear-icon, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-clear-icon, - .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-clear-icon, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon { - @if $input-skin-name == 'Material3' { - padding: $zero-value 4px $zero-value $zero-value; - } - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $zero-value; - } + .sf-rtl.sf-filled.sf-float-input.sf-input-group.sf-medium .sf-clear-icon, + .sf-medium .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-clear-icon, + .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-clear-icon, + .sf-medium .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon, + .sf-rtl .sf-filled.sf-float-input.sf-input-group.sf-medium .sf-clear-icon, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-clear-icon, + .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-clear-icon, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon { + padding: $zero-value 8px $zero-value $zero-value; } .sf-filled.sf-float-input .sf-input-in-wrap input:focus ~ label.sf-float-text, @@ -3503,26 +2828,22 @@ .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap input label.sf-float-text.sf-label-top { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { top: $filled-input-label-top-after-floating; - } } - .sf-filled.sf-float-input.sf-small .sf-input-in-wrap input:focus ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-small .sf-input-in-wrap input:valid ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-small .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-filled.sf-float-input .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-small .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-small .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input:focus ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input:valid ~ label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - top: $small-filled-input-label-top-after-floating; - } + .sf-filled.sf-float-input.sf-medium .sf-input-in-wrap input:focus ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-medium .sf-input-in-wrap input:valid ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-medium .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-filled.sf-float-input .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-medium .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-medium .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input:focus ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input:valid ~ label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap input ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input[readonly] ~ label.sf-label-top.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap input[disabled] ~ label.sf-label-top.sf-float-text { + top: $medium-filled-input-label-top-after-floating; } .sf-filled.sf-input-group.sf-float-icon-left.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input-in-wrap, @@ -3533,90 +2854,76 @@ .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-input-focus.sf-success:not(.sf-warning):not(.sf-error) .sf-input-in-wrap, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-input-focus.sf-warning:not(.sf-success):not(.sf-error) .sf-input-in-wrap, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-input-focus.sf-error:not(.sf-success):not(.sf-warning) .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border-style: none; border-width: $zero-value; - } } .sf-filled.sf-float-input .sf-input-in-wrap label.sf-float-text, .sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { top: $filled-input-label-top; - } } - .sf-filled.sf-float-input.sf-small .sf-input-in-wrap label.sf-float-text, - .sf-small .sf-filled.sf-float-input .sf-input-in-wrap label.sf-float-text, - .sf-filled.sf-float-input.sf-small:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - top: $small-filled-input-label-top; - } + .sf-filled.sf-float-input.sf-medium .sf-input-in-wrap label.sf-float-text, + .sf-medium .sf-filled.sf-float-input .sf-input-in-wrap label.sf-float-text, + .sf-filled.sf-float-input.sf-medium:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { + top: $medium-filled-input-label-top; } .sf-filled.sf-input-group:not(.sf-float-input).sf-float-icon-left > .sf-input-group-icon, .sf-filled.sf-input-group:not(.sf-float-input).sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { font-size: $filled-input-icon-size; margin: $zero-value; min-height: $filled-input-left-icon-min-height; min-width: $filled-input-left-icon-min-width; padding: $zero-value; - } } - .sf-filled.sf-input-group:not(.sf-float-input).sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-filled.sf-input-group:not(.sf-float-input).sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-filled.sf-input-group:not(.sf-float-input).sf-control-wrapper.sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-filled.sf-input-group:not(.sf-float-input).sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-small .sf-filled.sf-input-group:not(.sf-float-input).sf-float-icon-left > .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group:not(.sf-float-input).sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - font-size: $small-filled-input-icon-size; + .sf-filled.sf-input-group:not(.sf-float-input).sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-filled.sf-input-group:not(.sf-float-input).sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-filled.sf-input-group:not(.sf-float-input).sf-control-wrapper.sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-filled.sf-input-group:not(.sf-float-input).sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-medium .sf-filled.sf-input-group:not(.sf-float-input).sf-float-icon-left > .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group:not(.sf-float-input).sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon { + font-size: $medium-filled-input-icon-size; margin: $zero-value; - min-height: $small-filled-input-left-icon-min-height; - min-width: $small-filled-input-left-icon-min-width; + min-height: $medium-filled-input-left-icon-min-height; + min-width: $medium-filled-input-left-icon-min-width; padding: $zero-value; - } } .sf-filled.sf-input-group.sf-float-icon-left > .sf-input-group-icon, .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon, .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { margin: $zero-value; min-height: $filled-input-left-icon-min-height; min-width: $filled-input-left-icon-min-width; padding: $zero-value; - } } - .sf-filled.sf-input-group.sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-filled.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-small .sf-filled.sf-input-group.sf-float-icon-left > .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon, - .sf-filled.sf-float-input.sf-input-group.sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-small .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon, - .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small.sf-float-icon-left > .sf-input-group-icon, - .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-small, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-input-group.sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-filled.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-medium .sf-filled.sf-input-group.sf-float-icon-left > .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon, + .sf-filled.sf-float-input.sf-input-group.sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-medium .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon, + .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium.sf-float-icon-left > .sf-input-group-icon, + .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-medium, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon { margin: $zero-value; - min-height: $small-filled-input-left-icon-min-height; - min-width: $small-filled-input-left-icon-min-width; + min-height: $medium-filled-input-left-icon-min-height; + min-width: $medium-filled-input-left-icon-min-width; padding: $zero-value; - } } .sf-filled.sf-float-input .sf-input-in-wrap label.sf-float-text, @@ -3625,71 +2932,63 @@ .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-small .sf-input-in-wrap label.sf-float-text, - .sf-filled.sf-float-input.sf-small .sf-input-in-wrap label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap label.sf-float-text, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small .sf-input-in-wrap label.sf-float-text.sf-label-bottom, - .sf-filled.sf-float-input.sf-small:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input .sf-input-in-wrap label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-float-input.sf-medium .sf-input-in-wrap label.sf-float-text, + .sf-filled.sf-float-input.sf-medium .sf-input-in-wrap label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap label.sf-float-text, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium .sf-input-in-wrap label.sf-float-text.sf-label-bottom, + .sf-filled.sf-float-input.sf-medium:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input .sf-input-in-wrap label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper .sf-input-in-wrap label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus) .sf-input-in-wrap input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { left: $zero-value; - } } .sf-filled.sf-input-group .sf-input-group-icon:last-child, - .sf-filled.sf-input-group.sf-small .sf-input-group-icon:last-child, - .sf-small .sf-filled.sf-input-group .sf-input-group-icon:last-child, + .sf-filled.sf-input-group.sf-medium .sf-input-group-icon:last-child, + .sf-medium .sf-filled.sf-input-group .sf-input-group-icon:last-child, .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon:last-child, - .sf-filled.sf-input-group.sf-small.sf-control-wrapper .sf-input-group-icon:last-child, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon:last-child, + .sf-filled.sf-input-group.sf-medium.sf-control-wrapper .sf-input-group-icon:last-child, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon:last-child, .sf-filled.sf-input-group .sf-input-group-icon, .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, - .sf-filled.sf-input-group.sf-small .sf-input-group-icon, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-input-group.sf-medium .sf-input-group-icon, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon { margin-right: $zero-value; - } } .sf-filled.sf-input-group.sf-rtl .sf-input-group-icon:last-child, - .sf-filled.sf-input-group.sf-small.sf-rtl .sf-input-group-icon:last-child, - .sf-small .sf-filled.sf-input-group.sf-rtl .sf-input-group-icon:last-child, + .sf-filled.sf-input-group.sf-medium.sf-rtl .sf-input-group-icon:last-child, + .sf-medium .sf-filled.sf-input-group.sf-rtl .sf-input-group-icon:last-child, .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon:last-child, - .sf-filled.sf-input-group.sf-small.sf-control-wrapper.sf-rtl .sf-input-group-icon:last-child, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon:last-child, + .sf-filled.sf-input-group.sf-medium.sf-control-wrapper.sf-rtl .sf-input-group-icon:last-child, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon:last-child, .sf-rtl .sf-filled.sf-input-group .sf-input-group-icon:last-child, - .sf-rtl .sf-filled.sf-input-group.sf-small .sf-input-group-icon:last-child, - .sf-rtl.sf-small .sf-filled.sf-input-group .sf-input-group-icon:last-child, + .sf-rtl .sf-filled.sf-input-group.sf-medium .sf-input-group-icon:last-child, + .sf-rtl.sf-medium .sf-filled.sf-input-group .sf-input-group-icon:last-child, .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon:last-child, - .sf-rtl .sf-filled.sf-input-group.sf-small.sf-control-wrapper .sf-input-group-icon:last-child, - .sf-rtl.sf-small .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon:last-child { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-rtl .sf-filled.sf-input-group.sf-medium.sf-control-wrapper .sf-input-group-icon:last-child, + .sf-rtl.sf-medium .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon:last-child { margin-left: $zero-value; - } } .sf-filled.sf-rtl.sf-input-group .sf-input-group-icon, .sf-filled.sf-rtl.sf-input-group.sf-control-wrapper .sf-input-group-icon, .sf-rtl .sf-filled.sf-input-group .sf-input-group-icon, .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon, - .sf-filled.sf-input-group.sf-small.sf-rtl .sf-input-group-icon, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small.sf-rtl .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group.sf-rtl .sf-input-group-icon, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon, - .sf-rtl .sf-filled.sf-input-group.sf-small .sf-input-group-icon, - .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, - .sf-rtl.sf-small .sf-filled.sf-input-group .sf-input-group-icon, - .sf-rtl.sf-small .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-input-group.sf-medium.sf-rtl .sf-input-group-icon, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium.sf-rtl .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group.sf-rtl .sf-input-group-icon, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-rtl .sf-input-group-icon, + .sf-rtl .sf-filled.sf-input-group.sf-medium .sf-input-group-icon, + .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, + .sf-rtl.sf-medium .sf-filled.sf-input-group .sf-input-group-icon, + .sf-rtl.sf-medium .sf-filled.sf-input-group.sf-control-wrapper .sf-input-group-icon { margin-left: $zero-value; margin-right: $zero-value; - } } .sf-filled textarea.sf-input#{$css}, @@ -3709,79 +3008,63 @@ .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus textarea, .sf-filled.sf-input-group.sf-control-wrapper textarea.sf-input, .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus textarea.sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-textarea-padding; - } } .sf-filled.sf-float-input textarea, .sf-filled.sf-float-input.sf-control-wrapper textarea { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-float-textarea-padding; - } } - .sf-filled.sf-input-group.sf-small textarea, - .sf-filled.sf-input-group.sf-small textarea.sf-input, - .sf-filled.sf-input-group textarea.sf-small, - .sf-filled.sf-input-group textarea.sf-input.sf-small, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small textarea, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small textarea.sf-input, - .sf-small .sf-filled.sf-input-group textarea, - .sf-small .sf-filled.sf-input-group textarea.sf-input, - .sf-filled.sf-input-group.sf-input-focus.sf-small textarea, - .sf-filled.sf-input-group.sf-input-focus.sf-small textarea.sf-input, - .sf-filled.sf-input-group.sf-input-focus textarea.sf-small, - .sf-filled.sf-input-group.sf-input-focus textarea.sf-input.sf-small, - .sf-filled.sf-input-group.sf-input-focus textarea.sf-input.sf-small, - .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus.sf-small textarea, - .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus.sf-small textarea.sf-input, - .sf-small .sf-filled.sf-input-group.sf-input-focus textarea, - .sf-small .sf-filled.sf-input-group.sf-input-focus textarea.sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $filled-small-textarea-padding; - } - } - - .sf-filled.sf-float-input.sf-small textarea, - .sf-filled.sf-float-input textarea.sf-small, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small textarea, - .sf-filled.sf-float-input.sf-control-wrapper textarea.sf-small, - .sf-small .sf-filled.sf-float-input textarea, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper textarea { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $filled-small-float-textarea-padding; - } + .sf-filled.sf-input-group.sf-medium textarea, + .sf-filled.sf-input-group.sf-medium textarea.sf-input, + .sf-filled.sf-input-group textarea.sf-medium, + .sf-filled.sf-input-group textarea.sf-input.sf-medium, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium textarea, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium textarea.sf-input, + .sf-medium .sf-filled.sf-input-group textarea, + .sf-medium .sf-filled.sf-input-group textarea.sf-input, + .sf-filled.sf-input-group.sf-input-focus.sf-medium textarea, + .sf-filled.sf-input-group.sf-input-focus.sf-medium textarea.sf-input, + .sf-filled.sf-input-group.sf-input-focus textarea.sf-medium, + .sf-filled.sf-input-group.sf-input-focus textarea.sf-input.sf-medium, + .sf-filled.sf-input-group.sf-input-focus textarea.sf-input.sf-medium, + .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus.sf-medium textarea, + .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus.sf-medium textarea.sf-input, + .sf-medium .sf-filled.sf-input-group.sf-input-focus textarea, + .sf-medium .sf-filled.sf-input-group.sf-input-focus textarea.sf-input { + padding: $filled-medium-textarea-padding; + } + + .sf-filled.sf-float-input.sf-medium textarea, + .sf-filled.sf-float-input textarea.sf-medium, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium textarea, + .sf-filled.sf-float-input.sf-control-wrapper textarea.sf-medium, + .sf-medium .sf-filled.sf-float-input textarea, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper textarea { + padding: $filled-medium-float-textarea-padding; } .sf-filled.sf-input-group.sf-multi-line-input, .sf-filled.sf-input-group.sf-control-wrapper.sf-multi-line-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-textarea-wrapper-padding; - } } - .sf-filled.sf-input-group.sf-small.sf-multi-line-input, - .sf-filled.sf-input-group.sf-control-wrapper.sf-small.sf-multi-line-input, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-multi-line-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $small-filled-textarea-wrapper-padding; - } + .sf-filled.sf-input-group.sf-medium.sf-multi-line-input, + .sf-filled.sf-input-group.sf-control-wrapper.sf-medium.sf-multi-line-input, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-multi-line-input { + padding: $medium-filled-textarea-wrapper-padding; } .sf-filled.sf-float-input.sf-multi-line-input, .sf-filled.sf-float-input.sf-control-wrapper.sf-multi-line-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-float-textarea-wrapper-padding; - } } - .sf-filled.sf-float-input.sf-small.sf-multi-line-input, - .sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-multi-line-input, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-multi-line-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $small-filled-float-textarea-wrapper-padding; - } + .sf-filled.sf-float-input.sf-medium.sf-multi-line-input, + .sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-multi-line-input, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-multi-line-input { + padding: $medium-filled-float-textarea-wrapper-padding; } .sf-filled textarea.sf-input.sf-rtl#{$css}, @@ -3797,10 +3080,8 @@ .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea.sf-input:focus, .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input textarea.sf-input:focus, .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper textarea.sf-input:focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-textarea-padding; text-indent: 0; - } } .sf-filled.sf-float-input.sf-multi-line-input.sf-rtl textarea, @@ -3815,69 +3096,61 @@ .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea:focus, .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input textarea:focus, .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper textarea:focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { padding: $filled-float-textarea-padding; text-indent: 0; - } } - .sf-rtl .sf-filled textarea.sf-input.sf-small#{$css}, - .sf-filled textarea.sf-input#{$css}.sf-small.sf-rtl, - .sf-small.sf-rtl .sf-filled textarea.sf-input#{$css}, - .sf-small .sf-filled textarea.sf-input.sf-rtl#{$css}, - .sf-filled.sf-input-group.sf-multi-line-input.sf-small.sf-rtl textarea.sf-input, - .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-small.sf-rtl textarea.sf-input, - .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-small textarea.sf-input, - .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-small textarea.sf-input, - .sf-filled.sf-input-group.sf-multi-line-input.sf-rtl textarea.sf-input.sf-small, - .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea.sf-input.sf-small, - .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input textarea.sf-input.sf-small, - .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper textarea.sf-input.sf-small, - .sf-small .sf-filled.sf-input-group.sf-multi-line-input.sf-rtl textarea.sf-input, - .sf-small .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea.sf-input, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper textarea.sf-input, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-multi-line-input textarea.sf-input, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper textarea.sf-input:focus, - .sf-small.sf-rtl .sf-filled.sf-input-group.sf-multi-line-input textarea.sf-input:focus, - .sf-small .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea.sf-input:focus, - .sf-small .sf-filled.sf-input-group.sf-multi-line-input.sf-rtl textarea.sf-input:focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $filled-small-textarea-padding; + .sf-rtl .sf-filled textarea.sf-input.sf-medium#{$css}, + .sf-filled textarea.sf-input#{$css}.sf-medium.sf-rtl, + .sf-medium.sf-rtl .sf-filled textarea.sf-input#{$css}, + .sf-medium .sf-filled textarea.sf-input.sf-rtl#{$css}, + .sf-filled.sf-input-group.sf-multi-line-input.sf-medium.sf-rtl textarea.sf-input, + .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-medium.sf-rtl textarea.sf-input, + .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-medium textarea.sf-input, + .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-medium textarea.sf-input, + .sf-filled.sf-input-group.sf-multi-line-input.sf-rtl textarea.sf-input.sf-medium, + .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea.sf-input.sf-medium, + .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input textarea.sf-input.sf-medium, + .sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper textarea.sf-input.sf-medium, + .sf-medium .sf-filled.sf-input-group.sf-multi-line-input.sf-rtl textarea.sf-input, + .sf-medium .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea.sf-input, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper textarea.sf-input, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-multi-line-input textarea.sf-input, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper textarea.sf-input:focus, + .sf-medium.sf-rtl .sf-filled.sf-input-group.sf-multi-line-input textarea.sf-input:focus, + .sf-medium .sf-filled.sf-input-group.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea.sf-input:focus, + .sf-medium .sf-filled.sf-input-group.sf-multi-line-input.sf-rtl textarea.sf-input:focus { + padding: $filled-medium-textarea-padding; text-indent: 0; - } } - .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-small.sf-rtl textarea, - .sf-filled.sf-float-input.sf-multi-line-input.sf-small.sf-rtl textarea, - .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-small textarea, - .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-small textarea, - .sf-filled.sf-float-input.sf-multi-line-input.sf-rtl textarea.sf-small, - .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea.sf-small, - .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input textarea.sf-small, - .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper textarea.sf-small, - .sf-small .sf-filled.sf-float-input.sf-multi-line-input.sf-rtl textarea, - .sf-small .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper textarea, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-multi-line-input textarea, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper textarea:focus, - .sf-small.sf-rtl .sf-filled.sf-float-input.sf-multi-line-input textarea:focus, - .sf-small .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea:focus, - .sf-small .sf-filled.sf-float-input.sf-multi-line-input.sf-rtl textarea:focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - padding: $filled-small-float-textarea-padding; + .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-medium.sf-rtl textarea, + .sf-filled.sf-float-input.sf-multi-line-input.sf-medium.sf-rtl textarea, + .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-medium textarea, + .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-medium textarea, + .sf-filled.sf-float-input.sf-multi-line-input.sf-rtl textarea.sf-medium, + .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea.sf-medium, + .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input textarea.sf-medium, + .sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper textarea.sf-medium, + .sf-medium .sf-filled.sf-float-input.sf-multi-line-input.sf-rtl textarea, + .sf-medium .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper textarea, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-multi-line-input textarea, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper textarea:focus, + .sf-medium.sf-rtl .sf-filled.sf-float-input.sf-multi-line-input textarea:focus, + .sf-medium .sf-filled.sf-float-input.sf-multi-line-input.sf-control-wrapper.sf-rtl textarea:focus, + .sf-medium .sf-filled.sf-float-input.sf-multi-line-input.sf-rtl textarea:focus { + padding: $filled-medium-float-textarea-padding; text-indent: 0; - } } .sf-filled.sf-float-input.sf-float-icon-left .sf-input-in-wrap, .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, .sf-filled.sf-input-group.sf-float-icon-left .sf-input-in-wrap, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border: $input-group-full-border; border-width: $zero-value; margin-left: 8px; - } } .sf-rtl .sf-filled.sf-float-input.sf-float-icon-left .sf-input-in-wrap, @@ -3887,74 +3160,62 @@ .sf-filled.sf-float-input.sf-float-icon-left.sf-rtl .sf-input-in-wrap, .sf-filled.sf-input-group.sf-float-icon-left.sf-rtl .sf-input-in-wrap, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { margin-left: $zero-value; margin-right: 8px; - } } - .sf-filled.sf-float-input.sf-float-icon-left.sf-small .sf-input-in-wrap, - .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left.sf-small .sf-input-in-wrap, - .sf-filled.sf-input-group.sf-float-icon-left.sf-small .sf-input-in-wrap, - .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-small .sf-input-in-wrap, - .sf-small .sf-filled.sf-float-input.sf-float-icon-left .sf-input-in-wrap, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, - .sf-small .sf-filled.sf-input-group.sf-float-icon-left .sf-input-in-wrap, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-filled.sf-float-input.sf-float-icon-left.sf-medium .sf-input-in-wrap, + .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left.sf-medium .sf-input-in-wrap, + .sf-filled.sf-input-group.sf-float-icon-left.sf-medium .sf-input-in-wrap, + .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-medium .sf-input-in-wrap, + .sf-medium .sf-filled.sf-float-input.sf-float-icon-left .sf-input-in-wrap, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, + .sf-medium .sf-filled.sf-input-group.sf-float-icon-left .sf-input-in-wrap, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap { margin-left: 4px; - } } - .sf-rtl.sf-small .sf-filled.sf-float-input.sf-float-icon-left .sf-input-in-wrap, - .sf-rtl.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, - .sf-rtl.sf-small .sf-filled.sf-input-group.sf-float-icon-left .sf-input-in-wrap, - .sf-rtl.sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, - .sf-rtl .sf-small.sf-filled.sf-float-input.sf-float-icon-left .sf-input-in-wrap, - .sf-rtl .sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, - .sf-rtl .sf-small.sf-filled.sf-input-group.sf-float-icon-left .sf-input-in-wrap, - .sf-rtl .sf-small.sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, - .sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap, - .sf-small.sf-filled.sf-float-input.sf-float-icon-left.sf-rtl .sf-input-in-wrap, - .sf-small.sf-filled.sf-input-group.sf-float-icon-left.sf-rtl .sf-input-in-wrap, - .sf-small.sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap, - .sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap, - .sf-small .sf-filled.sf-float-input.sf-float-icon-left.sf-rtl .sf-input-in-wrap, - .sf-small .sf-filled.sf-input-group.sf-float-icon-left.sf-rtl .sf-input-in-wrap, - .sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { + .sf-rtl.sf-medium .sf-filled.sf-float-input.sf-float-icon-left .sf-input-in-wrap, + .sf-rtl.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, + .sf-rtl.sf-medium .sf-filled.sf-input-group.sf-float-icon-left .sf-input-in-wrap, + .sf-rtl.sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, + .sf-rtl .sf-medium.sf-filled.sf-float-input.sf-float-icon-left .sf-input-in-wrap, + .sf-rtl .sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, + .sf-rtl .sf-medium.sf-filled.sf-input-group.sf-float-icon-left .sf-input-in-wrap, + .sf-rtl .sf-medium.sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, + .sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap, + .sf-medium.sf-filled.sf-float-input.sf-float-icon-left.sf-rtl .sf-input-in-wrap, + .sf-medium.sf-filled.sf-input-group.sf-float-icon-left.sf-rtl .sf-input-in-wrap, + .sf-medium.sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap, + .sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap, + .sf-medium .sf-filled.sf-float-input.sf-float-icon-left.sf-rtl .sf-input-in-wrap, + .sf-medium .sf-filled.sf-input-group.sf-float-icon-left.sf-rtl .sf-input-in-wrap, + .sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-rtl .sf-input-in-wrap { margin-left: $zero-value; margin-right: 4px; - } } .sf-filled.sf-input-group.sf-float-icon-left:not(.sf-float-input)::before, .sf-filled.sf-input-group.sf-float-icon-left:not(.sf-float-input)::after, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-float-input)::before, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-float-input)::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { @include input-group-animation; bottom: -1px; - } } .sf-filled.sf-input-group.sf-float-icon-left:not(.sf-float-input).sf-input-focus::before, .sf-filled.sf-input-group.sf-float-icon-left:not(.sf-float-input).sf-input-focus::after, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-float-input).sf-input-focus::before, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-float-input).sf-input-focus::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { @include input-group-animation-width; bottom: -1px; - } } .sf-filled.sf-input-group.sf-float-icon-left:not(.sf-float-input).sf-input-focus .sf-input-in-wrap::before, .sf-filled.sf-input-group.sf-float-icon-left:not(.sf-float-input).sf-input-focus .sf-input-in-wrap::after, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-float-input).sf-input-focus .sf-input-in-wrap::before, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-float-input).sf-input-focus .sf-input-in-wrap::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { width: $zero-value; - } } .sf-filled.sf-input-group.sf-float-icon-left, @@ -3965,10 +3226,8 @@ .sf-filled.sf-input-group.sf-control-wrapper.sf-success.sf-float-icon-left, .sf-filled.sf-input-group.sf-control-wrapper.sf-warning.sf-float-icon-left, .sf-filled.sf-input-group.sf-control-wrapper.sf-error.sf-float-icon-left { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border: $input-group-full-border; border-width: $input-group-full-border-width; - } } } @@ -3995,9 +3254,6 @@ textarea.sf-input#{$css}, .sf-float-input.sf-input-group.sf-control-wrapper { background: $input-bg-color; color: $input-font-color; - @if $input-skin-name == 'fluent2' { - border-bottom-color: $input-border-bottom-color; - } } .sf-input-group .sf-input-group-icon, @@ -4005,9 +3261,7 @@ textarea.sf-input#{$css}, background: transparent; border-color: $input-box-border-color; color: $input-icon-font-color; - @if ($input-skin-name == 'Material3' or $input-skin-name == 'fluent2') { margin: 0; - } } .sf-input-group.sf-disabled .sf-input-group-icon path, @@ -4019,8 +3273,6 @@ textarea.sf-input#{$css}, fill: $input-group-disabled-color; } -/* stylelint-disable property-no-vendor-prefix */ -/* stylelint-disable selector-no-vendor-prefix */ .sf-input#{$css}[disabled], .sf-input-group .sf-input[disabled], .sf-input-group.sf-control-wrapper .sf-input[disabled], @@ -4032,25 +3284,14 @@ textarea.sf-input#{$css}, .sf-float-input.sf-control-wrapper textarea[disabled], .sf-float-input.sf-disabled, .sf-float-input.sf-control-wrapper.sf-disabled { - @if ($input-skin-name != 'bootstrap5.3' and $input-skin-name != 'tailwind3') { -webkit-text-fill-color: $input-disable-font-color; - } - @if $input-skin-name == 'tailwind3' - { - -webkit-text-fill-color: $content-text-color-disabled; - } background: $input-disable-bg-color; color: $input-disable-font-color; - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { background-image: linear-gradient(90deg, $input-disable-border-color 0, $input-disable-border-color 33%, transparent 0); background-position: bottom -1px left 0; background-repeat: repeat-x; background-size: 4px 1px; border-bottom-color: transparent; - } - @else { - border-color: $input-disable-border-color; - } } .sf-input-group input.sf-input, @@ -4078,12 +3319,7 @@ textarea.sf-input#{$css}, .sf-float-input textarea[readonly], .sf-float-input.sf-control-wrapper textarea[readonly] { background: $input-readonly-bg-color; - @if $input-skin-name == 'bootstrap5.3' { - color: $input-readonly-color; - } - @if $input-skin-name != 'bootstrap5.3' { color: inherit; - } } .sf-float-input.sf-disabled input, @@ -4097,7 +3333,7 @@ textarea.sf-input#{$css}, color: inherit; } -/*! Added color to textbox for disbaled state */ +/*! Added color to textbox for disabled state */ .sf-float-input:not(.sf-disabled) input[disabled], .sf-float-input.sf-control-wrapper:not(.sf-disabled) input[disabled], .sf-float-input:not(.sf-disabled) textarea[disabled], @@ -4114,18 +3350,14 @@ textarea.sf-input#{$css}, .sf-input-group:not(.sf-disabled) .sf-input-group-icon:hover, .sf-input-group.sf-control-wrapper:not(.sf-disabled) .sf-input-group-icon:hover { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { border: $input-icon-hover-border; background: $input-icon-hover-bg-color; - } color: $input-group-hovered-color; } .sf-input-group.sf-disabled .sf-input-group-icon:hover, .sf-input-group.sf-control-wrapper.sf-disabled .sf-input-group-icon:hover { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { background: transparent; - } } .sf-input#{$css}.sf-success, @@ -4164,10 +3396,10 @@ textarea.sf-input#{$css}, label.sf-float-text, .sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-float-input.sf-small:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { +.sf-float-input.sf-medium:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { color: $input-header-font-color; } @@ -4220,9 +3452,7 @@ label.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-input-group) .sf-float-line::after, .sf-float-input.sf-control-wrapper:not(.sf-input-group) .sf-float-line::before, .sf-float-input.sf-control-wrapper:not(.sf-input-group) .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { background: $input-active-accent-color; - } } .sf-float-input.sf-success:not(.sf-input-group) .sf-float-line::before, @@ -4233,9 +3463,7 @@ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-success:not(.sf-input-group) .sf-float-line::after, .sf-float-input.sf-control-wrapper.sf-success:not(.sf-input-group) .sf-float-line::before, .sf-float-input.sf-control-wrapper.sf-success:not(.sf-input-group) .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { background: $input-success-color; - } } .sf-float-input.sf-warning:not(.sf-input-group) .sf-float-line::before, @@ -4246,9 +3474,7 @@ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-warning:not(.sf-input-group) .sf-float-line::after, .sf-float-input.sf-control-wrapper.sf-warning:not(.sf-input-group) .sf-float-line::before, .sf-float-input.sf-control-wrapper.sf-warning:not(.sf-input-group) .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { background: $input-warning-color; - } } input.sf-input#{$css}:-moz-placeholder, @@ -4305,16 +3531,12 @@ textarea.sf-input#{$css}:-ms-input-placeholder, .sf-float-input.sf-control-wrapper.sf-error:not(.sf-input-group) .sf-float-line::after, .sf-float-input.sf-control-wrapper.sf-error:not(.sf-input-group) .sf-float-line::before, .sf-float-input.sf-control-wrapper.sf-error:not(.sf-input-group) .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { background: $input-error-color; - } } .sf-input-group:not(.sf-disabled) .sf-input-group-icon:active, .sf-input-group.sf-control-wrapper:not(.sf-disabled) .sf-input-group-icon:active { - @if $skin-name != 'bootstrap5.3' and $skin-name != 'tailwind3' { background: $input-group-pressed-bg; - } color: $input-group-pressed-color; } @@ -4328,7 +3550,7 @@ textarea.sf-input#{$css}::selection, .sf-input-group.sf-control-wrapper textarea.sf-input::selection, .sf-float-input textarea::selection, .sf-float-input.sf-control-wrapper textarea::selection, -.sf-float-input.sf-small textarea::selection, +.sf-float-input.sf-medium textarea::selection, .sf-float-input textarea::selection { @include input-selection; } @@ -4357,18 +3579,7 @@ textarea.sf-input#{$css}::selection, .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left .sf-float-line::after, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left .sf-float-line::before, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - @include input-group-animation-bg; - } -} - -.sf-input-group::before, -.sf-input-group::after, -.sf-input-group.sf-control-wrapper::before, -.sf-input-group.sf-control-wrapper::after { - @if $input-skin-name != 'Material3' { @include input-group-animation-bg; - } } .sf-input-group:not(.sf-float-icon-left):not(.sf-float-input).sf-success::before, @@ -4401,18 +3612,7 @@ textarea.sf-input#{$css}::selection, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left.sf-success .sf-float-line::after, .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-success .sf-float-line::before, .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-success .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { @include input-group-success-animation-bg; - } -} - -.sf-input-group.sf-success::before, -.sf-input-group.sf-success::after, -.sf-input-group.sf-control-wrapper.sf-success::before, -.sf-input-group.sf-control-wrapper.sf-success::after { - @if $input-skin-name != 'Material3' { - @include input-group-success-animation-bg; - } } .sf-input-group:not(.sf-float-icon-left).sf-warning:not(.sf-float-input)::before, @@ -4433,18 +3633,7 @@ textarea.sf-input#{$css}::selection, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-warning:not(.sf-float-input)::after, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left.sf-warning .sf-float-line::before, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left.sf-warning .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - @include input-group-warning-animation-bg; - } -} - -.sf-input-group.sf-warning::before, -.sf-input-group.sf-warning::after, -.sf-input-group.sf-control-wrapper.sf-warning::before, -.sf-input-group.sf-control-wrapper.sf-warning::after { - @if $input-skin-name != 'Material3' { @include input-group-warning-animation-bg; - } } .sf-input-group:not(.sf-float-icon-left).sf-error:not(.sf-float-input)::before, @@ -4471,18 +3660,7 @@ textarea.sf-input#{$css}::selection, .sf-filled.sf-float-input.sf-input-group.sf-float-icon-left.sf-error .sf-float-line::after, .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-error .sf-float-line::before, .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-error .sf-float-line::after { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - @include input-group-error-animation-bg; - } -} - -.sf-input-group.sf-error::before, -.sf-input-group.sf-error::after, -.sf-input-group.sf-control-wrapper.sf-error::before, -.sf-input-group.sf-control-wrapper.sf-error::after { - @if $input-skin-name != 'Material3' { @include input-group-error-animation-bg; - } } .sf-input-group.sf-success .sf-input-group-icon, @@ -4501,9 +3679,6 @@ textarea.sf-input#{$css}::selection, .sf-input-group.sf-error:not(.sf-disabled):not(:active) .sf-input-group-icon:hover, .sf-input-group.sf-control-wrapper.sf-error:not(.sf-disabled):not(:active) .sf-input-group-icon:hover { color: $input-icon-font-color; - @if $input-skin-name == 'bootstrap5.3' { - color: $input-group-hovered-color; - } } .sf-input-group.sf-success:not(.sf-disabled) .sf-input-group-icon:active, @@ -4512,9 +3687,7 @@ textarea.sf-input#{$css}::selection, .sf-input-group.sf-control-wrapper.sf-warning:not(.sf-disabled) .sf-input-group-icon:active, .sf-input-group.sf-error:not(.sf-disabled) .sf-input-group-icon:active, .sf-input-group.sf-control-wrapper.sf-error:not(.sf-disabled) .sf-input-group-icon:active { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { color: $input-icon-font-color; - } } .sf-input-group input.sf-input, @@ -4535,30 +3708,7 @@ textarea.sf-input#{$css}::selection, .sf-float-input.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-group) textarea:focus, .sf-float-input:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-group).sf-input-focus input, .sf-float-input.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-group).sf-input-focus input { - @if $input-skin-name != 'fluent2' { border-color: $input-active-border-color; - } - @if $input-skin-name == 'tailwind3' or $input-skin-name == 'bootstrap5.3' { - border-radius: $input-groupo-focus-border-radius; - box-shadow: $input-group-focus-box-shadow; - } - @if $input-skin-name == 'fluent2' { - border-bottom-color: $input-active-border-color; - padding-bottom: 2px; - border-bottom-width: 2px; - } -} - -.sf-input-group .sf-input#{$css}:focus:not(.sf-success):not(.sf-warning):not(.sf-error), -.sf-input-group .sf-float-input:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-group) input:focus, -.sf-input-group .sf-float-input:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-group) textarea:focus, -.sf-input-group .sf-float-input.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-group) input:focus, -.sf-input-group .sf-float-input.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-group) textarea:focus, -.sf-input-group .sf-float-input:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-group).sf-input-focus input, -.sf-input-group .sf-float-input.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-group).sf-input-focus input { - @if $input-skin-name == 'tailwind3' or $input-skin-name == 'bootstrap5.3' { - box-shadow: none; - } } .sf-input-group:not(.sf-success):not(.sf-warning):not(.sf-error) input.sf-input:focus, @@ -4584,30 +3734,14 @@ textarea.sf-input#{$css}::selection, .sf-float-input.sf-control-wrapper.sf-input-focus.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) .sf-input-in-wrap:hover, .sf-float-input.sf-control-wrapper.sf-input-focus:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) input:not([disabled]), .sf-float-input.sf-control-wrapper.sf-input-focus:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) textarea:not([disabled]) { - @if $input-skin-name == 'Material3' { border-color: $input-group-border-color-focus; - } -} - -.sf-input-group.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error), -.sf-input-group.sf-control-wrapper.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) { - @if $input-skin-name != 'Material3' and $input-skin-name != 'fluent2' and $input-skin-name != 'tailwind3' { - border-color: $input-group-border-color-focus; - } - @if $input-skin-name == 'tailwind3' or $input-skin-name == 'bootstrap5.3' { - border-color: $input-group-border-color-focus; - border-radius: $input-groupo-focus-border-radius; - box-shadow: $input-group-focus-box-shadow; - } } .sf-input-group.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input:first-child:focus, .sf-input-group.sf-input-focus.sf-rtl:not(.sf-success):not(.sf-warning):not(.sf-error) span:last-child.sf-input-group-icon, .sf-input-group.sf-control-wrapper.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input:first-child:focus, .sf-input-group.sf-control-wrapper.sf-input-focus.sf-rtl:not(.sf-success):not(.sf-warning):not(.sf-error) span:last-child.sf-input-group-icon { - @if ($input-skin-name != 'bootstrap5.3') { border-color: $input-group-border-right-focus; - } } .sf-input-group.sf-input-focus.sf-rtl:not(.sf-success):not(.sf-warning):not(.sf-error) span.sf-input-group-icon, @@ -4620,9 +3754,7 @@ textarea.sf-input#{$css}::selection, .sf-input-focus.sf-control-wrapper.sf-rtl:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input:first-child:focus, .sf-input-focus.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input:last-child:focus, .sf-input-group.sf-control-wrapper.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) span.sf-input-group-icon { - @if ($input-skin-name != 'bootstrap5.3') { border-color: $input-group-border-left-focus; - } } .sf-input-group.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) span:first-child.sf-input-group-icon, @@ -4631,9 +3763,7 @@ textarea.sf-input#{$css}::selection, .sf-input-group.sf-control-wrapper.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) span:first-child.sf-input-group-icon, .sf-input-group.sf-control-wrapper.sf-input-focus.sf-rtl:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input:last-child:focus, .sf-input-group.sf-control-wrapper.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) span:first-child.sf-input-group-icon { - @if ($input-skin-name != 'bootstrap5.3') { border-color: transparent; - } } .sf-input-group:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-float-icon-left), @@ -4642,22 +3772,7 @@ textarea.sf-input#{$css}::selection, .sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input-in-wrap, .sf-float-input.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input-in-wrap, .sf-float-input.sf-control-wrapper.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input-in-wrap { - @if $input-skin-name == 'Material3' { - border-color: $input-group-full-border-color; - } - @else if $input-skin-name == 'fluent2' { - border-bottom-color: $input-group-full-border-bottom-color; - } -} - -.sf-input-group:not(.sf-success):not(.sf-warning):not(.sf-error), -.sf-input-group.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error) { - @if $input-skin-name != 'Material3' and $input-skin-name != 'fluent2' { border-color: $input-group-full-border-color; - } - @if $input-skin-name == 'tailwind3' { - box-shadow: $input-group-border-shadow; - } } .sf-input-group.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-float-icon-left) .sf-input:focus, @@ -4668,20 +3783,8 @@ textarea.sf-input#{$css}::selection, .sf-input-group.sf-float-icon-left.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input, .sf-input-group.sf-control-wrapper.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-float-icon-left) .sf-input, .sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { border-bottom-color: transparent; border-top-color: transparent; - } -} - -.sf-input-group.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input:focus, -.sf-input-group.sf-control-wrapper.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input:focus, -.sf-input-group.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input, -.sf-input-group.sf-control-wrapper.sf-input-focus:not(.sf-success):not(.sf-warning):not(.sf-error) .sf-input { - @if $input-skin-name != 'Material3' { - border-bottom-color: transparent; - border-top-color: transparent; - } } .sf-input-group.sf-success, @@ -4698,9 +3801,7 @@ textarea.sf-input#{$css}::selection, .sf-float-icon-left.sf-input-group.sf-control-wrapper.sf-error, .sf-input-group.sf-float-icon-left, .sf-input-group.sf-control-wrapper.sf-float-icon-left { - @if $input-skin-name == 'Material3' { border-color: transparent; - } } .sf-input-group.sf-success, @@ -4712,9 +3813,7 @@ textarea.sf-input#{$css}::selection, .sf-input-group.sf-float-icon-left.sf-success .sf-input-in-wrap, .sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-success .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { @include input-state-color ($input-group-success-color); - } } .sf-input-group.sf-warning, @@ -4726,9 +3825,7 @@ textarea.sf-input#{$css}::selection, .sf-input-group.sf-float-icon-left.sf-warning .sf-input-in-wrap, .sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-warning .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { @include input-state-color ($input-group-warning-color); - } } .sf-input-group.sf-error, @@ -4740,9 +3837,7 @@ textarea.sf-input#{$css}::selection, .sf-input-group.sf-float-icon-left.sf-error .sf-input-in-wrap, .sf-input-group.sf-control-wrapper.sf-float-icon-left.sf-error .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { @include input-state-color ($input-group-error-color); - } } .sf-float-input .sf-clear-icon path, @@ -4759,10 +3854,8 @@ textarea.sf-input#{$css}::selection, path { fill: $input-clear-icon-hover-color; } - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' { border: $input-clear-icon-hover-border; background: $input-clear-icon-hover-bg-color; - } } .sf-float-input.sf-input-focus .sf-input:focus, @@ -4778,58 +3871,58 @@ textarea.sf-input#{$css}::selection, .sf-float-input:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, .sf-float-input:not(.sf-error) input label.sf-float-text.sf-label-top, .sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) input:valid ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) input ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) input[readonly] ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) input:valid ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) input ~ label.sf-label-top.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-float-input:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-float-input.sf-small:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input:valid ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[readonly] ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) input:valid ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) input ~ label.sf-label-top.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) input:valid ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) input ~ label.sf-label-top.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) input[readonly] ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) input:valid ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) input ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-float-input:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-medium:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input:valid ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input ~ label.sf-label-top.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[readonly] ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) input:valid ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) input ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text, .sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, .sf-float-input:not(.sf-error) textarea:valid ~ label.sf-float-text, .sf-float-input:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, .sf-float-input:not(.sf-error) textarea[readonly] ~ label.sf-label-top.sf-float-text, .sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-float-input:not(.sf-error) textarea label.sf-float-text.sf-label-top, -.sf-float-input.sf-small:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) textarea:valid ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) textarea[readonly] ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) textarea:valid ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-medium:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) textarea:valid ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) textarea[readonly] ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) textarea:valid ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, .sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text { +.sf-medium .sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text { color: $float-label-font-color; } @@ -4846,33 +3939,28 @@ textarea.sf-input#{$css}::selection, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly] ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea label.sf-float-text.sf-label-top { - @if $input-skin-name != 'fluent2' and $input-skin-name != 'tailwind3' { color: $float-label-font-color; - } - @else { - color: $float-label-top-font-color; - } } .sf-float-input:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-float-input.sf-small:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-float-input:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { +.sf-float-input.sf-medium:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-float-input:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { color: $input-header-font-color; } -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea:valid ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[readonly] ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:valid ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top { +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea:valid ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[readonly] ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:valid ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top { color: $float-label-font-color; } @@ -4884,14 +3972,14 @@ textarea.sf-input#{$css}::selection, .sf-float-input.sf-disabled label.sf-float-text.sf-label-top, .sf-float-input:not(.sf-error) input[disabled] ~ label.sf-float-text, .sf-float-input:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, .sf-float-input textarea[disabled] ~ label.sf-float-text, .sf-float-input textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-float-text, .sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, .sf-float-input.sf-control-wrapper.sf-input-group.sf-disabled .sf-float-text, .sf-float-input.sf-control-wrapper.sf-input-group.sf-disabled .sf-float-text.sf-label-top, .sf-float-input.sf-control-wrapper.sf-disabled input[disabled] ~ label.sf-float-text, @@ -4902,70 +3990,68 @@ textarea.sf-input#{$css}::selection, .sf-float-input.sf-control-wrapper.sf-disabled label.sf-float-text.sf-label-top, .sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, .sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) input[disabled] ~ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, .sf-float-input.sf-control-wrapper.sf-disabled textarea[disabled] ~ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-disabled textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-float-text, .sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, .sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) textarea[disabled] ~ label.sf-float-text, .sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top { +.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top { color: $float-label-disbale-font-color; } .sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, .sf-float-input:not(.sf-error) input[readonly]:focus ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly]:focus ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) input[readonly]:focus ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) input[readonly]:focus ~ label.sf-float-text.sf-label-top, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[readonly]:focus ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly]:focus ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-medium:not(.sf-error) input[readonly]:focus ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) input[readonly]:focus ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[readonly]:focus ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly]:focus ~ label.sf-float-text.sf-label-top, .sf-float-input:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-small:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-medium:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text.sf-label-top, .sf-float-input:not(.sf-error) textarea[readonly]:focus ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly]:focus ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text.sf-label-top, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-medium:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text.sf-label-top, .sf-float-input:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-label-top.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-label-top.sf-float-text, -.sf-float-input.sf-small:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-float-input.sf-medium:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text.sf-label-top, .sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, .sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-float-input.sf-small:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-float-input.sf-medium:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { +.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text { color: $input-active-accent-color; - } } .sf-input-group:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled):not(.sf-float-icon-left), @@ -4981,66 +4067,7 @@ textarea.sf-input#{$css}::selection, .sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) input:not([disabled]), .sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) textarea:not([disabled]), .sf-float-input.sf-control-wrapper.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) .sf-input-in-wrap:hover { - @if $input-skin-name == 'Material3' { - border-color: $input-group-border-color-hover; - } - @else if $input-skin-name == 'fluent2' { - border-bottom-color: $input-group-full-border-bottom-color; - } -} - -.sf-input-group:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-input-group.sf-control-wrapper:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-float-input:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-float-input:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) input:not([disabled]), -.sf-float-input:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) textarea:not([disabled]), -.sf-float-input.sf-control-wrapper:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) input:not([disabled]), -.sf-float-input.sf-control-wrapper:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) textarea:not([disabled]) { - @if $input-skin-name != 'Material3' and $input-skin-name != 'bootstrap5.3' and $input-skin-name != 'fluent2' { border-color: $input-group-border-color-hover; - } -} - -.sf-input-group.sf-input-focus:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-input-group.sf-control-wrapper.sf-input-focus:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-float-input.sf-input-focus:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-float-input.sf-control-wrapper.sf-input-focus:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-float-input.sf-input-focus:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) input:not([disabled]), -.sf-float-input.sf-control-wrapper.sf-input-focus:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) input:not([disabled]), -.sf-float-input.sf-input-focus:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) textarea:not([disabled]), -.sf-float-input.sf-control-wrapper.sf-input-focus:hover:not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) textarea:not([disabled]) { - @if $input-skin-name != 'Material3' and $input-skin-name != 'fluent2' { - border-color: $input-group-border-color-focus; - } -} - -.sf-underline.sf-input-group:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-input-group.sf-control-wrapper:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-float-input.sf-control-wrapper:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-input-group.sf-float-icon-left:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-input-group.sf-control-wrapper.sf-float-icon-left:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-float-input.sf-float-icon-left:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-float-input.sf-control-wrapper.sf-float-icon-left:not(.sf-input-focus):hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-float-input:hover:not(.sf-input-focus):not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) { - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - border-color: $outline-hover-border-color; - color: $outline-hover-font-color; - } -} - -.sf-underline.sf-input-group:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-input-group.sf-control-wrapper:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-float-input.sf-control-wrapper:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-input-group.sf-float-icon-left:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-input-group.sf-control-wrapper.sf-float-icon-left:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-float-input.sf-float-icon-left:hover:not(.sf-input-focus):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-float-input.sf-control-wrapper.sf-float-icon-left:not(.sf-input-focus):hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled), -.sf-underline.sf-float-input:hover:not(.sf-input-focus):not(.sf-input-group):not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled) { - @if $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { - border-bottom-color: $underline-hover-border-color; - color: $outline-hover-font-color; - } } .sf-filled.sf-input-group, @@ -5049,10 +4076,8 @@ textarea.sf-input#{$css}::selection, .sf-filled.sf-float-input.sf-input-group, .sf-filled.sf-float-input.sf-control-wrapper, .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { background: $filled-input-bg-color; transition: $background-color-transition; - } } .sf-filled.sf-input-group:hover, @@ -5061,10 +4086,8 @@ textarea.sf-input#{$css}::selection, .sf-filled.sf-float-input.sf-input-group:hover, .sf-filled.sf-float-input.sf-control-wrapper:hover, .sf-filled.sf-float-input.sf-input-group.sf-control-wrapper:hover { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { background: $filled-input-overlay-focused-bg-color; transition: $background-color-transition; - } } .sf-filled.sf-input-group.sf-input-focus, @@ -5079,10 +4102,8 @@ textarea.sf-input#{$css}::selection, .sf-filled.sf-float-input:hover.sf-input-group.sf-input-focus, .sf-filled.sf-float-input:hover.sf-control-wrapper.sf-input-focus, .sf-filled.sf-float-input:hover.sf-input-group.sf-control-wrapper.sf-input-focus { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { background: $filled-input-overlay-activated-bg-color; transition: $background-color-transition; - } } .sf-filled.sf-input-group:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled):not(.sf-float-icon-left), @@ -5115,54 +4136,44 @@ textarea.sf-input#{$css}::selection, .sf-filled.sf-input-group.sf-control-wrapper.sf-input-focus:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled).sf-float-icon-left, .sf-filled.sf-float-input.sf-input-focus:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled).sf-float-icon-left, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-focus:hover:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-disabled).sf-float-icon-left { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border-color: $filled-input-hover-border-color; - } } .sf-filled.sf-input-group.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error), .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error), .sf-filled.sf-float-input:not(.sf-success):not(.sf-warning):not(.sf-error), .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error) { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border-color: $input-group-full-border-color; - } } .sf-filled.sf-float-input.sf-success, .sf-filled.sf-float-input.sf-control-wrapper.sf-success, .sf-filled.sf-input-group.sf-float-icon-left.sf-success, .sf-filled.sf-input-group.sf-float-icon-left.sf-control-wrapper.sf-success { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { @include input-state-color ($input-success-color); - } } .sf-filled.sf-float-input.sf-warning, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning, .sf-filled.sf-input-group.sf-float-icon-left.sf-warning, .sf-filled.sf-input-group.sf-float-icon-left.sf-control-wrapper.sf-warning { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { @include input-state-color ($input-warning-color); - } } .sf-filled.sf-float-input.sf-error, .sf-filled.sf-float-input.sf-control-wrapper.sf-error, .sf-filled.sf-input-group.sf-float-icon-left.sf-error, .sf-filled.sf-input-group.sf-float-icon-left.sf-control-wrapper.sf-error { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { @include input-state-color ($input-error-color); - } } .sf-filled label.sf-float-text, .sf-filled.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-filled.sf-float-input.sf-small:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-filled.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-filled.sf-float-input.sf-medium:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-filled.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, .sf-filled.sf-float-input:not(.sf-error) input:valid ~ label.sf-float-text, .sf-filled.sf-float-input:not(.sf-error) input ~ label.sf-label-top.sf-float-text, @@ -5178,17 +4189,17 @@ textarea.sf-filled.sf-input#{$css}::-webkit-input-placeholder, .sf-input-group.sf-filled textarea.sf-input::-webkit-input-placeholder, .sf-input-group.sf-filled.sf-control-wrapper textarea.sf-input::-webkit-input-placeholder, .sf-filled.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error):not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-error) textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, input.sf-filled.sf-input#{$css}:-moz-placeholder, .sf-input-group.sf-filled input.sf-input:-moz-placeholder, .sf-input-group.sf-filled input.sf-input:-moz-placeholder, @@ -5210,9 +4221,7 @@ input.sf-filled.sf-input#{$css}:-ms-input-placeholder, .sf-input-group.sf-filled.sf-control-wrapper input.sf-input:-ms-input-placeholder, textarea.sf-filled.sf-input#{$css}:-ms-input-placeholder, .sf-input-group.sf-filled.sf-control-wrapper textarea.sf-input:-ms-input-placeholder { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { color: $filled-input-float-label-color; - } } .sf-filled.sf-float-input.sf-error label.sf-float-text, @@ -5223,9 +4232,7 @@ textarea.sf-filled.sf-input#{$css}:-ms-input-placeholder, .sf-filled.sf-float-input.sf-control-wrapper.sf-error textarea:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-error.sf-input-focus input ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-error.sf-input-focus input ~ label.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { color: $input-error-color; - } } .sf-filled.sf-float-input.sf-success label.sf-float-text, @@ -5257,33 +4264,31 @@ textarea.sf-filled.sf-input#{$css}:-ms-input-placeholder, .sf-filled.sf-float-input.sf-control-wrapper.sf-success input[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input .sf-control-wrapper.sf-success input label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success input:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success input ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success input[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success input[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success input:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success input ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-success input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-small.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success input:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success input ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success input[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success input[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success input:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success input ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input ~ label.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { +.sf-filled.sf-float-input.sf-medium.sf-success input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success input:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success input ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success input[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success input[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success input:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success input ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-success input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium.sf-success.sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success.sf-input-focus input ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success input:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success input ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success input[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success input[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success input:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success input ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success.sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input ~ label.sf-float-text { color: $input-success-color; - } } .sf-filled.sf-float-input.sf-warning label.sf-float-text, @@ -5315,21 +4320,19 @@ textarea.sf-filled.sf-input#{$css}:-ms-input-placeholder, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input .sf-control-wrapper.sf-warning input label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning input:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning input ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning input[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning input[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning input:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning input ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-warning input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-small.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning.sf-input-focus input ~ label.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { +.sf-filled.sf-float-input.sf-medium.sf-warning input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning input:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning input ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning input[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning input[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning input:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning input ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-warning input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium.sf-warning.sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning.sf-input-focus input ~ label.sf-float-text { color: $input-warning-color; - } } .sf-filled.sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, @@ -5346,36 +4349,36 @@ textarea.sf-filled.sf-input#{$css}:-ms-input-placeholder, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input .sf-control-wrapper:not(.sf-error) input label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) input:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) input ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) input[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) input:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) input ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-small:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) input:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) input ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) input[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) input:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) input ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text, .sf-filled.sf-float-input:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-filled.sf-float-input:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input:not(.sf-input-focus):not(.sf-disabled) textarea:not(:focus):not(:valid) ~ label.sf-float-text:not(.sf-label-top), .sf-filled.sf-float-input:not(.sf-input-focus) textarea:not(:focus):not(:valid) ~ label.sf-float-text:not(.sf-label-top), .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-input-focus):not(.sf-disabled) textarea:not(:focus):not(:valid) ~ label.sf-float-text:not(.sf-label-top), @@ -5391,105 +4394,88 @@ textarea.sf-filled.sf-input#{$css}:-ms-input-placeholder, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) textarea:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) textarea:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top { color: $filled-input-float-label-color; - } } .sf-filled.sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) input:focus ~ label.sf-float-text, .sf-filled.sf-float-input:not(.sf-error) input[readonly]:focus ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly]:focus ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) input[readonly]:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) input[readonly]:focus ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[readonly]:focus ~ label.sf-float-text, -.sf-small .sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly]:focus ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) input[readonly]:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) input[readonly]:focus ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[readonly]:focus ~ label.sf-float-text, +.sf-medium .sf-float-input.sf-control-wrapper:not(.sf-error) input[readonly]:focus ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input[readonly] ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input:not(.sf-error) textarea[readonly]:focus ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly]:focus ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[readonly]:focus ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus textarea[readonly] ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error) textarea:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea:focus ~ label.sf-float-text, .sf-filled.sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input:not(.sf-error).sf-input-focus input ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error).sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error).sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error).sf-input-focus input ~ label.sf-float-text { color: $input-active-accent-color; - } -} - -.sf-input-group textarea.sf-input:-ms-input-placeholder, -.sf-input-group textarea.sf-input:-moz-placeholder, -.sf-input-group .sf-input:-ms-input-placeholder, -input.sf-input#{$css}::-webkit-input-placeholder { - @if $input-skin-name == 'tailwind3' or $input-skin-name == 'bootstrap5.3' { - color: $input-placeholder-color; - } } .sf-filled.sf-float-input:not(.sf-disabled) .sf-clear-icon:hover, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-disabled) .sf-clear-icon:hover, .sf-filled.sf-input-group:not(.sf-disabled) .sf-clear-icon:hover, .sf-filled.sf-input-group.sf-control-wrapper:not(.sf-disabled) .sf-clear-icon:hover { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { path { fill: $filled-input-clear-icon-hover-color; } - } } .sf-filled.sf-float-input:not(.sf-disabled) .sf-clear-icon:active, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-disabled) .sf-clear-icon:active, .sf-filled.sf-input-group:not(.sf-disabled) .sf-clear-icon:active, .sf-filled.sf-input-group.sf-control-wrapper:not(.sf-disabled) .sf-clear-icon:active { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { path { fill: $filled-input-clear-icon-active-color; } - } } .sf-filled.sf-input#{$css}[disabled], @@ -5503,28 +4489,23 @@ input.sf-input#{$css}::-webkit-input-placeholder { .sf-filled.sf-float-input.sf-control-wrapper textarea[disabled], .sf-filled.sf-float-input.sf-disabled, .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { background-image: none; background-position: initial; background-repeat: no-repeat; background-size: $zero-value; border-color: $filled-input-disabled-border-color; color: $filled-input-disabled-font-color; - } } .sf-filled.sf-float-input.sf-disabled:not(.sf-success):not(.sf-warning):not(.sf-error), .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-success):not(.sf-warning):not(.sf-error) { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { border-color: $filled-input-disabled-border-color; - } } .sf-filled.sf-input-group:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-float-icon-left).sf-disabled, .sf-filled.sf-input-group.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error).sf-disabled .sf-input-in-wrap, .sf-filled.sf-input-group.sf-control-wrapper:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-float-icon-left).sf-disabled, .sf-filled.sf-input-group.sf-control-wrapper.sf-float-icon-left:not(.sf-success):not(.sf-warning):not(.sf-error).sf-disabled .sf-input-in-wrap { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { background: $filled-input-disabled-bg-color; background-image: none; background-position: initial; @@ -5532,7 +4513,6 @@ input.sf-input#{$css}::-webkit-input-placeholder { background-size: $zero-value; border-color: $filled-input-disabled-border-color; color: $filled-input-disabled-font-color; - } } .sf-filled.sf-float-input.sf-input-group.sf-disabled .sf-float-text, @@ -5543,14 +4523,14 @@ input.sf-input#{$css}::-webkit-input-placeholder { .sf-filled.sf-float-input.sf-disabled label.sf-float-text.sf-label-top, .sf-filled.sf-float-input:not(.sf-error) input[disabled] ~ label.sf-float-text, .sf-filled.sf-float-input:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input textarea[disabled] ~ label.sf-float-text, .sf-filled.sf-float-input textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-float-text, .sf-filled.sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-disabled .sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-disabled .sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled input[disabled] ~ label.sf-float-text, @@ -5561,24 +4541,24 @@ input.sf-input#{$css}::-webkit-input-placeholder { .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) input[disabled] ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled textarea[disabled] ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) textarea[disabled] ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, input.sf-filled.sf-disabled.sf-input#{$css}:-moz-placeholder, .sf-input-group.sf-filled.sf-disabled input.sf-input:-moz-placeholder, .sf-input-group.sf-filled.sf-disabled input.sf-input:-moz-placeholder, @@ -5616,17 +4596,15 @@ textarea.sf-filled.sf-disabled.sf-input#{$css}::-webkit-input-placeholder, .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-disabled) input[disabled], .sf-filled.sf-float-input:not(.sf-disabled) textarea[disabled], .sf-filled.sf-float-input.sf-control-wrapper:not(.sf-disabled) textarea[disabled] { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { color: $filled-input-disabled-font-color; - } } .sf-filled.sf-float-input.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-filled.sf-float-input.sf-small.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-filled.sf-float-input.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-filled.sf-float-input.sf-medium.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-filled.sf-float-input.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-success textarea:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-success textarea:valid ~ label.sf-float-text, .sf-filled.sf-float-input.sf-success textarea ~ label.sf-label-top.sf-float-text, @@ -5639,79 +4617,77 @@ textarea.sf-filled.sf-disabled.sf-input#{$css}::-webkit-input-placeholder, .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea[readonly] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-small.sf-success textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success textarea:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success textarea ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success textarea[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success textarea[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success textarea:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success textarea ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-success textarea[disabled] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success textarea:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success textarea ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success textarea[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success textarea[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium.sf-success textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success textarea:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success textarea ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success textarea[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success textarea[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success textarea:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success textarea ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-success textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success textarea:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success textarea ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success textarea[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success textarea[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea[disabled] ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-success input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success input:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-success textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success textarea:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-success input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success input:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success textarea:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success.sf-input-focus input ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-success.sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success.sf-input-focus input ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-success.sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input ~ label.sf-float-text, .sf-filled.sf-float-input.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-success textarea:not(:focus):-webkit-autofill ~ label.sf-float-text { color: $input-success-color; - } } -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning input:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning input ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning input[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning input[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus input ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning input:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning input ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning input[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning input[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning.sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus input ~ label.sf-float-text, .sf-filled.sf-float-input.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-filled.sf-float-input.sf-small.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-filled.sf-float-input.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-filled.sf-float-input.sf-medium.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-filled.sf-float-input.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, .sf-filled.sf-float-input.sf-warning textarea:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-warning textarea:valid ~ label.sf-float-text, .sf-filled.sf-float-input.sf-warning textarea ~ label.sf-label-top.sf-float-text, @@ -5724,57 +4700,65 @@ textarea.sf-filled.sf-disabled.sf-input#{$css}::-webkit-input-placeholder, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea[readonly] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea[disabled] ~ label.sf-label-top.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-small.sf-warning textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning textarea:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning textarea ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning textarea[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning textarea[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning textarea:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning textarea ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-warning textarea[disabled] ~ label.sf-float-text.sf-label-top, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning textarea:valid ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning textarea ~ label.sf-label-top.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning textarea[readonly] ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning textarea[disabled] ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:valid ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea ~ label.sf-label-top.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea[readonly] ~ label.sf-float-text.sf-label-top, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-medium.sf-warning textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning textarea:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning textarea ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning textarea[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning textarea[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning textarea:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning textarea ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-warning textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning textarea:valid ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning textarea ~ label.sf-label-top.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning textarea[readonly] ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning textarea[disabled] ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:valid ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea ~ label.sf-label-top.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea[readonly] ~ label.sf-float-text.sf-label-top, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea[disabled] ~ label.sf-float-text.sf-label-top, .sf-filled.sf-float-input.sf-warning input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning input:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-warning textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning textarea:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning input:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning input:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning input:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:focus ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning textarea:focus ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:focus ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning textarea:focus ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning textarea:focus ~ label.sf-float-text, .sf-filled.sf-float-input.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-small.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning.sf-input-focus input ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-medium.sf-warning.sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning.sf-input-focus input ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus inputs ~ label.sf-float-text, -.sf-filled.sf-float-input.sf-control-wrapper.sf-small.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus input ~ label.sf-float-text, +.sf-filled.sf-float-input.sf-control-wrapper.sf-medium.sf-warning.sf-input-focus input ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus input ~ label.sf-float-text, .sf-filled.sf-float-input.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning:not(.sf-input-focus) input:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium .sf-filled.sf-float-input.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text { - @if $input-skin-name == 'Material3' or $input-skin-name == 'fluent2' or $input-skin-name == 'tailwind3' { +.sf-medium .sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text, +.sf-medium.sf-filled.sf-float-input.sf-control-wrapper.sf-autofill.sf-warning textarea:not(:focus):-webkit-autofill ~ label.sf-float-text { color: $input-warning-color; - } +} + +.sf-form-label { + font-size: $form-label-font-size; + line-height: $form-label-line-height; +} + +.sf-form-error { + font-size: $form-error-font-size; + color: $form-error-color; } diff --git a/components/inputs/styles/input/_material3-definition.scss b/components/inputs/src/inputs/input/_material3-definition.scss similarity index 64% rename from components/inputs/styles/input/_material3-definition.scss rename to components/inputs/src/inputs/input/_material3-definition.scss index d620f84..e43b81a 100644 --- a/components/inputs/styles/input/_material3-definition.scss +++ b/components/inputs/src/inputs/input/_material3-definition.scss @@ -1,48 +1,45 @@ -$input-skin-name: 'Material3' !default; $zero-value: 0 !default; $one-value: 1px !default; $border-type: solid !default; $border-size: 1px !default; $input-child-padding-bottom: $zero-value !default; -$input-child-small-bigger-padding-bottom: $zero-value !default; -$input-child-small-padding-bottom: $zero-value !default; +$input-child-medium-bigger-padding-bottom: $zero-value !default; +$input-child-medium-padding-bottom: $zero-value !default; $input-child-bigger-padding-bottom: $zero-value !default; $input-group-full-border: $border-size $border-type !default; -$input-child-small-bigger-padding-top: $zero-value !default; -$input-child-small-padding-top: $zero-value !default; +$input-child-medium-bigger-padding-top: $zero-value !default; +$input-child-medium-padding-top: $zero-value !default; $input-child-padding-top: $zero-value !default; $input-child-bigger-padding-top: $zero-value !default; -$input-success-color: rgba($success-font-color) !default; -$input-warning-color: rgba($warning-font-color) !default; -$input-error-color: rgba($error-font-color) !default; +$input-success-color: rgb($success-font-color) !default; +$input-warning-color: rgb($warning-font-color) !default; +$input-error-color: rgb($error-font-color) !default; $input-disable-bg-color: $transparent !default; -$input-active-bg-color: $flyout-bg-color !default; -$input-active-font-color: rgba($content-text-color) !default; -$input-press-font-color: rgba($primary-text-color) !default; +$input-active-font-color: rgb($content-text-color) !default; $input-box-border-radius: $zero-value !default; $input-group-child-border-width: $zero-value !default; $input-bg-color: transparent !default; $input-font-size: $text-sm !default; -$input-small-font-size: $text-xs !default; +$input-medium-font-size: $text-base !default; $input-padding: 4px $zero-value 4px !default; $textarea-padding: 4px $zero-value 4px !default; $textarea-bigger-padding: 5px $zero-value 5px !default; -$textarea-small-padding: 4px $zero-value 4px !default; -$textarea-bigger-small-padding: 4px $zero-value 4px !default; +$textarea-medium-padding: 7px $zero-value 7px !default; +$textarea-bigger-medium-padding: 4px $zero-value 4px !default; $textarea-min-height: 15px !default; $textarea-bigger-min-height: 17px !default; -$textarea-bigger-small-min-height: 15px !default; -$textarea-small-min-height: 14px !default; +$textarea-bigger-medium-min-height: 15px !default; +$textarea-medium-min-height: 24px !default; $float-input-padding: 4px $zero-value 4px !default; $float-label-padding: $zero-value !default; $float-input-wrap-padding-top: $zero-value !default; $input-padding-bottom: 4px !default; $input-focus-padding-bottom: 3px !default; $input-clear-icon: 16px !default; -$input-small-clear-icon: $font-icon-16 !default; +$input-medium-clear-icon: $font-icon-16 !default; $input-clear-icon-padding: $zero-value !default; $input-clear-icon-margin: 0 !default; -$input-small-clear-icon-border-radius: 16px !default; +$input-medium-clear-icon-border-radius: 20px !default; $input-clear-icon-position: center !default; $float-input-bigger-padding: 5px $zero-value 5px !default; $float-input-bigger-wrap-padding-top: 1px !default; @@ -52,40 +49,39 @@ $input-child-min-height: 30px !default; $input-child-min-width: 30px !default; $input-child-icon-border-radius: 16px !default; $input-margin-bottom: 0 !default; -$input-small-margin-bottom: 4px !default; -$input-small-bigger-margin-bottom: 4px !default; +$input-medium-margin-bottom: 4px !default; +$input-medium-bigger-margin-bottom: 4px !default; $input-margin-top: 16px !default; -$input-small-margin-top: 16px !default; -$input-small-bigger-margin-top: 17px !default; -$input-small-padding: 3px $zero-value 3px !default; -$float-input-small-padding: 4px $zero-value 4px !default; -$float-input-small-wrap-padding-top: $zero-value !default; -$input-small-padding-bottom: 4px !default; -$input-focus-small-padding-bottom: 3px !default; -$float-input-bigger-small-padding: 4px $zero-value 4px !default; -$float-input-bigger-small-wrap-padding-top: 1px !default; -$input-focus-bigger-small-padding-bottom: 3px !default; -$input-small-child-padding: $zero-value !default; -$input-small-child-min-height: 22px !default; -$input-small-child-min-width: 24px !default; -$input-small-child-border-radius: 14px !default; +$input-medium-margin-top: 16px !default; +$input-medium-bigger-margin-top: 17px !default; +$input-medium-padding: 7px $zero-value 7px !default; +$float-input-medium-padding: 8px $zero-value 8px !default; +$float-input-medium-wrap-padding-top: $zero-value !default; +$input-medium-padding-bottom: 6px !default; +$input-focus-medium-padding-bottom: 6px !default; +$float-input-bigger-medium-padding: 4px $zero-value 4px !default; +$float-input-bigger-medium-wrap-padding-top: 1px !default; +$input-focus-bigger-medium-padding-bottom: 3px !default; +$input-medium-child-padding: $zero-value !default; +$input-medium-child-min-height: 40px !default; +$input-medium-child-min-width: 40px !default; +$input-medium-child-border-radius: 20px !default; $input-font-family: $font-family !default; $input-font-style: normal !default; -$input-font-normal: normal !default; $input-border: $zero-value $border-type !default; $float-input-border: $zero-value $border-type !default; $input-box-border-width: $zero-value $zero-value $one-value $zero-value !default; $float-input-border-width: $zero-value $zero-value $one-value $zero-value !default; $input-focus-border-width: $zero-value $zero-value 2px $zero-value !default; -$input-box-border-color: rgba($border) !default; -$input-font-color: rgba($content-text-color) !default; -$input-icon-font-color: rgba($icon-color) !default; -$input-clear-icon-color: rgba($icon-color) !default; -$input-clear-icon-hover-color: rgba($icon-color-hover) !default; -$input-error-color: rgba($error-font-color) !default; -$input-accent: rgba($primary) !default; -$input-placeholder: rgba($placeholder-text-color) !default; -$input-active-border-color: rgba($primary) !default; +$input-box-border-color: rgb($border) !default; +$input-font-color: rgb($content-text-color) !default; +$input-icon-font-color: rgb($icon-color) !default; +$input-clear-icon-color: rgb($icon-color) !default; +$input-clear-icon-hover-color: rgb($icon-color-hover) !default; +$input-error-color: rgb($error-font-color) !default; +$input-accent: rgb($primary) !default; +$input-placeholder: rgb($placeholder-text-color) !default; +$input-active-border-color: rgb($primary) !default; $input-group-active-border-color: $transparent !default; $input-disable-border-type: dashed !default; $input-disable-border-color: $input-box-border-color !default; @@ -95,23 +91,21 @@ $input-group-active-border: $transparent !default; $input-group-border: 1px $border-type !default; $input-group-border-width: $zero-value !default; $input-group-border-radius: 2px !default; -$input-group-pressed-bg: rgba($content-text-color, .12) !default; -$input-hover-bg-color: rgba($secondary) !default; -$input-active-accent-color: rgba($primary) !default; -$input-valid-group-border-width: 2px !default; -$input-header-font-color: rgba($placeholder-text-color) !default; +$input-group-pressed-bg: rgb($content-text-color, .12) !default; +$input-active-accent-color: rgb($primary) !default; +$input-header-font-color: rgb($placeholder-text-color) !default; $input-child-margin-bottom: 4px !default; $input-child-margin-right: 4px !default; $input-child-margin-top: 4px !default; $float-input-child-margin-top: 2px !default; -$input-child-small-bigger-margin-bottom: 4px !default; -$input-child-small-bigger-margin-right: 4px !default; -$input-child-small-bigger-margin-top: 4px !default; -$float-input-child-small-bigger-margin-top: 4px !default; -$input-child-small-margin-bottom: 4px !default; -$input-child-small-margin-right: 4px !default; -$input-child-small-margin-top: 4px !default; -$float-input-child-small-margin-top: 4px !default; +$input-child-medium-bigger-margin-bottom: 4px !default; +$input-child-medium-bigger-margin-right: 4px !default; +$input-child-medium-bigger-margin-top: 4px !default; +$float-input-child-medium-bigger-margin-top: 4px !default; +$input-child-medium-margin-bottom: 4px !default; +$input-child-medium-margin-right: 4px !default; +$input-child-medium-margin-top: 4px !default; +$float-input-child-medium-margin-top: 4px !default; $input-child-bigger-margin-bottom: 5px !default; $input-child-bigger-margin-right: 8px !default; $input-child-bigger-margin-top: 6px !default; @@ -123,7 +117,7 @@ $input-opacity-filter: 100 !default; $input-group-border-width-focus: $zero-value $zero-value $one-value $zero-value !default; $input-group-border-type-focus: $border-type !default; $input-group-border-color-focus: $input-box-border-color !default; -$input-group-border-color-hover: rgba($content-text-color) !default; +$input-group-border-color-hover: rgb($content-text-color) !default; $input-group-border-width-hover: 1px !default; $input-group-border-right-focus: $transparent !default; $input-group-border-left-focus: $transparent !default; @@ -133,54 +127,48 @@ $input-group-success-color: $input-success-color !default; $input-group-warning-color: $input-warning-color !default; $input-group-error-color: $input-error-color !default; $input-valid-border-bottom-width: 2px !default; -$input-group-pressed-color: rgba($content-text-color) !default; -$input-select-font-color: rgba($primary-text-color) !default; +$input-group-pressed-color: rgb($content-text-color) !default; +$input-select-font-color: rgb($primary-text-color) !default; $input-right-border-width: $zero-value !default; $input-text-indent: $zero-value !default; -$input-small-text-indent: $zero-value !default; -$input-small-bigger-text-indent: $zero-value !default; -$input-disable-font-color: rgba($content-text-color, .38) !default; -$float-label-font-color: rgba($placeholder-text-color) !default; +$input-medium-text-indent: $zero-value !default; +$input-medium-bigger-text-indent: $zero-value !default; +$input-disable-font-color: rgb($content-text-color, .38) !default; +$float-label-font-color: rgb($placeholder-text-color) !default; $float-label-disbale-font-color: $input-disable-font-color !default; $float-label-font-size: 12px !default; $float-label-bigger-font-size: 12px !default; -$float-label-small-font-size: 12px !default; -$float-label-bigger-small-font-size: 12px !default; +$float-label-medium-font-size: 12px !default; +$float-label-bigger-medium-font-size: 12px !default; $float-placeholder-font-size: 13px !default; $float-placeholder-bigger-font-size: 14px !default; -$float-placeholder-small-font-size: 12px !default; -$float-placeholder-bigger-small-font-size: 13px !default; +$float-placeholder-medium-font-size: 16px !default; +$float-placeholder-bigger-medium-font-size: 13px !default; $input-border-size: 1px !default; $input-normal-height: 32px - $input-border-size !default; -$input-small-height: 26px - $input-border-size !default; +$input-medium-height: 40px - $input-border-size !default; $float-input-normal-height: 32px !default; $float-input-bigger-height: 40px !default; -$float-input-small-height: 26px !default; -$float-input-bigger-small-height: 36px !default; +$float-input-medium-height: 40px !default; +$float-input-bigger-medium-height: 36px !default; $input-full-height: 100% !default; $textarea-normal-height: auto !default; $textarea-bigger-height: auto !default; -$textarea-small-height: auto !default; -$textarea-bigger-small-height: auto !default; +$textarea-medium-height: auto !default; +$textarea-bigger-medium-height: auto !default; $textarea-full-height: 100% !default; -$input-group-disabled-color: rgba($content-text-color, .38) !default; +$input-group-disabled-color: rgb($content-text-color, .38) !default; $input-group-hovered-color: $input-icon-font-color !default; $input-icon-font-size: $font-icon-16 !default; -$input-small-icon-font-size: $font-icon-16 !default; +$input-medium-icon-font-size: $font-icon-20 !default; $input-inner-wrap-margin-left: 8px !default; -$input-clear-icon-padding-bottom: 4px !default; -$input-clear-icon-padding-right: 8px !default; -$input-clear-icon-padding-left: $input-clear-icon-padding-right !default; -$input-clear-icon-padding-top: 4px !default; -$float-input-clear-icon-padding-top: 4px !default; -$input-clear-icon-small-bigger-padding-bottom: 4px !default; -$input-clear-icon-small-bigger-padding-right: 4px !default; -$input-clear-icon-small-bigger-padding-top: 4px !default; -$float-input-clear-icon-small-bigger-padding-top: 4px !default; -$input-clear-icon-small-padding-bottom: 4px !default; -$input-clear-icon-small-padding-right: 4px !default; -$input-clear-icon-small-padding-top: 4px !default; -$float-input-clear-icon-small-padding-top: 4px !default; +$input-clear-icon-medium-bigger-padding-right: 4px !default; +$input-clear-icon-medium-bigger-padding-top: 4px !default; +$float-input-clear-icon-medium-bigger-padding-top: 4px !default; +$input-clear-icon-medium-padding-bottom: 4px !default; +$input-clear-icon-medium-padding-right: 4px !default; +$input-clear-icon-medium-padding-top: 4px !default; +$float-input-clear-icon-medium-padding-top: 4px !default; $input-clear-icon-bigger-padding-bottom: 5px !default; $input-clear-icon-bigger-padding-right: 8px !default; $input-clear-icon-bigger-padding-top: 6px !default; @@ -189,38 +177,39 @@ $input-clear-icon-min-height: 30px !default; $input-clear-icon-min-width: 30px !default; $input-bigger-clear-icon-min-height: 38px !default; $input-bigger-clear-icon-min-width: 38px !default; -$input-bigger-small-clear-icon-min-height: 34px !default; -$input-bigger-small-clear-icon-min-width: 34px !default; +$input-bigger-medium-clear-icon-min-height: 40px !default; +$input-bigger-medium-clear-icon-min-width: 40px !default; $input-bigger-clear-icon-border-radius: 20px !default; -$input-small-clear-icon-min-height: 22px !default; -$input-small-clear-icon-min-width: 22px !default; -$input-smaller-min-height: 15px !default; +$input-medium-clear-icon-min-height: 48px !default; +$input-medium-clear-icon-min-width: 48px !default; +$input-smaller-min-height: 24px !default; $textarea-smaller-min-height: 18px !default; $input-min-height: 23px !default; $textarea-min-height: 23px !default; $float-label-rtl-value: -7px !default; $input-left-icon-font-size: 20px !default; -$input-small-left-icon-font-size: 20px !default; +$input-medium-left-icon-font-size: 20px !default; $input-left-child-min-height: 30px !default; $input-left-child-min-width: 30px !default; -$input-small-left-child-min-height: 28px !default; -$input-small-left-child-min-width: 28px !default; +$input-medium-left-child-min-height: 28px !default; +$input-medium-left-child-min-width: 28px !default; $input-icon-inner-width: 10px !default; $input-icon-inner-height: 10px !default; $input-readonly-bg-color: none !default; -$input-child-small-margin: 0 !default; +$input-child-medium-margin: 0 !default; $input-icon-hover-border: 1px !default; $input-icon-hover-border-radius: 16px !default; -$input-icon-hover-bg-color: rgba($content-text-color, .08) !default; +$input-opacity-low: 0.08 !default; +$input-icon-hover-bg-color: rgb($content-text-color, $input-opacity-low) !default; $input-clear-icon-hover-border: 1px !default; $input-clear-icon-hover-border-radius: 16px !default; -$input-clear-icon-hover-bg-color: rgba($content-text-color, .08) !default; -$outline-border-color: rgba($border) !default; -$outline-input-font-color: rgba($content-text-color) !default; -$outline-input-label-font-color: rgba($placeholder-text-color) !default; -$outline-input-font-size: 14px !default; -$outline-hover-border-color: rgba($content-text-color) !default; -$outline-hover-font-color: rgba($content-text-color) !default; +$input-clear-icon-hover-bg-color: rgb($content-text-color, .08) !default; +$outline-border-color: rgb($border) !default; +$outline-input-font-color: rgb($content-text-color) !default; +$outline-input-label-font-color: rgb($placeholder-text-color) !default; +$outline-input-font-size: 20px !default; +$outline-hover-border-color: rgb($content-text-color) !default; +$outline-hover-font-color: rgb($content-text-color) !default; $outline-active-input-border: 1px !default; $outline-input-padding-left: 12px !default; $outline-input-padding-top: 10px !default; @@ -228,32 +217,32 @@ $outline-input-padding-bottom: 9px !default; $outline-input-min-height: 40px !default; $outline-input-icon-margin-left: $zero-value !default; $outline-input-icon-margin-right: 12px !default; -$outline-small-input-min-height: 32px !default; -$outline-small-input-padding-left: 10px !default; -$outline-small-input-padding-top: 7px !default; -$outline-small-input-icon-margin-left: 10px !default; -$outline-small-input-icon-margin-right: 6px !default; -$outline-bigger-input-min-height: 56px !default; +$outline-medium-input-min-height: 53px !default; +$outline-medium-input-padding-left: 16px !default; +$outline-medium-input-padding-top: 16px !default; +$outline-medium-input-icon-margin-left: 10px !default; +$outline-medium-input-icon-margin-right: 6px !default; +$outline-bigger-input-min-height: 53px !default; $outline-bigger-input-icon-margin-left: 16px !default; $outline-bigger-input-icon-margin-right: 8px !default; $outline-bigger-input-padding-left: 16px !default; $outline-bigger-input-padding-top: 15px !default; -$outline-small-bigger-input-min-height: 48px !default; -$outline-small-bigger-input-margin-top: 10px !default; -$outline-small-bigger-input-margin-bottom: 9px !default; -$outline-small-bigger-input-margin-left: 12px !default; -$outline-small-bigger-icon-margin-left: 12px !default; -$outline-small-bigger-icon-margin-right: 8px !default; +$outline-medium-bigger-input-min-height: 48px !default; +$outline-medium-bigger-input-margin-top: 10px !default; +$outline-medium-bigger-input-margin-bottom: 9px !default; +$outline-medium-bigger-input-margin-left: 12px !default; +$outline-medium-bigger-icon-margin-left: 12px !default; +$outline-medium-bigger-icon-margin-right: 8px !default; $outline-input-border: $zero-value !default; $outline-input-group-border-width: $zero-value !default; -$outline-disabled-border-color: rgba($border, .38) !default; +$outline-disabled-border-color: rgb($border, .38) !default; $outline-float-label-top: -6px !default; $outline-label-before-element-margin-top: 6px !default; $outline-wrapper-border-infocused: 2px !default; $outline-label-min-width: 9px !default; -$outline-small-label-min-width: 7px !default; +$outline-medium-label-min-width: 12px !default; $outline-bigger-label-min-width: 13px !default; -$outline-bigger-small-label-min-width: 9px !default; +$outline-bigger-medium-label-min-width: 9px !default; $outline-label-height: 8px !default; $outline-label-margin: 4px !default; $outline-label-border-radius: 5px !default; @@ -267,65 +256,78 @@ $outline-bigger-input-font-size: 16px !default; $outline-bigger-input-icon-padding: 15px !default; $outline-bigger-input-icon-font-size: 20px !default; $outline-bigger-clear-icon-font-size: 20px !default; -$outline-small-bigger-input-label-font-size: 12px !default; -$outline-small-bigger-input-icon-font-size: 18px !default; -$outline-small-bigger-clear-icon-font-size: 18px !default; -$outline-label-font-color-with-value: rgba($placeholder-text-color) !default; +$outline-medium-bigger-input-label-font-size: 12px !default; +$outline-medium-bigger-input-icon-font-size: 18px !default; +$outline-medium-bigger-clear-icon-font-size: 18px !default; +$outline-label-font-color-with-value: rgb($placeholder-text-color) !default; $outline-valid-input-font-size: 14px !default; $outline-label-default-line-height: 13px !default; $outline-label-before-left-rtl: 5px !default; $outline-label-after-left-rtl: -6px !default; $outline-label-line-height: 54px !default; $outline-bigger-label-line-height: 70px !default; -$outline-small-bigger-label-line-height: 53px !default; -$outline-small-label-line-height: 46px !default; +$outline-medium-bigger-label-line-height: 53px !default; +$outline-medium-label-line-height: 68px !default; $outline-textarea-label-line-height: 50px !default; $outline-valid-textarea-font-size: 14px !default; -$outline-textarea-small-label-line-height: 60px !default; +$outline-textarea-medium-label-line-height: 60px !default; $outline-textarea-bigger-label-line-height: 66px !default; -$outline-textarea-small-bigger-label-line-height: 60px !default; +$outline-textarea-medium-bigger-label-line-height: 60px !default; $outline-textarea-margin-top: 8px $zero-value 1px !default; -$outline-small-textarea-margin-top: 8px $zero-value 1px !default; +$outline-medium-textarea-margin-top: 12px $zero-value $zero-value !default; $outline-bigger-textarea-maring-top: 8px $zero-value 1px !default; -$outline-small-input-font-size: 13px !default; -$outline-small-input-icon-font-size: 16px !default; -$outline-small-clear-icon-font-size: 16px !default; -$outline-small-label-font-size: 11px !default; +$outline-medium-input-font-size: 16px !default; +$outline-medium-input-icon-font-size: 16px !default; +$outline-medium-clear-icon-font-size: 20px !default; +$outline-medium-label-font-size: 12px !default; $outline-bigger-label-font-size: 12px !default; -$outline-small-bigger-input-font-size: 14px !default; -$outline-small-bigger-label-font-size: 12px !default; -$outline-bigger-small-group-icon-top: 9px !default; -$outline-bigger-small-group-icon-bottom: 9px !default; -$outline-input-small-clear-icon: 14px !default; +$outline-medium-bigger-input-font-size: 14px !default; +$outline-medium-bigger-label-font-size: 12px !default; +$outline-bigger-medium-group-icon-top: 4px !default; +$outline-bigger-medium-group-icon-bottom: 9px !default; +$outline-input-medium-clear-icon: 14px !default; $outline-input-clear-icon: 16px !default; $outline-input-bigger-clear-icon: 20px !default; -$outline-input-bigger-small-clear-icon: 18px !default; -$outline-input-clear-icon-hover-color: rgba($icon-color-hover) !default; -$outline-input-clear-icon-active-color: rgba($icon-color) !default; +$outline-input-bigger-medium-clear-icon: 18px !default; +$outline-input-clear-icon-hover-color: rgb($icon-color-hover) !default; +$outline-input-clear-icon-active-color: rgb($icon-color) !default; $outline-float-label-font-size: 14px !default; $bigger-outline-float-label-font-size: 16px !default; -$bigger-small-outline-float-label-font-size: 14px !default; -$small-outline-float-label-font-size: 13px !default; -$outline-float-label-disbale-font-color: rgba($content-text-color-alt1, .38) !default; -$outline-disabled-input-font-color: rgba($content-text-color, .38) !default; -$outline-input-group-disabled-color: rgba($content-text-color) !default; +$bigger-medium-outline-float-label-font-size: 14px !default; +$medium-outline-float-label-font-size: 16px !default; +$outline-float-label-disbale-font-color: rgb($content-text-color-alt1, .38) !default; +$outline-disabled-input-font-color: rgb($content-text-color, .38) !default; +$outline-input-group-disabled-color: rgb($content-text-color) !default; $textarea-float-top: -22px !default; +$transition-duration-standard: 0.25s !default; +$transition-standard-curve: cubic-bezier(.25, .8, .25, 1) !default; + +$form-label-font-size: $font-size-16 !default; +$form-label-line-height: 1rem !default; +$form-error-font-size: $font-size-12 !default; +$form-error-color: rgb($error-font-color) !default; + @mixin input-sizing { box-sizing: content-box; } + @mixin input-height ($height) { content: ''; } + @mixin input-state-color ($color) { border-bottom-color: $color; } + @mixin input-selection { background: $input-accent; - color: rgba($primary-text-color); + color: rgb($primary-text-color); } + @mixin float-label-alignment { content: ''; } + /* stylelint-disable property-no-vendor-prefix */ @mixin input-group-animation { -moz-transition: .2s cubic-bezier(.4, 0, .4, 1); @@ -336,24 +338,31 @@ $textarea-float-top: -22px !default; transition: .2s cubic-bezier(.4, 0, .4, 1); width: 0; } + @mixin input-group-animation-left { left: 50%; } + @mixin input-group-animation-width { width: 50%; } + @mixin input-group-animation-right { right: 50%; } + @mixin input-group-animation-bg { background: $input-active-border-color; } + @mixin input-group-hover-bg { background: $input-group-border-color-hover; } + @mixin input-group-success-animation-bg { background: $input-success-color; } + @mixin input-group-warning-animation-bg { background: $input-warning-color; } @@ -361,9 +370,11 @@ $textarea-float-top: -22px !default; @mixin input-group-error-animation-bg { background: $input-error-color; } + @mixin input-ripple-parent { position: relative; } + @mixin input-ripple-style { background: $grey-400; border-radius: 100%; @@ -375,146 +386,155 @@ $textarea-float-top: -22px !default; transform: scale(0); width: 40%; } + @mixin input-ripple-animation { animation: e-input-ripple .45s linear; } + @keyframes e-input-ripple { 100% { opacity: 0; transform: scale(4); } } + @keyframes slideTopUp { - from{ transform: translate3d($zero-value, $zero-value, $zero-value) scale(1); } - to{ transform: translate3d($zero-value, $zero-value, $zero-value) scale(1); } + from { + transform: translate3d($zero-value, $zero-value, $zero-value) scale(1); + } + + to { + transform: translate3d($zero-value, $zero-value, $zero-value) scale(1); + } } + $filled-input-font-size: 14px !default; $bigger-filled-input-font-size: 16px !default; -$bigger-small-filled-input-font-size: 14px !default; -$small-filled-input-font-size: 13px !default; +$bigger-medium-filled-input-font-size: 14px !default; +$medium-filled-input-font-size: 16px !default; $filled-float-label-font-size: 14px !default; $bigger-filled-float-label-font-size: 16px !default; -$bigger-small-filled-float-label-font-size: 14px !default; -$small-filled-float-label-font-size: 13px !default; +$bigger-medium-filled-float-label-font-size: 14px !default; +$medium-filled-float-label-font-size: 16px !default; $filled-input-clear-icon-size: 16px !default; $bigger-filled-input-clear-icon-size: 20px !default; -$bigger-small-filled-input-clear-icon-size: 18px !default; -$small-filled-input-clear-icon-size: 14px !default; +$bigger-medium-filled-input-clear-icon-size: 18px !default; +$medium-filled-input-clear-icon-size: 20px !default; $filled-input-icon-size: 16px !default; $bigger-filled-input-icon-size: 20px !default; -$bigger-small-filled-input-icon-size: 18px !default; -$small-filled-input-icon-size: 14px !default; +$bigger-medium-filled-input-icon-size: 18px !default; +$medium-filled-input-icon-size: 20px !default; $filled-float-input-wrapper-padding: 3px 10px $zero-value 12px !default; $bigger-filled-float-input-wrapper-padding: $zero-value 12px $zero-value 16px !default; -$bigger-small-filled-float-input-wrapper-padding: $zero-value 10px $zero-value 12px !default; -$small-filled-float-input-wrapper-padding: $zero-value 6px $zero-value 8px !default; +$bigger-medium-filled-float-input-wrapper-padding: $zero-value 10px $zero-value 12px !default; +$medium-filled-float-input-wrapper-padding: $zero-value $zero-value $zero-value 16px !default; $filled-float-input-wrapper-rtl-padding: $zero-value 12px $zero-value 10px !default; $bigger-filled-float-input-wrapper-rtl-padding: $zero-value 16px $zero-value 12px !default; -$bigger-small-filled-float-input-wrapper-rtl-padding: $zero-value 12px $zero-value 10px !default; -$small-filled-float-input-wrapper-rtl-padding: $zero-value 8px $zero-value 6px !default; +$bigger-medium-filled-float-input-wrapper-rtl-padding: $zero-value 12px $zero-value 10px !default; +$medium-filled-float-input-wrapper-rtl-padding: $zero-value 16px $zero-value $zero-value !default; $filled-wrapper-padding: $zero-value 10px $zero-value 12px !default; $bigger-filled-wrapper-padding: $zero-value 12px $zero-value 16px !default; -$bigger-small-filled-wrapper-padding: $zero-value 10px $zero-value 12px !default; -$small-filled-wrapper-padding: $zero-value 6px $zero-value 8px !default; +$bigger-medium-filled-wrapper-padding: $zero-value 10px $zero-value 12px !default; +$medium-filled-wrapper-padding: $zero-value $zero-value $zero-value 16px !default; $filled-wrapper-rtl-padding: $zero-value 12px $zero-value 10px !default; $bigger-filled-wrapper-rtl-padding: $zero-value 16px $zero-value 12px !default; -$bigger-small-filled-wrapper-rtl-padding: $zero-value 12px $zero-value 10px !default; -$small-filled-wrapper-rtl-padding: $zero-value 8px $zero-value 6px !default; +$bigger-medium-filled-wrapper-rtl-padding: $zero-value 12px $zero-value 10px !default; +$medium-filled-wrapper-rtl-padding: $zero-value 16px $zero-value $zero-value !default; $filled-wrapper-margin: $zero-value !default; $filled-input-padding: 10px 2px 2px $zero-value !default; $filled-bigger-input-padding: 16px 4px 16px $zero-value !default; -$filled-bigger-small-input-padding: 10px 2px 10px $zero-value !default; -$filled-small-input-padding: 10px 2px 2px $zero-value !default; +$filled-bigger-medium-input-padding: 10px 2px 10px $zero-value !default; +$filled-medium-input-padding: 26px $zero-value 9px $zero-value !default; $filled-input-rtl-padding: 10px $zero-value 10px 2px !default; $filled-bigger-input-rtl-padding: 16px $zero-value 16px 4px !default; -$filled-bigger-small-input-rtl-padding: 10px $zero-value 10px 2px !default; -$filled-small-input-rtl-padding: 7px $zero-value 7px 2px !default; +$filled-bigger-medium-input-rtl-padding: 10px $zero-value 10px 2px !default; +$filled-medium-input-rtl-padding: 26px $zero-value 10px $zero-value !default; $filled-input-padding-bottom: 10px !default; $filled-bigger-input-padding-bottom: 16px !default; -$filled-bigger-small-input-padding-bottom: 10px !default; -$filled-small-input-padding-bottom: 7px !default; +$filled-bigger-medium-input-padding-bottom: 10px !default; +$filled-medium-input-padding-bottom: 10px !default; $filled-float-input-padding: 14px 2px 5px $zero-value !default; $filled-bigger-float-input-padding: 20px 4px 6px $zero-value !default; -$filled-bigger-small-float-input-padding: 14px 2px 5px $zero-value !default; -$filled-small-float-input-padding: 12px 2px 4px $zero-value !default; +$filled-bigger-medium-float-input-padding: 14px 2px 5px $zero-value !default; +$filled-medium-float-input-padding: 26px $zero-value 10px $zero-value !default; $filled-float-input-rtl-padding: 14px $zero-value 5px 2px !default; $filled-bigger-float-input-rtl-padding: 20px $zero-value 6px 4px !default; -$filled-bigger-small-float-input-rtl-padding: 14px $zero-value 5px 2px !default; -$filled-small-float-input-rtl-padding: 12px $zero-value 4px 2px !default; +$filled-bigger-medium-float-input-rtl-padding: 14px $zero-value 5px 2px !default; +$filled-medium-float-input-rtl-padding: 26px $zero-value 10px $zero-value !default; $filled-textarea-padding: $zero-value 12px 9px !default; $filled-bigger-textarea-padding: $zero-value 16px 8px !default; -$filled-bigger-small-textarea-padding: $zero-value 12px 10px !default; -$filled-small-textarea-padding: $zero-value 8px 7px !default; +$filled-bigger-medium-textarea-padding: $zero-value 12px 10px !default; +$filled-medium-textarea-padding: $zero-value 16px 10px 16px !default; $filled-float-textarea-padding: 4px 12px 5px !default; $filled-bigger-float-textarea-padding: $zero-value 16px 8px !default; -$filled-bigger-small-float-textarea-padding: $zero-value 12px 5px !default; -$filled-small-float-textarea-padding: $zero-value 8px 4px !default; +$filled-bigger-medium-float-textarea-padding: $zero-value 12px 5px !default; +$filled-medium-float-textarea-padding: $zero-value 16px 10px 16px !default; $filled-textarea-wrapper-padding: 10px $zero-value $zero-value !default; $bigger-filled-textarea-wrapper-padding: 16px $zero-value $zero-value !default; -$bigger-small-filled-textarea-wrapper-padding: 10px $zero-value $zero-value !default; -$small-filled-textarea-wrapper-padding: 7px $zero-value $zero-value !default; +$bigger-medium-filled-textarea-wrapper-padding: 10px $zero-value $zero-value !default; +$medium-filled-textarea-wrapper-padding: 10px $zero-value $zero-value !default; $filled-float-textarea-wrapper-padding: 14px $zero-value $zero-value !default; $bigger-filled-float-textarea-wrapper-padding: 20px $zero-value $zero-value !default; -$bigger-small-filled-float-textarea-wrapper-padding: 14px $zero-value $zero-value !default; -$small-filled-float-textarea-wrapper-padding: 12px $zero-value $zero-value !default; +$bigger-medium-filled-float-textarea-wrapper-padding: 14px $zero-value $zero-value !default; +$medium-filled-float-textarea-wrapper-padding: 26px $zero-value $zero-value !default; $filled-input-label-top: 13px !default; $bigger-filled-input-label-top: 18px !default; -$bigger-small-filled-input-label-top: 12px !default; -$small-filled-input-label-top: 10px !default; +$bigger-medium-filled-input-label-top: 12px !default; +$medium-filled-input-label-top: 26px !default; $filled-input-label-top-after-floating: 10px !default; $bigger-filled-input-label-top-after-floating: 18px !default; -$bigger-small-filled-input-label-top-after-floating: 12px !default; -$small-filled-input-label-top-after-floating: 10px !default; +$bigger-medium-filled-input-label-top-after-floating: 12px !default; +$medium-filled-input-label-top-after-floating: 26px !default; $filled-input-label-left: 12px !default; $bigger-filled-input-label-left: 16px !default; -$bigger-small-filled-input-label-left: 12px !default; -$small-filled-input-label-left: 8px !default; +$bigger-medium-filled-input-label-left: 12px !default; +$medium-filled-input-label-left: 16px !default; $filled-input-label-initial-transform: none !default; $filled-input-label-line-height: 1.2 !default; $bigger-filled-input-label-line-height: 1.25 !default; -$small-filled-input-label-line-height: 1.2 !default; -$bigger-small-filled-input-label-line-height: 1.25 !default; +$medium-filled-input-label-line-height: 1.2 !default; +$bigger-medium-filled-input-label-line-height: 1.25 !default; $filled-input-label-width: auto !default; $filled-default-input-min-height: 39px !default; $bigger-filled-default-input-min-height: 55px !default; -$bigger-small-filled-default-input-min-height: 39px !default; -$small-filled-default-input-min-height: 35px !default; +$bigger-medium-filled-default-input-min-height: 39px !default; +$medium-filled-default-input-min-height: 55px !default; $filled-input-min-height: 40px !default; $bigger-filled-input-min-height: 56px !default; -$bigger-small-filled-input-min-height: 39px !default; -$small-filled-input-min-height: 32px !default; +$bigger-medium-filled-input-min-height: 39px !default; +$medium-filled-input-min-height: 52px !default; $filled-input-icon-min-height: 32px !default; $bigger-filled-input-icon-min-height: 40px !default; -$bigger-small-filled-input-icon-min-height: 20px !default; -$small-filled-input-icon-min-height: 32px !default; +$bigger-medium-filled-input-icon-min-height: 20px !default; +$medium-filled-input-icon-min-height: 48px !default; $filled-input-left-icon-min-height: 16px !default; $bigger-filled-input-left-icon-min-height: 20px !default; -$bigger-small-filled-input-left-icon-min-height: 20px !default; -$small-filled-input-left-icon-min-height: 16px !default; +$bigger-medium-filled-input-left-icon-min-height: 20px !default; +$medium-filled-input-left-icon-min-height: 48px !default; $filled-input-left-icon-min-width: 16px !default; $bigger-filled-input-left-icon-min-width: 20px !default; -$bigger-small-filled-input-left-icon-min-width: 20px !default; -$small-filled-input-left-icon-min-width: 16px !default; -$filled-input-bg-color: rgba($series-1) !default; -$filled-input-overlay-bg-color: rgba($series-1) !default; -$filled-input-overlay-focused-bg-color: rgba($series-1) !default; -$filled-input-overlay-activated-bg-color: rgba($series-1) !default; +$bigger-medium-filled-input-left-icon-min-width: 20px !default; +$medium-filled-input-left-icon-min-width: 48px !default; +$filled-input-bg-color: rgb($series-1) !default; +$filled-input-overlay-bg-color: rgb($series-1) !default; +$filled-input-overlay-focused-bg-color: rgb($series-1) !default; +$filled-input-overlay-activated-bg-color: rgb($series-1) !default; $background-color-transition: opacity 15ms linear, background-color 15ms linear !default; -$filled-input-hover-border-color: rgba($content-text-color) !default; -$filled-input-float-label-color: rgba($placeholder-text-color) !default; -$filled-input-disabled-font-color: rgba($content-text-color) !default; -$filled-input-disabled-bg-color: rgba($series-1, .38) !default; -$filled-input-disabled-border-color: rgba($border) !default; -$filled-input-clear-icon-hover-color: rgba($icon-color-hover) !default; -$filled-input-clear-icon-active-color: rgba($icon-color) !default; - -.sf-float-input.sf-outline.sf-float-icon-left:not(.sf-rtl) .sf-input-in-wrap input ~ label.sf-float-text.sf-label-top { +$filled-input-hover-border-color: rgb($content-text-color) !default; +$filled-input-float-label-color: rgb($placeholder-text-color) !default; +$filled-input-disabled-font-color: rgb($content-text-color) !default; +$filled-input-disabled-bg-color: rgb($series-1, .38) !default; +$filled-input-disabled-border-color: rgb($border) !default; +$filled-input-clear-icon-hover-color: rgb($icon-color-hover) !default; +$filled-input-clear-icon-active-color: rgb($icon-color) !default; + +.sf-float-input.sf-outline.sf-float-icon-left:not(.sf-rtl) .sf-input-in-wrap input~label.sf-float-text.sf-label-top { left: -34px; width: auto; } -.sf-float-input.sf-outline.sf-float-icon-left.sf-rtl .sf-input-in-wrap input ~ label.sf-float-text.sf-label-top { +.sf-float-input.sf-outline.sf-float-icon-left.sf-rtl .sf-input-in-wrap input~label.sf-float-text.sf-label-top { right: -34px; width: auto; } @@ -563,18 +583,20 @@ $filled-input-clear-icon-active-color: rgba($icon-color) !default; .sf-input-group.sf-control-wrapper.sf-success:not(.sf-float-icon-left), .sf-input-group.sf-control-wrapper.sf-warning:not(.sf-float-icon-left), .sf-input-group.sf-control-wrapper.sf-error:not(.sf-float-icon-left) { - border: $input-group-full-border; + border-style: solid; border-width: $input-group-full-border-width; padding-top: 1px; } [class*="e-input-focus"] { + &.sf-input-group, &.sf-input-group.sf-control-wrapper, &.sf-float-input.sf-input-group, &.sf-float-input.sf-control-wrapper.sf-input-group, &.sf-float-input:not(.sf-input-group):not(.sf-disabled), &.sf-float-input.sf-control-wrapper:not(.sf-input-group):not(.sf-disabled) { + &:not(.sf-float-icon-left):not(.sf-success):not(.sf-warning):not(.sf-error), &:not(.sf-float-icon-left).sf-success:not(.sf-warning):not(.sf-error), &:not(.sf-float-icon-left):not(.sf-success).sf-warning:not(.sf-error), @@ -588,11 +610,14 @@ $filled-input-clear-icon-active-color: rgba($icon-color) !default; &.sf-float-input.sf-control-wrapper:not(.sf-input-group):not(.sf-float-icon-left):not(.sf-disabled), &.sf-float-input:not(.sf-input-group):not(.sf-float-icon-left):not(.sf-disabled) { + &:not(.sf-success):not(.sf-warning):not(.sf-error), &.sf-success:not(.sf-warning):not(.sf-error), &:not(.sf-success).sf-warning:not(.sf-error), &:not(.sf-success):not(.sf-warning).sf-error { - input, textarea { + + input, + textarea { border-style: $input-group-border-type-focus; border-width: $zero-value $zero-value 1px; } @@ -600,6 +625,7 @@ $filled-input-clear-icon-active-color: rgba($icon-color) !default; } &.sf-input-group.sf-float-icon-left { + &:not(.sf-success):not(.sf-warning):not(.sf-error), &.sf-success:not(.sf-warning):not(.sf-error), &.sf-warning:not(.sf-success):not(.sf-error), @@ -830,18 +856,18 @@ input.sf-outline.sf-input, margin-left: $zero-value; } -.sf-outline.sf-input-group.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-float-input.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon { +.sf-outline.sf-input-group.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-float-input.sf-input-group.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-input-group.sf-control-wrapper.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-float-input.sf-input-group.sf-control-wrapper.sf-float-icon-left>.sf-input-group-icon { margin-left: 4px; margin-right: $zero-value; } -.sf-outline.sf-input-group.sf-rtl.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-float-input.sf-rtl.sf-input-group.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-input-group.sf-rtl.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-float-input.sf-input-group.sf-rtl.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon { +.sf-outline.sf-input-group.sf-rtl.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-float-input.sf-rtl.sf-input-group.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-input-group.sf-rtl.sf-control-wrapper.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-float-input.sf-input-group.sf-rtl.sf-control-wrapper.sf-float-icon-left>.sf-input-group-icon { margin-right: 4px; margin-left: $zero-value; } @@ -892,30 +918,30 @@ input.sf-outline.sf-input, border: $zero-value; } -.sf-outline.sf-input-group .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-outline.sf-input-group.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child { +.sf-outline.sf-input-group .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-outline.sf-input-group.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child { margin-left: $zero-value; } -.sf-outline.sf-input-group.sf-small:not(.sf-float-input) .sf-input, -.sf-small .sf-outline.sf-input-group:not(.sf-float-input) .sf-input, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small:not(.sf-float-input) .sf-input, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper:not(.sf-float-input) .sf-input, -.sf-outline.sf-float-input.sf-small input, -.sf-small .sf-outline.sf-float-input input, -.sf-outline.sf-float-input.sf-input-group.sf-small input, -.sf-small .sf-outline.sf-float-input.sf-input-group input, -.sf-outline.sf-float-input.sf-input-group.sf-control-wrapper.sf-small input, -.sf-small .sf-outline.sf-float-input.sf-input-group.sf-control-wrapper input, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small input, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper input, -.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-small input, -.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-small input, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-input-group input { +.sf-outline.sf-input-group.sf-medium:not(.sf-float-input) .sf-input, +.sf-medium .sf-outline.sf-input-group:not(.sf-float-input) .sf-input, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium:not(.sf-float-input) .sf-input, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper:not(.sf-float-input) .sf-input, +.sf-outline.sf-float-input.sf-medium input, +.sf-medium .sf-outline.sf-float-input input, +.sf-outline.sf-float-input.sf-input-group.sf-medium input, +.sf-medium .sf-outline.sf-float-input.sf-input-group input, +.sf-outline.sf-float-input.sf-input-group.sf-control-wrapper.sf-medium input, +.sf-medium .sf-outline.sf-float-input.sf-input-group.sf-control-wrapper input, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium input, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper input, +.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium input, +.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium input, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-input-group input { box-sizing: border-box; - min-height: $outline-small-input-min-height; + min-height: $outline-medium-input-min-height; } .sf-outline.sf-input-group, @@ -925,132 +951,132 @@ input.sf-outline.sf-input, font-size: $outline-valid-input-font-size; } -.sf-outline.sf-input-group.sf-small, -.sf-small .sf-outline.sf-input-group, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper, -.sf-outline.sf-float-input.sf-small, -.sf-small .sf-outline.sf-float-input, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper { - font-size: $outline-small-input-font-size; -} - -input.sf-input.sf-small.sf-outline, -.sf-small input.sf-input.sf-outline, -.sf-input-group.sf-small.sf-outline input.sf-input, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small input.sf-input, -.sf-outline.sf-float-input.sf-small input, -.sf-outline.sf-float-input.sf-control-wrapper input.sf-small, -.sf-outline.sf-float-input.sf-small input, -.sf-outline.sf-float-input.sf-control-wrapper input.sf-small, -.sf-outline.sf-input-group input.sf-input.sf-small, -.sf-outline.sf-input-group.sf-control-wrapper input.sf-input.sf-small, -.sf-small .sf-outline.sf-float-input input, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper input, -.sf-small .sf-outline.sf-input-group input.sf-input, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper input.sf-input, -.sf-outline.sf-input-group.sf-small input.sf-input:focus, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small input.sf-input:focus, -.sf-outline.sf-float-input.sf-small input:focus, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small input:focus, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper input.sf-input:focus, -.sf-small .sf-outline.sf-input-group input.sf-input:focus, -.sf-small .sf-outline.sf-float-input input:focus, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper input:focus, -.sf-outline.sf-float-input.sf-small.sf-input-focus input, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-input-focus input, -.sf-small .sf-outline.sf-float-input.sf-input-focus input, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-input-focus input, -.sf-outline.sf-input-group.sf-small.sf-input-focus input.sf-input, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small.sf-input-focus input.sf-input, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input, -.sf-small .sf-outline.sf-input-group.sf-input-focus input.sf-input { - padding: $outline-small-input-padding-top $outline-small-input-padding-left; -} - -textarea.sf-input.sf-small.sf-outline, -.sf-small textarea.sf-input.sf-outline, -.sf-input-group.sf-small.sf-outline textarea.sf-input, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small textarea.sf-input, -.sf-outline.sf-float-input.sf-control-wrapper textarea.sf-small, -.sf-outline.sf-float-input.sf-small textarea, -.sf-outline.sf-input-group textarea.sf-input.sf-small, -.sf-outline.sf-input-group.sf-control-wrapper textarea.sf-input.sf-small, -.sf-small .sf-outline.sf-float-input textarea, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper textarea, -.sf-small .sf-outline.sf-input-group textarea.sf-input, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper textarea.sf-input, -.sf-outline.sf-input-group.sf-small textarea.sf-input:focus, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small textarea.sf-input:focus, -.sf-outline.sf-float-input.sf-small textarea:focus, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea:focus, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper textarea.sf-input:focus, -.sf-small .sf-outline.sf-input-group textarea.sf-input:focus, -.sf-small .sf-outline.sf-float-input textarea:focus, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper textarea:focus { +.sf-outline.sf-input-group.sf-medium, +.sf-medium .sf-outline.sf-input-group, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper, +.sf-outline.sf-float-input.sf-medium, +.sf-medium .sf-outline.sf-float-input, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper { + font-size: $outline-medium-input-font-size; +} + +input.sf-input.sf-medium.sf-outline, +.sf-medium input.sf-input.sf-outline, +.sf-input-group.sf-medium.sf-outline input.sf-input, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium input.sf-input, +.sf-outline.sf-float-input.sf-medium input, +.sf-outline.sf-float-input.sf-control-wrapper input.sf-medium, +.sf-outline.sf-float-input.sf-medium input, +.sf-outline.sf-float-input.sf-control-wrapper input.sf-medium, +.sf-outline.sf-input-group input.sf-input.sf-medium, +.sf-outline.sf-input-group.sf-control-wrapper input.sf-input.sf-medium, +.sf-medium .sf-outline.sf-float-input input, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper input, +.sf-medium .sf-outline.sf-input-group input.sf-input, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper input.sf-input, +.sf-outline.sf-input-group.sf-medium input.sf-input:focus, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium input.sf-input:focus, +.sf-outline.sf-float-input.sf-medium input:focus, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium input:focus, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper input.sf-input:focus, +.sf-medium .sf-outline.sf-input-group input.sf-input:focus, +.sf-medium .sf-outline.sf-float-input input:focus, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper input:focus, +.sf-outline.sf-float-input.sf-medium.sf-input-focus input, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-input-focus input, +.sf-medium .sf-outline.sf-float-input.sf-input-focus input, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-input-focus input, +.sf-outline.sf-input-group.sf-medium.sf-input-focus input.sf-input, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium.sf-input-focus input.sf-input, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper.sf-input-focus input.sf-input, +.sf-medium .sf-outline.sf-input-group.sf-input-focus input.sf-input { + padding: 14px $outline-medium-input-padding-top 15px $outline-medium-input-padding-left; +} + +textarea.sf-input.sf-medium.sf-outline, +.sf-medium textarea.sf-input.sf-outline, +.sf-input-group.sf-medium.sf-outline textarea.sf-input, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium textarea.sf-input, +.sf-outline.sf-float-input.sf-control-wrapper textarea.sf-medium, +.sf-outline.sf-float-input.sf-medium textarea, +.sf-outline.sf-input-group textarea.sf-input.sf-medium, +.sf-outline.sf-input-group.sf-control-wrapper textarea.sf-input.sf-medium, +.sf-medium .sf-outline.sf-float-input textarea, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper textarea, +.sf-medium .sf-outline.sf-input-group textarea.sf-input, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper textarea.sf-input, +.sf-outline.sf-input-group.sf-medium textarea.sf-input:focus, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium textarea.sf-input:focus, +.sf-outline.sf-float-input.sf-medium textarea:focus, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea:focus, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper textarea.sf-input:focus, +.sf-medium .sf-outline.sf-input-group textarea.sf-input:focus, +.sf-medium .sf-outline.sf-float-input textarea:focus, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper textarea:focus { box-sizing: border-box; - margin: $outline-small-textarea-margin-top; - padding: $zero-value $outline-small-input-padding-left $outline-small-input-padding-left; -} - -.sf-outline.sf-input-group.sf-small.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-small, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon.sf-small, -.sf-small .sf-outline.sf-input-group.sf-float-icon-left > .sf-input-group-icon, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-float-input.sf-input-group.sf-small.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-small, -.sf-small .sf-outline.sf-float-input.sf-input-group.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-small.sf-float-icon-left > .sf-input-group-icon, -.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon.sf-small, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left > .sf-input-group-icon { + margin: $outline-medium-textarea-margin-top; + padding: $zero-value 16px 10px 16px; +} + +.sf-outline.sf-input-group.sf-medium.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-input-group.sf-float-icon-left>.sf-input-group-icon.sf-medium, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-input-group.sf-control-wrapper.sf-float-icon-left>.sf-input-group-icon.sf-medium, +.sf-medium .sf-outline.sf-input-group.sf-float-icon-left>.sf-input-group-icon, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-float-input.sf-input-group.sf-medium.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-float-input.sf-input-group.sf-float-icon-left>.sf-input-group-icon.sf-medium, +.sf-medium .sf-outline.sf-float-input.sf-input-group.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium.sf-float-icon-left>.sf-input-group-icon, +.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left>.sf-input-group-icon.sf-medium, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-float-icon-left>.sf-input-group-icon { margin-left: $zero-value; - margin-right: $outline-small-input-icon-margin-right; + margin-right: $outline-medium-input-icon-margin-right; } -.sf-outline.sf-input-group.sf-small .sf-input-group-icon, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, -.sf-small .sf-outline.sf-input-group .sf-input-group-icon, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon { +.sf-outline.sf-input-group.sf-medium .sf-input-group-icon, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, +.sf-medium .sf-outline.sf-input-group .sf-input-group-icon, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon { margin-left: $zero-value; - margin-right: $outline-small-input-icon-margin-left; -} - -.sf-outline.sf-input-group.sf-prepend.sf-small .sf-input-group-icon, -.sf-outline.sf-input-group.sf-prepend.sf-control-wrapper.sf-small .sf-input-group-icon, -.sf-small .sf-outline.sf-input-group.sf-prepend .sf-input-group-icon, -.sf-small .sf-outline.sf-input-group.sf-prepend.sf-control-wrapper .sf-input-group-icon, -.sf-rtl.sf-outline.sf-input-group.sf-small .sf-input-group-icon, -.sf-rtl .sf-outline.sf-input-group.sf-small .sf-input-group-icon, -.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, -.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon, -.sf-small .sf-rtl.sf-outline.sf-input-group .sf-input-group-icon, -.sf-rtl.sf-small .sf-outline.sf-input-group .sf-input-group-icon, -.sf-small .sf-rtl.sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon .sf-rtl.sf-small .sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon, -.sf-rtl.sf-outline.sf-input-group.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl .sf-outline.sf-input-group.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child { - margin-left: $outline-small-input-icon-margin-left; + margin-right: $outline-medium-input-icon-margin-left; +} + +.sf-outline.sf-input-group.sf-prepend.sf-medium .sf-input-group-icon, +.sf-outline.sf-input-group.sf-prepend.sf-control-wrapper.sf-medium .sf-input-group-icon, +.sf-medium .sf-outline.sf-input-group.sf-prepend .sf-input-group-icon, +.sf-medium .sf-outline.sf-input-group.sf-prepend.sf-control-wrapper .sf-input-group-icon, +.sf-rtl.sf-outline.sf-input-group.sf-medium .sf-input-group-icon, +.sf-rtl .sf-outline.sf-input-group.sf-medium .sf-input-group-icon, +.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, +.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon, +.sf-medium .sf-rtl.sf-outline.sf-input-group .sf-input-group-icon, +.sf-rtl.sf-medium .sf-outline.sf-input-group .sf-input-group-icon, +.sf-medium .sf-rtl.sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon .sf-rtl.sf-medium .sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon, +.sf-rtl.sf-outline.sf-input-group.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl .sf-outline.sf-input-group.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child { + margin-left: $outline-medium-input-icon-margin-left; margin-right: $zero-value; } -.sf-outline.sf-input-group.sf-small .sf-clear-icon, -.sf-outline.sf-input-group .sf-clear-icon.sf-small, -.sf-small .sf-outline.sf-input-group .sf-clear-icon, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, -.sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon.sf-small, -.sf-small .sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon { - font-size: $outline-small-clear-icon-font-size; +.sf-outline.sf-input-group.sf-medium .sf-clear-icon, +.sf-outline.sf-input-group .sf-clear-icon.sf-medium, +.sf-medium .sf-outline.sf-input-group .sf-clear-icon, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon, +.sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon.sf-medium, +.sf-medium .sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon { + font-size: $outline-medium-clear-icon-font-size; } -.sf-outline.sf-float-input.sf-input-group.sf-small .sf-input-group-icon, -.sf-small .sf-outline.sf-float-input.sf-input-group .sf-input-group-icon, -.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-input-group-icon, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { - margin-top: $outline-bigger-small-group-icon-top; +.sf-outline.sf-float-input.sf-input-group.sf-medium .sf-input-group-icon, +.sf-medium .sf-outline.sf-float-input.sf-input-group .sf-input-group-icon, +.sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-input-group-icon, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-input-group .sf-input-group-icon { + margin-top: $outline-bigger-medium-group-icon-top; } .sf-outline.sf-float-input input, @@ -1064,20 +1090,20 @@ textarea.sf-input.sf-small.sf-outline, .sf-outline label.sf-float-text, .sf-outline.sf-float-input label.sf-float-text, .sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text, -.sf-outline.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom { +.sf-outline.sf-float-input:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-medium:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-medium textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom { box-sizing: border-box; display: flex; left: $zero-value; @@ -1088,63 +1114,63 @@ textarea.sf-input.sf-small.sf-outline, transition: color .2s, font-size .2s, line-height .2s; } -.sf-outline.sf-float-input.sf-small:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small.sf-outline:not(.sf-valid-input):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid) ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text { - line-height: $outline-small-label-line-height; -} - -.sf-outline.sf-float-input.sf-small:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:valid ~ label.sf-float-text.sf-label-bottom, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:valid ~ label.sf-float-text.sf-label-bottom, -.sf-small.sf-outline:not(.sf-valid-input):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid) ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-small:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small.sf-outline:not(.sf-valid-input):not(.sf-valid-input) textarea:not(:focus):not(:valid) ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input:not(.sf-valid-input) textarea:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input) textarea:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text { - line-height: $outline-textarea-small-label-line-height; -} - -.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) input:valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) input:valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline:not(.sf-valid-input):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid) ~ label.sf-float-text, -.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text { +.sf-outline.sf-float-input.sf-medium:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium.sf-outline:not(.sf-valid-input):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid)~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text { + line-height: $outline-medium-label-line-height; +} + +.sf-outline.sf-float-input.sf-medium:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:valid~label.sf-float-text.sf-label-bottom, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:valid~label.sf-float-text.sf-label-bottom, +.sf-medium.sf-outline:not(.sf-valid-input):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid)~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text, +.sf-outline.sf-float-input.sf-medium:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium.sf-outline:not(.sf-valid-input):not(.sf-valid-input) textarea:not(:focus):not(:valid)~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input:not(.sf-valid-input) textarea:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input) textarea:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text { + line-height: $outline-textarea-medium-label-line-height; +} + +.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) input:valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) input:valid~label.sf-float-text.sf-label-bottom, +.sf-outline:not(.sf-valid-input):not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid)~label.sf-float-text, +.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) input:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text { line-height: $outline-label-line-height; } -.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline:not(.sf-valid-input):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid) ~ label.sf-float-text, -.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text, -.sf-outline.sf-float-input:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline:not(.sf-valid-input):not(.sf-valid-input) textarea:not(:focus):not(:valid) ~ label.sf-float-text, -.sf-outline.sf-float-input:not(.sf-valid-input) textarea:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input) textarea:not(:focus):not(:valid):not(.sf-valid-input) ~ label.sf-float-text { +.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:valid~label.sf-float-text.sf-label-bottom, +.sf-outline:not(.sf-valid-input):not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid)~label.sf-float-text, +.sf-outline.sf-float-input:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input):not(.sf-input-focus) textarea:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text, +.sf-outline.sf-float-input:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error):not(.sf-valid-input) textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline:not(.sf-valid-input):not(.sf-valid-input) textarea:not(:focus):not(:valid)~label.sf-float-text, +.sf-outline.sf-float-input:not(.sf-valid-input) textarea:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-valid-input) textarea:not(:focus):not(:valid):not(.sf-valid-input)~label.sf-float-text { line-height: $outline-textarea-label-line-height; } @@ -1177,19 +1203,19 @@ textarea.sf-input.sf-small.sf-outline, transition: none; } -.sf-small.sf-outline label.sf-float-text::before, -.sf-small.sf-outline label.sf-float-text::after, -.sf-small.sf-outline.sf-float-input label.sf-float-text::before, -.sf-small.sf-outline.sf-float-input label.sf-float-text::after, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text::before, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text::after, -.sf-small .sf-outline label.sf-float-text::before, -.sf-small .sf-outline label.sf-float-text::after, -.sf-small .sf-outline.sf-float-input label.sf-float-text::before, -.sf-small .sf-outline.sf-float-input label.sf-float-text::after, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text::before, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text::after { - min-width: $outline-small-label-min-width; +.sf-medium.sf-outline label.sf-float-text::before, +.sf-medium.sf-outline label.sf-float-text::after, +.sf-medium.sf-outline.sf-float-input label.sf-float-text::before, +.sf-medium.sf-outline.sf-float-input label.sf-float-text::after, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text::before, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text::after, +.sf-medium .sf-outline label.sf-float-text::before, +.sf-medium .sf-outline label.sf-float-text::after, +.sf-medium .sf-outline.sf-float-input label.sf-float-text::before, +.sf-medium .sf-outline.sf-float-input label.sf-float-text::after, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text::before, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text::after { + min-width: $outline-medium-label-min-width; } .sf-outline.sf-valid-input:not(.sf-input-focus) label.sf-float-text::before, @@ -1316,32 +1342,32 @@ textarea.sf-input.sf-small.sf-outline, box-shadow: inset 1px $zero-value $input-active-border-color, inset -1px $zero-value $input-active-border-color, inset $zero-value -1px $input-active-border-color; } -.sf-outline.sf-float-input input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input input:valid ~ label.sf-float-text, -.sf-outline.sf-float-input input ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input input[readonly] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input input[disabled] ~ label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input input:focus~label.sf-float-text, +.sf-outline.sf-float-input input:valid~label.sf-float-text, +.sf-outline.sf-float-input input~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input input[readonly]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input input[disabled]~label.sf-label-top.sf-float-text, .sf-outline.sf-float-input input label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper input:valid ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper input ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper input[readonly] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper input[disabled] ~ label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper input:valid~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper input~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper input[readonly]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper input[disabled]~label.sf-label-top.sf-float-text, .sf-outline.sf-float-input.sf-control-wrapper input label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input textarea:valid ~ label.sf-float-text, -.sf-outline.sf-float-input textarea ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input textarea[readonly] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input textarea[disabled] ~ label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input textarea:valid~label.sf-float-text, +.sf-outline.sf-float-input textarea~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input textarea[readonly]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input textarea[disabled]~label.sf-label-top.sf-float-text, .sf-outline.sf-float-input textarea label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper textarea:valid ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper textarea ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper textarea[readonly] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper textarea:valid~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper textarea~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper textarea[readonly]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper textarea[disabled]~label.sf-label-top.sf-float-text, .sf-outline.sf-float-input.sf-control-wrapper textarea label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-input-focus input ~ label.sf-float-text { +.sf-outline.sf-float-input.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-input-focus input~label.sf-float-text { font-size: $outline-floating-label-font-size; top: $outline-float-label-top; transform: translate3d($zero-value, $zero-value, $zero-value) scale(1); @@ -1359,14 +1385,14 @@ e-rtl .sf-outline.sf-float-input.sf-control-wrapper .sf-clear-icon, padding-right: $outline-input-icon-margin-left; } -.sf-rtl .sf-outline.sf-input-group .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl.sf-outline.sf-input-group .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl.sf-outline.sf-input-group.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl .sf-outline.sf-input-group.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-input-group-icon + .sf-input-group-icon:last-child, +.sf-rtl .sf-outline.sf-input-group .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl.sf-outline.sf-input-group .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl.sf-outline.sf-input-group.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl .sf-outline.sf-input-group.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-input-group-icon+.sf-input-group-icon:last-child, .sf-outline.sf-float-input.sf-rtl.sf-float-icon-left .sf-input-in-wrap, .sf-outline.sf-float-input.sf-rtl.sf-control-wrapper.sf-float-icon-left .sf-input-in-wrap, .sf-outline.sf-input-group.sf-rtl.sf-float-icon-left .sf-input-in-wrap, @@ -1374,8 +1400,8 @@ e-rtl .sf-outline.sf-float-input.sf-control-wrapper .sf-clear-icon, margin-right: $zero-value; } -.sf-rtl .sf-outline.sf-input-group .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl.sf-outline.sf-input-group .sf-input-group-icon + .sf-input-group-icon:last-child { +.sf-rtl .sf-outline.sf-input-group .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl.sf-outline.sf-input-group .sf-input-group-icon+.sf-input-group-icon:last-child { margin-left: $outline-input-icon-margin-right; } @@ -1386,25 +1412,25 @@ e-rtl .sf-outline.sf-float-input.sf-control-wrapper .sf-clear-icon, margin-right: $outline-input-icon-margin-left; } -.sf-rtl.sf-outline.sf-input-group.sf-small .sf-clear-icon, -.sf-rtl .sf-outline.sf-input-group.sf-small .sf-clear-icon, -.sf-rtl.sf-outline.sf-input-group .sf-clear-icon.sf-small, -.sf-rtl .sf-outline.sf-input-group .sf-clear-icon.sf-small, -.sf-rtl.sf-small .sf-outline.sf-input-group .sf-clear-icon, -.sf-small .sf-rtl.sf-outline.sf-input-group .sf-clear-icon, -.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, -.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, -.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon.sf-small, -.sf-small .sf-rtl.sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon .sf-rtl.sf-small .sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon { - padding-left: $outline-small-input-icon-margin-left; +.sf-rtl.sf-outline.sf-input-group.sf-medium .sf-clear-icon, +.sf-rtl .sf-outline.sf-input-group.sf-medium .sf-clear-icon, +.sf-rtl.sf-outline.sf-input-group .sf-clear-icon.sf-medium, +.sf-rtl .sf-outline.sf-input-group .sf-clear-icon.sf-medium, +.sf-rtl.sf-medium .sf-outline.sf-input-group .sf-clear-icon, +.sf-medium .sf-rtl.sf-outline.sf-input-group .sf-clear-icon, +.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon, +.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon, +.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon.sf-medium, +.sf-medium .sf-rtl.sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon .sf-rtl.sf-medium .sf-outline.sf-input-group.sf-control-wrapper .sf-clear-icon { + padding-left: $outline-medium-input-icon-margin-left; padding-right: $zero-value; } -.sf-rtl .sf-outline.sf-input-group .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl.sf-outline.sf-input-group .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon + .sf-input-group-icon:last-child, -.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon + .sf-input-group-icon:last-child { +.sf-rtl .sf-outline.sf-input-group .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl.sf-outline.sf-input-group .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl .sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon+.sf-input-group-icon:last-child, +.sf-rtl.sf-outline.sf-input-group.sf-control-wrapper .sf-input-group-icon+.sf-input-group-icon:last-child { margin-left: $outline-input-icon-margin-right; margin-right: $zero-value; } @@ -1417,35 +1443,35 @@ textarea.sf-outline, box-sizing: border-box; } -.sf-outline.sf-float-input.sf-valid-input:not(.sf-input-focus) input:valid ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input:not(.sf-input-focus) input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-valid-input:not(.sf-input-focus) textarea:valid ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input:not(.sf-input-focus) textarea:focus ~ label.sf-float-text { +.sf-outline.sf-float-input.sf-valid-input:not(.sf-input-focus) input:valid~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input:not(.sf-input-focus) input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-valid-input:not(.sf-input-focus) textarea:valid~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input:not(.sf-input-focus) textarea:focus~label.sf-float-text { color: $outline-label-font-color-with-value; } -.sf-rtl.sf-outline.sf-float-input.sf-small textarea ~ label.sf-float-text, -.sf-rtl.sf-outline.sf-float-input textarea ~ label.sf-float-text.sf-small, -.sf-rtl.sf-outline.sf-float-input textarea.sf-small ~ label.sf-float-text, -.sf-small .sf-rtl.sf-outline.sf-float-input textarea ~ label.sf-float-text, -.sf-rtl.sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea ~ label.sf-float-text, -.sf-rtl.sf-outline.sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text.sf-small, -.sf-rtl.sf-outline.sf-float-input.sf-control-wrapper textarea.sf-small ~ label.sf-float-text, -.sf-small .sf-rtl.sf-outline.sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text { +.sf-rtl.sf-outline.sf-float-input.sf-medium textarea~label.sf-float-text, +.sf-rtl.sf-outline.sf-float-input textarea~label.sf-float-text.sf-medium, +.sf-rtl.sf-outline.sf-float-input textarea.sf-medium~label.sf-float-text, +.sf-medium .sf-rtl.sf-outline.sf-float-input textarea~label.sf-float-text, +.sf-rtl.sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea~label.sf-float-text, +.sf-rtl.sf-outline.sf-float-input.sf-control-wrapper textarea~label.sf-float-text.sf-medium, +.sf-rtl.sf-outline.sf-float-input.sf-control-wrapper textarea.sf-medium~label.sf-float-text, +.sf-medium .sf-rtl.sf-outline.sf-float-input.sf-control-wrapper textarea~label.sf-float-text { top: $outline-float-label-top; } -.sf-outline.sf-float-input.sf-small .sf-clear-icon::before, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small .sf-clear-icon::before, -.sf-outline.sf-input-group.sf-small .sf-clear-icon::before, -.sf-outline.sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon::before, -.sf-outline.sf-float-input.sf-control-wrapper input.sf-small:first-child ~ .sf-clear-icon::before, -.sf-outline.sf-small .sf-float-input.sf-control-wrapper .sf-clear-icon::before, -.sf-outline.sf-float-input input.sf-small:first-child ~ .sf-clear-icon::before, -.sf-outline.sf-small .sf-float-input .sf-clear-icon::before, -.sf-outline.sf-small .sf-input-group .sf-clear-icon::before, -.sf-outline.sf-small .sf-input-group.sf-control-wrapper .sf-clear-icon::before { - font-size: $outline-input-small-clear-icon; +.sf-outline.sf-float-input.sf-medium .sf-clear-icon::before, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium .sf-clear-icon::before, +.sf-outline.sf-input-group.sf-medium .sf-clear-icon::before, +.sf-outline.sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon::before, +.sf-outline.sf-float-input.sf-control-wrapper input.sf-medium:first-child~.sf-clear-icon::before, +.sf-outline.sf-medium .sf-float-input.sf-control-wrapper .sf-clear-icon::before, +.sf-outline.sf-float-input input.sf-medium:first-child~.sf-clear-icon::before, +.sf-outline.sf-medium .sf-float-input .sf-clear-icon::before, +.sf-outline.sf-medium .sf-input-group .sf-clear-icon::before, +.sf-outline.sf-medium .sf-input-group.sf-control-wrapper .sf-clear-icon::before { + font-size: $outline-input-medium-clear-icon; } .sf-outline.sf-float-input .sf-clear-icon::before, @@ -1455,114 +1481,116 @@ textarea.sf-outline, font-size: $outline-input-clear-icon; } -.sf-outline.sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, .sf-outline label.sf-float-text, .sf-outline.sf-float-input label.sf-float-text, .sf-outline.sf-float-input.sf-control-wrapper label.sf-float-text, -.sf-outline.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { +.sf-outline.sf-float-input:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input textarea:-webkit-autofill~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill~label.sf-float-text.sf-label-bottom { font-size: $outline-float-label-font-size; } -.sf-outline.sf-float-input.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input textarea:-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-small label.sf-float-text, -.sf-outline.sf-float-input.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid ~ label.sf-float-text.sf-label-bottom, -.sf-outline.sf-float-input.sf-small textarea ~ label.sf-float-text, -.sf-outline.sf-float-input textarea ~ label.sf-float-text.sf-small, -.sf-outline.sf-float-input textarea.sf-small ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input textarea ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small textarea ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text.sf-small, -.sf-outline.sf-float-input.sf-control-wrapper textarea.sf-small ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper textarea ~ label.sf-float-text { - font-size: $small-outline-float-label-font-size; -} - -.sf-outline.sf-float-input input:-webkit-autofill ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { - animation-name: slideTopUp;/* stylelint-disable-line no-unknown-animations */ -} - -.sf-small .sf-outline.sf-float-input input:-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input input:-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill ~ label.sf-float-text, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom, -.sf-small.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill ~ label.sf-float-text.sf-label-bottom { - animation-name: slideTopUp;/* stylelint-disable-line no-unknown-animations */ -} - -.sf-filled.sf-input-group.sf-float-input.sf-small .sf-clear-icon, -.sf-filled.sf-input-group.sf-float-input .sf-clear-icon.sf-small, -.sf-small .sf-filled.sf-input-group.sf-float-input .sf-clear-icon, -.sf-filled.sf-input-group.sf-control-wrapper.sf-float-input.sf-small .sf-clear-icon, -.sf-filled.sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon.sf-small, -.sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon, -.sf-filled.sf-input-group.sf-float-input.sf-control-wrapper.sf-small .sf-clear-icon, -.sf-filled.sf-input-group.sf-float-input.sf-control-wrapper .sf-clear-icon.sf-small, -.sf-small .sf-filled.sf-input-group.sf-float-input.sf-control-wrapper .sf-clear-icon, -.sf-filled.sf-input-group.sf-control-wrapper.sf-float-input.sf-small .sf-clear-icon, -.sf-filled.sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon.sf-small, -.sf-small .sf-filled.sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon { +.sf-outline.sf-float-input.sf-medium textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input textarea:-webkit-autofill~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input textarea:-webkit-autofill~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:-webkit-autofill~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill~label.sf-float-text.sf-label-bottom, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill textarea:not(:focus):valid:-webkit-autofill~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-medium label.sf-float-text, +.sf-outline.sf-float-input.sf-medium:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper:not(.sf-input-focus) input:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-medium textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper textarea:not(:focus):valid~label.sf-float-text.sf-label-bottom, +.sf-outline.sf-float-input.sf-medium textarea~label.sf-float-text, +.sf-outline.sf-float-input textarea~label.sf-float-text.sf-medium, +.sf-outline.sf-float-input textarea.sf-medium~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input textarea~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium textarea~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper textarea~label.sf-float-text.sf-medium, +.sf-outline.sf-float-input.sf-control-wrapper textarea.sf-medium~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper textarea~label.sf-float-text { + font-size: $medium-outline-float-label-font-size; +} + +.sf-outline.sf-float-input input:-webkit-autofill~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill~label.sf-float-text.sf-label-bottom { + animation-name: slideTopUp; + /* stylelint-disable-line no-unknown-animations */ +} + +.sf-medium .sf-outline.sf-float-input input:-webkit-autofill~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input input:-webkit-autofill~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill~label.sf-float-text, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill input:-webkit-autofill~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill~label.sf-float-text.sf-label-bottom, +.sf-medium.sf-outline.sf-float-input.sf-control-wrapper.sf-autofill:not(.sf-input-focus) input:not(:focus):valid:-webkit-autofill~label.sf-float-text.sf-label-bottom { + animation-name: slideTopUp; + /* stylelint-disable-line no-unknown-animations */ +} + +.sf-filled.sf-input-group.sf-float-input.sf-medium .sf-clear-icon, +.sf-filled.sf-input-group.sf-float-input .sf-clear-icon.sf-medium, +.sf-medium .sf-filled.sf-input-group.sf-float-input .sf-clear-icon, +.sf-filled.sf-input-group.sf-control-wrapper.sf-float-input.sf-medium .sf-clear-icon, +.sf-filled.sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon.sf-medium, +.sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon, +.sf-filled.sf-input-group.sf-float-input.sf-control-wrapper.sf-medium .sf-clear-icon, +.sf-filled.sf-input-group.sf-float-input.sf-control-wrapper .sf-clear-icon.sf-medium, +.sf-medium .sf-filled.sf-input-group.sf-float-input.sf-control-wrapper .sf-clear-icon, +.sf-filled.sf-input-group.sf-control-wrapper.sf-float-input.sf-medium .sf-clear-icon, +.sf-filled.sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon.sf-medium, +.sf-medium .sf-filled.sf-input-group.sf-control-wrapper.sf-float-input .sf-clear-icon { padding: $zero-value $zero-value $zero-value 4px; } .sf-rtl.sf-filled.sf-input-group .sf-clear-icon, .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon, -.sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, -.sf-small .sf-rtl.sf-filled.sf-input-group .sf-clear-icon, -.sf-small .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon, +.sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon, +.sf-medium .sf-rtl.sf-filled.sf-input-group .sf-clear-icon, +.sf-medium .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon, .sf-rtl .sf-filled.sf-input-group .sf-clear-icon, .sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon, -.sf-rtl .sf-filled.sf-input-group.sf-small .sf-clear-icon, -.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, -.sf-small.sf-rtl .sf-filled.sf-input-group .sf-clear-icon, -.sf-small.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon, +.sf-rtl .sf-filled.sf-input-group.sf-medium .sf-clear-icon, +.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon, +.sf-medium.sf-rtl .sf-filled.sf-input-group .sf-clear-icon, +.sf-medium.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon, .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-clear-icon, .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon, -.sf-rtl.sf-filled.sf-float-input.sf-input-group.sf-small .sf-clear-icon, -.sf-small .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-clear-icon, -.sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-clear-icon, -.sf-small .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon, +.sf-rtl.sf-filled.sf-float-input.sf-input-group.sf-medium .sf-clear-icon, +.sf-medium .sf-rtl.sf-filled.sf-float-input.sf-input-group .sf-clear-icon, +.sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-clear-icon, +.sf-medium .sf-rtl.sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon, .sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-clear-icon, .sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon, -.sf-rtl .sf-filled.sf-float-input.sf-input-group.sf-small .sf-clear-icon, -.sf-small.sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-clear-icon, -.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-small .sf-clear-icon, -.sf-small.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon { +.sf-rtl .sf-filled.sf-float-input.sf-input-group.sf-medium .sf-clear-icon, +.sf-medium.sf-rtl .sf-filled.sf-float-input.sf-input-group .sf-clear-icon, +.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group.sf-medium .sf-clear-icon, +.sf-medium.sf-rtl .sf-filled.sf-float-input.sf-control-wrapper.sf-input-group .sf-clear-icon { padding: $zero-value 8px $zero-value $zero-value; } -.sf-rtl.sf-filled.sf-input-group.sf-small .sf-clear-icon, -.sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, -.sf-small .sf-rtl.sf-filled.sf-input-group .sf-clear-icon, -.sf-small .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon, -.sf-rtl .sf-filled.sf-input-group.sf-small .sf-clear-icon, -.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-small .sf-clear-icon, -.sf-small.sf-rtl .sf-filled.sf-input-group .sf-clear-icon, -.sf-small.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon { +.sf-rtl.sf-filled.sf-input-group.sf-medium .sf-clear-icon, +.sf-rtl.sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon, +.sf-medium .sf-rtl.sf-filled.sf-input-group .sf-clear-icon, +.sf-medium .sf-rtl.sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon, +.sf-rtl .sf-filled.sf-input-group.sf-medium .sf-clear-icon, +.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper.sf-medium .sf-clear-icon, +.sf-medium.sf-rtl .sf-filled.sf-input-group .sf-clear-icon, +.sf-medium.sf-rtl .sf-filled.sf-input-group.sf-control-wrapper .sf-clear-icon { padding: $zero-value 4px $zero-value $zero-value; } @@ -1578,12 +1606,7 @@ textarea.sf-outline, } .sf-input-in-wrap .sf-input-group-icon { - @if $input-skin-name == 'material' { - padding-right: 17px; - } - @else { - margin-right: 10px; - } + margin-right: 10px; } } @@ -1784,90 +1807,90 @@ textarea.sf-outline, border-color: $outline-disabled-border-color; } -.sf-outline.sf-float-input.sf-success.sf-input-focus input:focus ~ label.sf-float-text, +.sf-outline.sf-float-input.sf-success.sf-input-focus input:focus~label.sf-float-text, .sf-outline.sf-float-input.sf-input-group.sf-success.sf-valid-input label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-small.sf-success.sf-input-focus input:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-success input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-success textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-small.sf-success textarea:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-success textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-success input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-success input:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-success input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-success textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-success textarea:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-success textarea:focus ~ label.sf-float-text, +.sf-outline.sf-float-input.sf-medium.sf-success.sf-input-focus input:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-success input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-success textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-medium.sf-success textarea:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-success textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-success input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-success input:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-success input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-success textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-success textarea:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-success textarea:focus~label.sf-float-text, .sf-outline.sf-float-input.sf-success label.sf-float-text.sf-label-top, .sf-outline.sf-float-input.sf-input-group.sf-success label.sf-float-text.sf-label-top, .sf-outline.sf-float-input.sf-control-wrapper.sf-success label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-valid-input.sf-success:not(.sf-input-focus) input:valid ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-success:not(.sf-input-focus) input:focus ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-valid-input.sf-success:not(.sf-input-focus) textarea:valid ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-success:not(.sf-input-focus) textarea:focus ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-small.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-success.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input ~ label.sf-float-text { +.sf-outline.sf-float-input.sf-valid-input.sf-success:not(.sf-input-focus) input:valid~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-success:not(.sf-input-focus) input:focus~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-valid-input.sf-success:not(.sf-input-focus) textarea:valid~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-success:not(.sf-input-focus) textarea:focus~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-success.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-medium.sf-success.sf-input-focus input~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-success.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-success.sf-input-focus input~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-success.sf-input-focus input~label.sf-float-text { color: $input-success-color; } -.sf-outline.sf-float-input.sf-error.sf-input-focus input:focus ~ label.sf-float-text, +.sf-outline.sf-float-input.sf-error.sf-input-focus input:focus~label.sf-float-text, .sf-outline.sf-float-input.sf-input-group.sf-error.sf-valid-input label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-small.sf-error.sf-input-focus input:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-error input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-error textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-small.sf-error textarea:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-error textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-error input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-error input:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-error input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-error textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-error textarea:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-error textarea:focus ~ label.sf-float-text, +.sf-outline.sf-float-input.sf-medium.sf-error.sf-input-focus input:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-error input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-error textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-medium.sf-error textarea:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-error textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-error input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-error input:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-error input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-error textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-error textarea:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-error textarea:focus~label.sf-float-text, .sf-outline.sf-float-input.sf-error label.sf-float-text.sf-label-top, .sf-outline.sf-float-input.sf-input-group.sf-error label.sf-float-text.sf-label-top, .sf-outline.sf-float-input.sf-control-wrapper.sf-error label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-valid-input.sf-error:not(.sf-input-focus) input:valid ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-error:not(.sf-input-focus) input:focus ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-valid-input.sf-error:not(.sf-input-focus) textarea:valid ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-error:not(.sf-input-focus) textarea:focus ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-error.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-small.sf-error.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-error.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-error.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-error.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-error.sf-input-focus input ~ label.sf-float-text { +.sf-outline.sf-float-input.sf-valid-input.sf-error:not(.sf-input-focus) input:valid~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-error:not(.sf-input-focus) input:focus~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-valid-input.sf-error:not(.sf-input-focus) textarea:valid~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-error:not(.sf-input-focus) textarea:focus~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-error.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-medium.sf-error.sf-input-focus input~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-error.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-error.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-error.sf-input-focus input~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-error.sf-input-focus input~label.sf-float-text { color: $input-error-color; } -.sf-outline.sf-float-input.sf-warning.sf-input-focus input:focus ~ label.sf-float-text, +.sf-outline.sf-float-input.sf-warning.sf-input-focus input:focus~label.sf-float-text, .sf-outline.sf-float-input.sf-input-group.sf-warning.sf-valid-input label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-small.sf-warning.sf-input-focus input:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-warning input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-warning textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-small.sf-warning textarea:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-warning textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-warning input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-warning input:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-warning input:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-warning textarea:focus ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-warning textarea:focus ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-warning textarea:focus ~ label.sf-float-text, +.sf-outline.sf-float-input.sf-medium.sf-warning.sf-input-focus input:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-warning input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-warning textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-medium.sf-warning textarea:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-warning textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-warning input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-warning input:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-warning input:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-warning textarea:focus~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-warning textarea:focus~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-warning textarea:focus~label.sf-float-text, .sf-outline.sf-float-input.sf-warning label.sf-float-text.sf-label-top, .sf-outline.sf-float-input.sf-input-group.sf-warning label.sf-float-text.sf-label-top, .sf-outline.sf-float-input.sf-control-wrapper.sf-warning label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-valid-input.sf-warning:not(.sf-input-focus) input:valid ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-warning:not(.sf-input-focus) input:focus ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-valid-input.sf-warning:not(.sf-input-focus) textarea:valid ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-warning:not(.sf-input-focus) textarea:focus ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-small.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small.sf-warning.sf-input-focus input ~ label.sf-float-text, -.sf-small .sf-outline.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus input ~ label.sf-float-text { +.sf-outline.sf-float-input.sf-valid-input.sf-warning:not(.sf-input-focus) input:valid~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-warning:not(.sf-input-focus) input:focus~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-valid-input.sf-warning:not(.sf-input-focus) textarea:valid~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-control-wrapper.sf-valid-input.sf-warning:not(.sf-input-focus) textarea:focus~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-warning.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-medium.sf-warning.sf-input-focus input~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-warning.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus input~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium.sf-warning.sf-input-focus input~label.sf-float-text, +.sf-medium .sf-outline.sf-float-input.sf-control-wrapper.sf-warning.sf-input-focus input~label.sf-float-text { color: $input-warning-color; } @@ -1900,48 +1923,48 @@ textarea.sf-outline, .sf-outline.sf-float-input.sf-input-group.sf-disabled .sf-float-text, .sf-outline.sf-float-input.sf-input-group.sf-disabled .sf-float-text.sf-label-top, -.sf-outline.sf-float-input input[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input input[disabled] ~ label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input input[disabled]~label.sf-float-text, +.sf-outline.sf-float-input input[disabled]~label.sf-label-top.sf-float-text, .sf-outline.sf-float-input.sf-disabled label.sf-float-text, .sf-outline.sf-float-input.sf-disabled label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input textarea[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input:not(.sf-error) input[disabled]~label.sf-float-text, +.sf-outline.sf-float-input:not(.sf-error) input[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-medium:not(.sf-error) input[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-medium:not(.sf-error) input[disabled]~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input textarea[disabled]~label.sf-float-text, +.sf-outline.sf-float-input textarea[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input:not(.sf-error) textarea[disabled]~label.sf-float-text, +.sf-outline.sf-float-input:not(.sf-error) textarea[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-medium:not(.sf-error) textarea[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-medium:not(.sf-error) textarea[disabled]~label.sf-float-text.sf-label-top, .sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-disabled .sf-float-text, .sf-outline.sf-float-input.sf-control-wrapper.sf-input-group.sf-disabled .sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled input[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper input[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper input[disabled] ~ label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled input[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled input[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper input[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper input[disabled]~label.sf-label-top.sf-float-text, .sf-outline.sf-float-input.sf-control-wrapper.sf-disabled label.sf-float-text, .sf-outline.sf-float-input.sf-control-wrapper.sf-disabled label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) input[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) input[disabled] ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled textarea[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) textarea[disabled] ~ label.sf-label-top.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text, -.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled.sf-small:not(.sf-error) textarea[disabled] ~ label.sf-float-text.sf-label-top { +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error) input[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) input[disabled]~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) input[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) input[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) input[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) input[disabled]~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled textarea[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled textarea[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper textarea[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper textarea[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper:not(.sf-error) textarea[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-medium:not(.sf-error) textarea[disabled]~label.sf-float-text.sf-label-top, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) textarea[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled:not(.sf-error) textarea[disabled]~label.sf-label-top.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) textarea[disabled]~label.sf-float-text, +.sf-outline.sf-float-input.sf-control-wrapper.sf-disabled.sf-medium:not(.sf-error) textarea[disabled]~label.sf-float-text.sf-label-top { color: $outline-float-label-disbale-font-color; } diff --git a/components/buttons/styles/radio-button/_all.scss b/components/inputs/src/inputs/numerictextbox/_all.scss similarity index 100% rename from components/buttons/styles/radio-button/_all.scss rename to components/inputs/src/inputs/numerictextbox/_all.scss diff --git a/components/inputs/src/inputs/numerictextbox/_layout.scss b/components/inputs/src/inputs/numerictextbox/_layout.scss new file mode 100644 index 0000000..6a37da3 --- /dev/null +++ b/components/inputs/src/inputs/numerictextbox/_layout.scss @@ -0,0 +1,38 @@ +.sf-numeric-container { + width: 100%; +} + +.sf-content-placeholder.sf-numeric.sf-placeholder-numeric { + background-size: $numeric-placeholder-background-size; + min-height: $numeric-placeholder-min-height; +} + +.sf-numeric { + + .sf-numeric-hidden, + &.sf-input-focus .sf-numeric-hidden { + border: 0; + height: 0; + margin: 0; + padding: 0; + text-indent: 0; + visibility: hidden; + width: 0; + } + + &.sf-control-wrapper { + .sf-input-group:not(.sf-disabled):active:not(.sf-success):not(.sf-warning):not(.sf-error):not(.sf-input-focus) { + border-color: $input-group-full-border-color; + box-shadow: none; + } + + .sf-input-group .sf-input-group-icon { + font-size: $numeric-input-icon-size; + } + } + + .sf-input-group-icon.sf-spin-down path, + .sf-input-group-icon.sf-spin-up path { + fill: rgb($icon-color); + } +} \ No newline at end of file diff --git a/components/inputs/src/inputs/numerictextbox/_material3-definition.scss b/components/inputs/src/inputs/numerictextbox/_material3-definition.scss new file mode 100644 index 0000000..bb668a9 --- /dev/null +++ b/components/inputs/src/inputs/numerictextbox/_material3-definition.scss @@ -0,0 +1,3 @@ +$numeric-input-icon-size: 16px !default; +$numeric-placeholder-background-size: 300px 33px !default; +$numeric-placeholder-min-height: 33px !default; \ No newline at end of file diff --git a/components/inputs/styles/numerictextbox/_all.scss b/components/inputs/src/inputs/textarea/_all.scss similarity index 100% rename from components/inputs/styles/numerictextbox/_all.scss rename to components/inputs/src/inputs/textarea/_all.scss diff --git a/components/inputs/styles/textarea/_layout.scss b/components/inputs/src/inputs/textarea/_layout.scss similarity index 81% rename from components/inputs/styles/textarea/_layout.scss rename to components/inputs/src/inputs/textarea/_layout.scss index cbd73f3..2da2090 100644 --- a/components/inputs/styles/textarea/_layout.scss +++ b/components/inputs/src/inputs/textarea/_layout.scss @@ -2,20 +2,17 @@ width: auto; } -.sf-input-group.sf-multi-line-input { - textarea.sf-resize-x { +.sf-input-group.sf-multi-line-input textarea { + &.sf-resize-x { resize: horizontal; } - - textarea.sf-resize-y { + &.sf-resize-y { resize: vertical; } - - textarea.sf-resize-xy { + &.sf-resize-xy { resize: both; } - - textarea.sf-textarea.sf-resize-none { + &.sf-textarea.sf-resize-none { resize: none; } } diff --git a/components/popups/styles/popup/_material3-definition.scss b/components/inputs/src/inputs/textarea/_material3-definition.scss similarity index 100% rename from components/popups/styles/popup/_material3-definition.scss rename to components/inputs/src/inputs/textarea/_material3-definition.scss diff --git a/components/inputs/styles/textarea/_all.scss b/components/inputs/src/inputs/textbox/_all.scss similarity index 100% rename from components/inputs/styles/textarea/_all.scss rename to components/inputs/src/inputs/textbox/_all.scss diff --git a/components/inputs/styles/textbox/_layout.scss b/components/inputs/src/inputs/textbox/_layout.scss similarity index 51% rename from components/inputs/styles/textbox/_layout.scss rename to components/inputs/src/inputs/textbox/_layout.scss index ef7010c..211057a 100644 --- a/components/inputs/styles/textbox/_layout.scss +++ b/components/inputs/src/inputs/textbox/_layout.scss @@ -1,12 +1,12 @@ .sf-content-placeholder.sf-textbox.sf-placeholder-textbox { - background-size: 300px 33px; - min-height: 33px; + background-size: $textbox-background-size-width $textbox-background-size-height; + min-height: $textbox-min-height; } .sf-input-group.sf-input-focus.sf-multi-line-input textarea.sf-textbox { - padding-right: 30px; + padding-right: $textbox-padding-right-focus; } .sf-input-group.sf-input-focus.sf-rtl.sf-multi-line-input textarea.sf-textbox { - padding-left: 30px; + padding-left: $textbox-padding-right-focus; } diff --git a/components/inputs/src/inputs/textbox/_material3-definition.scss b/components/inputs/src/inputs/textbox/_material3-definition.scss new file mode 100644 index 0000000..c67a741 --- /dev/null +++ b/components/inputs/src/inputs/textbox/_material3-definition.scss @@ -0,0 +1,4 @@ +$textbox-min-height: 33px !default; +$textbox-padding-right-focus: 30px !default; +$textbox-background-size-width: 300px !default; +$textbox-background-size-height: 33px !default; \ No newline at end of file diff --git a/components/inputs/src/numeric-textbox/index.ts b/components/inputs/src/numeric-textbox/index.ts new file mode 100644 index 0000000..3c73ba9 --- /dev/null +++ b/components/inputs/src/numeric-textbox/index.ts @@ -0,0 +1,4 @@ +/** + * NumericTextBox modules + */ +export * from './numeric-textbox'; diff --git a/components/inputs/src/numerictextbox/numerictextbox.tsx b/components/inputs/src/numeric-textbox/numeric-textbox.tsx similarity index 72% rename from components/inputs/src/numerictextbox/numerictextbox.tsx rename to components/inputs/src/numeric-textbox/numeric-textbox.tsx index c3f4f87..f9a2a84 100644 --- a/components/inputs/src/numerictextbox/numerictextbox.tsx +++ b/components/inputs/src/numeric-textbox/numeric-textbox.tsx @@ -1,28 +1,40 @@ import { useRef, useState, useCallback, useEffect, forwardRef, Ref, useImperativeHandle, useMemo } from 'react'; -import { InputBase, renderFloatLabelElement, renderClearButton, FloatLabelType, CLASS_NAMES } from '../common/inputbase'; +import { InputBase, renderFloatLabelElement, renderClearButton, LabelMode, CLASS_NAMES } from '../common/inputbase'; import { IL10n, isNullOrUndefined, L10n, preRender, RippleEffect, SvgIcon, useProviderContext, useRippleEffect } from '@syncfusion/react-base'; import { formatUnit } from '@syncfusion/react-base'; import { getNumberFormat, getNumberParser } from '@syncfusion/react-base'; -import { getUniqueID } from '@syncfusion/react-base'; +import { getUniqueID, getValue, getNumericObject } from '@syncfusion/react-base'; import { Size } from '../textbox/textbox'; - +export { LabelMode }; const ROOT: string = 'sf-numeric'; const SPINICON: string = 'sf-input-group-icon'; const SPINUP: string = 'sf-spin-up'; const SPINDOWN: string = 'sf-spin-down'; +export interface NumericChangeEvent { + /** + * Specifies the initial event object received from the input element. + */ + event?: React.ChangeEvent; + + /** + * Specifies the current value of the NumericTextBox. + */ + value?: number | null; +} + export interface NumericTextBoxProps { /** - * Sets the value of the NumericTextBox. When provided, component becomes controlled. + * Specifies the value of the NumericTextBox. When provided, component becomes controlled. * * @default null */ value?: number | null; /** - * Sets the default value of the NumericTextBox for uncontrolled mode. + * Specifies the default value of the NumericTextBox for uncontrolled mode. * * @default - */ @@ -50,14 +62,14 @@ export interface NumericTextBoxProps { step?: number; /** - * Gets or sets the string shown as a hint/placeholder when the NumericTextBox is empty. + * Specifies the string shown as a hint/placeholder when the NumericTextBox is empty. * * @default - */ placeholder?: string; /** - * Determines whether to show increment and decrement buttons (spin buttons) within the input field. + * Specifies whether to show increment and decrement buttons (spin buttons) within the input field. * When enabled, up/down buttons appear that allow users to increment or decrement * the numeric value in the input by a predefined step * @@ -66,18 +78,18 @@ export interface NumericTextBoxProps { spinButton?: boolean; /** - * Determines whether to show a clear button within the input field. + * Specifies whether to show a clear button within the input field. * When enabled, a clear button (×) appears when the field has a value, * allowing users to quickly clear the input with a single click. * * @default false */ - clearButton?: boolean; + clearButton?: React.ReactNode; /** * Specifies the number format that indicates the display format for the value of the NumericTextBox. * - * @default 'n2' + * @default - */ format?: string; @@ -96,15 +108,6 @@ export interface NumericTextBoxProps { */ currency?: string; - /** - * Specifies the currency code to use in currency formatting. - * Possible values are the ISO 4217 currency codes, such as 'USD' for the US dollar,'EUR' for the euro. - * - * @default - - * @private - */ - currencyCode?: string; - /** * Specifies a value that indicates whether the NumericTextBox control allows the value for the specified range. * If it is true, the input value will be restricted between the min and max range. @@ -123,14 +126,14 @@ export interface NumericTextBoxProps { validateOnType?: boolean; /** - * Defines the floating label type for the component. + * Specifies the floating label type for the component. * * @default 'Never' */ - labelMode?: FloatLabelType; + labelMode?: LabelMode; /** - * Triggers when the value of the NumericTextBox changes. + * Specifies the callback function that triggers when the value of the NumericTextBox changes. * The change event of the NumericTextBox component will be triggered in the following scenarios: * * Changing the previous value using keyboard interaction and then focusing out of the component. * * Focusing on the component and scrolling within the input. @@ -139,20 +142,20 @@ export interface NumericTextBoxProps { * * @event onChange */ - onChange?: (args : React.ChangeEvent, value: number | null) => void; + onChange?: (event: NumericChangeEvent) => void; /** - * The size configuration of the component. + * Specifies the size configuration of the component. * * @default Size.Medium */ size?: Size; } -export interface INumericTextBox extends NumericTextBoxProps{ +export interface INumericTextBox extends NumericTextBoxProps { /** - * This is NumericTextBox component element. + * Specifies the DOM element NumericTextBox component. * * @private * @default null @@ -167,8 +170,9 @@ type INumericTextBoxProps = NumericTextBoxProps & Omit + * import { NumericTextBox } from "@syncfusion/react-inputs"; + * + * * ``` */ export const NumericTextBox: React.ForwardRefExoticComponent> = @@ -183,7 +187,7 @@ forwardRef((props: INumericTextBoxProps, placeholder = '', spinButton = true, clearButton = false, - format = 'n2', + format, decimals = null, strictMode = true, validateOnType = false, @@ -193,10 +197,12 @@ forwardRef((props: INumericTextBoxProps, currency = null, width = null, className = '', - size, + autoComplete = 'off', + size = Size.Medium, onChange, onFocus, onBlur, + onKeyDown, ...otherProps } = props; @@ -208,6 +214,8 @@ forwardRef((props: INumericTextBoxProps, ); const [isFocused, setIsFocused] = useState(false); const [previousValue, setPreviousValue] = useState(isControlled ? (value ?? null) : (defaultValue ?? null)); + const [inputString, setInputString] = useState(''); + const [arrowKeyPressed, setIsArrowKeyPressed] = useState(false); const { locale, dir, ripple } = useProviderContext(); const rippleRef1: RippleEffect = useRippleEffect(ripple, { duration: 500, isCenterRipple: true }); @@ -240,17 +248,22 @@ forwardRef((props: INumericTextBoxProps, disabled ? CLASS_NAMES.DISABLE : '', isFocused ? CLASS_NAMES.TEXTBOX_FOCUS : '', (!isNullOrUndefined(currentValueRef.current) && labelMode !== 'Always') ? CLASS_NAMES.VALIDINPUT : '', - size && size.toLowerCase() !== 'medium' ? `sf-${size.toLowerCase()}` : '' + size && size.toLowerCase() !== 'small' ? `sf-${size.toLowerCase()}` : '' ); }; const spinSize: string = size?.toLocaleLowerCase() === 'small' ? '12' : '14'; - const setPlaceholder: string = useMemo(() => { - const l10n: IL10n = L10n('numerictextbox', { placeholder: placeholder }, locale); - l10n.setLocale(locale); - return l10n.getConstant('placeholder'); - }, [locale, placeholder]); + const { incrementText, decrementText } = useMemo(() => { + const l10n: IL10n = L10n('numericTextbox', { + increment: 'Increment value', + decrement: 'Decrement value' + }, locale); + return { + incrementText: l10n.getConstant('increment'), + decrementText: l10n.getConstant('decrement') + }; + }, [locale]); useEffect(() => { preRender('numerictextbox'); @@ -318,36 +331,50 @@ forwardRef((props: INumericTextBoxProps, }, [decimals]); const formatNumber: (value: number | null) => string = useCallback((value: number | null): string => { - if (value === null || value === undefined) { return ''; } + if (value === null || value === undefined) { + if (isFocused) { + return inputString || ''; + } + return ''; + } try { - if (isFocused && format.toLowerCase().includes('p')) { + if (isFocused && format && format.toLowerCase().includes('p')) { const percentValue: number = Math.round((value * 100) * 1e12) / 1e12; const numberOfDecimals: number = getNumberOfDecimals(percentValue); return percentValue.toFixed(numberOfDecimals); } else if (isFocused) { - if (typeof value === 'number') { - if (Number.isInteger(value)) { - return value.toString(); + if (arrowKeyPressed) { + if (typeof value === 'number') { + if (Number.isInteger(value)) { + if (value.toString() !== inputString) { + setInputString(value.toString()); + } + return value.toString(); + } + const strValue: string = value.toString(); + return strValue.replace(/\.?0+$/, ''); } - const strValue: string = value.toString(); - return strValue.replace(/\.?0+$/, ''); + return String(value); } - return String(value); + return inputString; } const numberOfDecimals: number = getNumberOfDecimals(value); - return getNumberFormat(locale, { + const formattedValue: string = getNumberFormat({ + locale: locale, format: format, maximumFractionDigits: numberOfDecimals, minimumFractionDigits: numberOfDecimals, - useGrouping: format.toLowerCase().includes('n'), + useGrouping: format ? format.toLowerCase().includes('n') : false, currency: currency })(value); + if (inputString === '' && !isFocused) { setInputString(formattedValue); } + return formattedValue; } catch (error) { return value.toFixed(2); } - }, [format, currency, isFocused, getNumberOfDecimals]); + }, [format, currency, isFocused, inputString, arrowKeyPressed, getNumberOfDecimals]); const updateValue: (newValue: number | null, e?: React.ChangeEvent | Event) => void = useCallback((newValue: number | null, e?: React.ChangeEvent | Event) => { @@ -360,15 +387,25 @@ forwardRef((props: INumericTextBoxProps, } if (onChange) { - onChange(e as React.ChangeEvent, newValue); + onChange({ event: e as React.ChangeEvent, value: newValue }); } }, [inputValue, onChange, isControlled, formatNumber]); const handleChange: (e: React.ChangeEvent) => void = useCallback((e: React.ChangeEvent) => { - const parsedValue: number | null = getNumberParser(locale, { format: format })(e.target.value); - let newValue: number | null | undefined = Number.isNaN(parsedValue) ? 0 : parsedValue; + let rawStringValue: string = e.target.value; + if (rawStringValue !== null) { + const minusCount: number = (rawStringValue.match(/-/g) || []).length; + if (minusCount > 1) { + rawStringValue = rawStringValue.replace(/-/g, ''); + } else if (minusCount === 1) { + rawStringValue = '-' + rawStringValue.replace(/-/g, ''); + } + setInputString(rawStringValue); + } + const parsedValue: number | null = getNumberParser({ locale: locale, format: format })(rawStringValue); + let newValue: number | null | undefined = Number.isNaN(parsedValue) ? null : parsedValue; if (strictMode && newValue !== null) { newValue = trimValue(newValue as number); } @@ -382,6 +419,7 @@ forwardRef((props: INumericTextBoxProps, if (disabled || readOnly) { return; } + setIsArrowKeyPressed(true); if (increments) { increment(); } else { @@ -398,11 +436,12 @@ forwardRef((props: INumericTextBoxProps, const handleBlur: (e: React.FocusEvent) => void = useCallback((e: React.FocusEvent) => { setIsFocused(false); + setIsArrowKeyPressed(false); let newValue: number | null | undefined; if (e.currentTarget.value === '') { newValue = null; } else { - newValue = getNumberParser(locale, { format: format })(e.currentTarget.value); + newValue = getNumberParser({ locale: locale, format: format })(e.currentTarget.value); if (isNaN(newValue as number)) { newValue = currentValueRef.current; } @@ -413,8 +452,11 @@ forwardRef((props: INumericTextBoxProps, newValue = trimValue(newValue as number); } } - - updateValue(newValue as number, e); + const updatedValue: number = isControlled ? value as number : newValue as number; + if (updatedValue) { + setInputString(updatedValue.toString()); + } else { setInputString(''); } + updateValue(updatedValue, e); if (onBlur) { onBlur(e); @@ -426,7 +468,7 @@ forwardRef((props: INumericTextBoxProps, let newValue: number = ((currentValueRef.current === null || currentValueRef.current === undefined) ? 0 : currentValueRef.current) + adjustment; let precision: number = 10; - if (format.toLowerCase().includes('p')) { + if (format && format.toLowerCase().includes('p')) { const match: RegExpMatchArray | null = format.match(/p(\d+)/i); if (match && match[1]) { precision = parseInt(match[1], 10) + 2; @@ -458,43 +500,68 @@ forwardRef((props: INumericTextBoxProps, adjustValue(false); }, [adjustValue]); - const handleKeyDown: (e: React.KeyboardEvent) => void = useCallback((e: React.KeyboardEvent) => { - if ( - !/[0-9.-]/.test(e.key) && - !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'ArrowUp', 'ArrowDown'].includes(e.key) - ) { - e.preventDefault(); - } + const countDecimalSeparators: (value: string) => number = useCallback((value: string): number => { + const decimalSeparator: string = getValue('decimal', getNumericObject(locale)); + // eslint-disable-next-line security/detect-non-literal-regexp + const regex: RegExp = new RegExp(`\\${decimalSeparator}`, 'g'); + const matches: RegExpMatchArray | null = value.match(regex); + return matches ? matches.length : 0; + }, [value]); + const handleKeyDown: (e: React.KeyboardEvent) => void = useCallback((e: React.KeyboardEvent) => { if (!readOnly) { switch (e.key) { case 'ArrowUp': + setIsArrowKeyPressed(true); e.preventDefault(); increment(); break; case 'ArrowDown': + setIsArrowKeyPressed(true); e.preventDefault(); decrement(); break; case 'Enter': { e.preventDefault(); - const parsedValue: number = getNumberParser(locale, { format: format })(e.currentTarget.value); + const parsedValue: number = getNumberParser({ locale: locale, format: format })(e.currentTarget.value); let newValue: number | null = Number.isNaN(parsedValue) ? currentValueRef.current : parsedValue; - if (strictMode && newValue !== null) { newValue = trimValue(newValue); } - updateValue(newValue); } break; - default: break; + default: { + const hasModifierKey: boolean = e.ctrlKey || e.altKey || e.metaKey; + const isRestrictedLetter: boolean = /^[a-zA-Z!@#$%^&*()_=[\]{}|;:'"<>?/~`\\+]$/.test(e.key); + if ((isRestrictedLetter && !hasModifierKey) || hasModifierKey) { + if (isRestrictedLetter && !hasModifierKey) { + e.preventDefault(); + } + return; + } + setIsArrowKeyPressed(false); + let currentChar: string = e.currentTarget.value; + const decimalSeparator: string = getValue('decimal', getNumericObject(locale)); + const isAlterNumPadDecimalChar: boolean = e.code === 'NumpadDecimal' && e.key !== decimalSeparator; + if (isAlterNumPadDecimalChar) { + currentChar = decimalSeparator; + } + if (e.key === decimalSeparator && countDecimalSeparators(currentChar) >= 1) { + e.preventDefault(); + return; + } + } break; } } - }, [increment, decrement, strictMode, trimValue, updateValue, readOnly, format]); + if (onKeyDown) { + onKeyDown(e); + } + }, [increment, decrement, strictMode, trimValue, updateValue, readOnly, format, onKeyDown]); const clearValue: () => void = useCallback(() => { updateValue(null); + setInputString(''); }, [updateValue]); const displayValue: string = formatNumber( @@ -513,14 +580,14 @@ forwardRef((props: INumericTextBoxProps, onBlur={handleBlur} {...otherProps} role="spinbutton" - aria-label="numerictextbox" onKeyDown={handleKeyDown} floatLabelType={labelMode} - placeholder={setPlaceholder} + placeholder={placeholder} aria-valuemin={min} aria-valuemax={max} value={displayValue} aria-valuenow={currentValueRef.current || undefined} + autoComplete={autoComplete} tabIndex={0} disabled={disabled} readOnly={readOnly} @@ -529,12 +596,12 @@ forwardRef((props: INumericTextBoxProps, labelMode, isFocused, displayValue || '', - setPlaceholder, + placeholder, uniqueId )} {clearButton && renderClearButton( currentValueRef.current ? currentValueRef.current.toString() : '', - clearValue + clearValue, clearButton, 'numericTextbox', locale )} {spinButton && ( <> @@ -545,7 +612,7 @@ forwardRef((props: INumericTextBoxProps, e.preventDefault(); }} onClick={() => handleSpinClick(false)} - title="Decrement value" + title={decrementText} > {ripple && } @@ -557,7 +624,7 @@ forwardRef((props: INumericTextBoxProps, e.preventDefault(); }} onClick={() => handleSpinClick(true)} - title="Increment value" + title={incrementText} > {ripple && } diff --git a/components/inputs/src/numerictextbox/index.ts b/components/inputs/src/numerictextbox/index.ts deleted file mode 100644 index f3b3034..0000000 --- a/components/inputs/src/numerictextbox/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * NumericTextBox modules - */ -export * from './numerictextbox'; diff --git a/components/inputs/src/textarea/index.ts b/components/inputs/src/textarea/index.ts deleted file mode 100644 index f80acf7..0000000 --- a/components/inputs/src/textarea/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * textarea modules - */ -export * from './textarea'; diff --git a/components/inputs/src/textarea/textarea.tsx b/components/inputs/src/textarea/textarea.tsx deleted file mode 100644 index ed3a588..0000000 --- a/components/inputs/src/textarea/textarea.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import * as React from 'react'; -import { forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; -import { CLASS_NAMES, FloatLabelType, renderClearButton, renderFloatLabelElement } from '../common/inputbase'; -import { getUniqueID, IL10n, L10n, preRender, useProviderContext } from '@syncfusion/react-base'; -import { Variant } from '../textbox/textbox'; - - -/** - * Constant for horizontal resize mode - */ -const RESIZE_X: string = 'sf-resize-x'; - -/** - * Constant for vertical resize mode - */ -const RESIZE_Y: string = 'sf-resize-y'; - -/** - * Constant for both horizontal and vertical resize mode - */ -const RESIZE_XY: string = 'sf-resize-xy'; - -/** - * Constant for no resize mode - */ -const RESIZE_NONE: string = 'sf-resize-none'; - -/** - * Constant for multi-line input class - */ -const MULTILINE: string = 'sf-multi-line-input'; - -/** - * Constant for auto-width class - */ -const AUTOWIDTH: string = 'sf-auto-width'; - -/** - * Defines the available resize modes for components that support resizing. - * - * @enum {string} - */ -export enum ResizeMode { - /** - * Disables resizing functionality. - */ - None = 'None', - - /** - * Enables resizing in both horizontal and vertical directions. - */ - Both = 'Both', - - /** - * Enables resizing only in the horizontal direction. - */ - Horizontal = 'Horizontal', - - /** - * Enables resizing only in the vertical direction. - */ - Vertical = 'Vertical' -} - - -export interface TextAreaProps { - /** - * Sets the value of the component. When provided, the component will be controlled. - * - * @default - - */ - value?: string; - - /** - * Sets the default value of the component. Used for uncontrolled mode. - * - * @default - - */ - defaultValue?: string; - - /** - * Defines the floating label type for the component. - * - * @default 'Never' - */ - labelMode?: FloatLabelType; - - /** - * Sets the placeholder text for the component. - * - * @default - - */ - placeholder?: string; - - /** - * Resize mode for the textarea - * - * @default 'Both' - */ - resizeMode?: ResizeMode; - - /** - * Number of columns for the textarea - * - * @default - - */ - cols?: number; - - /** - * Determines whether to show a clear button within the input field. - * When enabled, a clear button (×) appears when the field has a value, - * allowing users to quickly clear the input with a single click. - * - * @default false - */ - clearButton?: boolean; - - /** - * Number of rows for the textarea - * - * @default 2 - */ - rows?: number; - - /** - * Callback fired when the input value is changed. - * - * @event onChange - * @param {React.ChangeEvent} event - The change event object containing the new value. - * @returns {void} - */ - onChange?: (event: React.ChangeEvent) => void; - - /** - * The visual style variant of the component. - * - * @default Variant.Standard - */ - variant?: Variant; -} - -export interface ITextArea extends TextAreaProps { - /** - * This is TextArea component element. - * - * @private - * @default null - */ - element?: HTMLTextAreaElement | null; -} - -type ITextAreaProps = TextAreaProps & Omit, keyof TextAreaProps>; - -/** - * TextArea component that provides a multi-line text input field with enhanced functionality. - * Supports both controlled and uncontrolled modes based on presence of value or defaultValue prop. - * - * ```typescript - *