diff --git a/public/rich-components/input-with-stepper.svg b/public/rich-components/input-with-stepper.svg new file mode 100644 index 00000000..be747f48 --- /dev/null +++ b/public/rich-components/input-with-stepper.svg @@ -0,0 +1,15 @@ + + + + + + 0 + + + + + + + + + \ No newline at end of file diff --git a/src/common/components/mock-components/front-rich-components/index.ts b/src/common/components/mock-components/front-rich-components/index.ts index 6664c0ae..c3c7392b 100644 --- a/src/common/components/mock-components/front-rich-components/index.ts +++ b/src/common/components/mock-components/front-rich-components/index.ts @@ -18,3 +18,4 @@ export * from './loading-indicator'; export * from './videoconference'; export * from './togglelightdark-shape'; export * from './gauge/gauge'; +export * from './input-with-stepper'; diff --git a/src/common/components/mock-components/front-rich-components/input-with-stepper/index.ts b/src/common/components/mock-components/front-rich-components/input-with-stepper/index.ts new file mode 100644 index 00000000..05cd467b --- /dev/null +++ b/src/common/components/mock-components/front-rich-components/input-with-stepper/index.ts @@ -0,0 +1,2 @@ +export * from './input-with-stepper'; +export * from './input-with-stepper.business'; diff --git a/src/common/components/mock-components/front-rich-components/input-with-stepper/input-with-stepper.business.ts b/src/common/components/mock-components/front-rich-components/input-with-stepper/input-with-stepper.business.ts new file mode 100644 index 00000000..50c75d65 --- /dev/null +++ b/src/common/components/mock-components/front-rich-components/input-with-stepper/input-with-stepper.business.ts @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; + +type MustBeANumberError = 'You must enter a number'; + +interface handleCounterInputWithStepperHook { + valueToString: string | MustBeANumberError; + handleIncrement: () => void; + handleDecrement: () => void; + isTextANumber: boolean; +} + +const MUST_BE_A_NUMBER: MustBeANumberError = 'You must enter a number'; + +export const useHandleCounterInputWithStepper = ( + text: string +): handleCounterInputWithStepperHook => { + const [value, setValue] = React.useState(0); + + const textToNumber = parseInt(text); + + const isTextANumber: boolean = !isNaN(textToNumber); + + useEffect(() => { + if (isTextANumber) { + setValue(textToNumber); + } else { + setValue(MUST_BE_A_NUMBER); + } + }, [text]); + + const handleIncrement = () => { + if (typeof value === 'number') { + setValue(value + 1); + } + }; + + const handleDecrement = () => { + if (typeof value === 'number') { + if (value === 0) return; + setValue(value - 1); + } + }; + + const valueToString: string = + typeof value === 'string' ? value : value.toString(); + + return { + valueToString, + handleIncrement, + handleDecrement, + isTextANumber, + }; +}; + +export const handleButtonWidth = (restrictedWidth: number): number => { + const buttonWidth = restrictedWidth * 0.3; + const minButtonWidth = 30; + const maxButtonWidth = 70; + + if (buttonWidth < minButtonWidth) return minButtonWidth; + if (buttonWidth > maxButtonWidth) return maxButtonWidth; + return buttonWidth; +}; diff --git a/src/common/components/mock-components/front-rich-components/input-with-stepper/input-with-stepper.tsx b/src/common/components/mock-components/front-rich-components/input-with-stepper/input-with-stepper.tsx new file mode 100644 index 00000000..e68649a3 --- /dev/null +++ b/src/common/components/mock-components/front-rich-components/input-with-stepper/input-with-stepper.tsx @@ -0,0 +1,176 @@ +import { forwardRef } from 'react'; +import { Group, Rect, Text } from 'react-konva'; +import { ShapeSizeRestrictions } from '@/core/model'; +import { ShapeType } from '../../../../../core/model/index'; +import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes'; +import { useShapeComponentSelection } from '../../../shapes/use-shape-selection.hook'; +import { ShapeProps } from '../../shape.model'; +import { + handleButtonWidth, + useHandleCounterInputWithStepper, +} from './input-with-stepper.business'; +import { INPUT_SHAPE } from '../../front-components/shape.const'; +import { KonvaEventObject } from 'konva/lib/Node'; +import { useShapeProps } from '@/common/components/shapes/use-shape-props.hook'; +import { useGroupShapeProps } from '../../mock-components.utils'; + +const inputWithStepperSizeRestrictions: ShapeSizeRestrictions = { + minWidth: 60, + minHeight: 35, + maxWidth: 500, + maxHeight: 35, + defaultWidth: 100, + defaultHeight: 35, +}; + +export const getInputWithStepperSizeRestrictions = (): ShapeSizeRestrictions => + inputWithStepperSizeRestrictions; + +const shapeType: ShapeType = 'inputWithStepper'; + +export const InputWithStepperShape = forwardRef( + (props, ref) => { + const { + x, + y, + width, + height, + id, + text, + onSelected, + otherProps, + ...shapeProps + } = props; + + const restrictedSize = fitSizeToShapeSizeRestrictions( + inputWithStepperSizeRestrictions, + width, + height + ); + const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; + + const { handleSelection } = useShapeComponentSelection(props, shapeType); + + const handleDoubleClickInButtons = (e: KonvaEventObject) => + (e.cancelBubble = true); + + const { + valueToString: value, + handleIncrement, + handleDecrement, + isTextANumber, + } = useHandleCounterInputWithStepper(text); + + const { stroke, strokeStyle, fill, textColor } = useShapeProps( + otherProps, + INPUT_SHAPE + ); + + // Reservar espacio para el stepper + const buttonWidth = handleButtonWidth(restrictedWidth); + const buttonHeight = restrictedHeight / 2; + + const commonGroupProps = useGroupShapeProps( + props, + restrictedSize, + shapeType, + ref + ); + + return ( + + {/* Caja del input */} + + + {/* Texto del input */} + + + {/* Botón de incremento (flecha arriba) */} + + + + + + {/* Botón de decremento (flecha abajo) */} + + + + + {!isTextANumber && ( + + + + )} + + ); + } +); diff --git a/src/core/model/index.ts b/src/core/model/index.ts index 3ce8dc4a..370dc3a3 100644 --- a/src/core/model/index.ts +++ b/src/core/model/index.ts @@ -68,6 +68,7 @@ export type ShapeType = | 'appBar' | 'buttonBar' | 'tooltip' + | 'inputWithStepper' | 'slider' | 'chip' | 'link' @@ -137,6 +138,7 @@ export const ShapeDisplayName: Record = { buttonBar: 'Button Bar', tooltip: 'Tooltip', slider: 'Slider', + inputWithStepper: 'Input With Stepper', chip: 'Chip', richtext: 'Rich Text', cilinder: 'Cilinder', diff --git a/src/pods/canvas/model/inline-editable.model.ts b/src/pods/canvas/model/inline-editable.model.ts index fd465059..09f6fd43 100644 --- a/src/pods/canvas/model/inline-editable.model.ts +++ b/src/pods/canvas/model/inline-editable.model.ts @@ -82,6 +82,7 @@ const shapeTypesWithDefaultText = new Set([ 'modalDialog', 'loading-indicator', 'gauge', + 'inputWithStepper', ]); // Map of ShapeTypes to their default text values @@ -122,6 +123,7 @@ const defaultTextValueMap: Partial> = { browser: 'https://example.com', modalDialog: 'Title here...', 'loading-indicator': 'Loading...', + inputWithStepper: '0', }; export const generateDefaultTextValue = ( diff --git a/src/pods/canvas/model/shape-other-props.utils.ts b/src/pods/canvas/model/shape-other-props.utils.ts index 43e9f5d7..c22aaad3 100644 --- a/src/pods/canvas/model/shape-other-props.utils.ts +++ b/src/pods/canvas/model/shape-other-props.utils.ts @@ -260,6 +260,15 @@ export const generateDefaultOtherProps = ( textColor: '#000000', strokeStyle: [], }; + case 'inputWithStepper': + return { + stroke: INPUT_SHAPE.DEFAULT_STROKE_COLOR, + backgroundColor: INPUT_SHAPE.DEFAULT_FILL_BACKGROUND, + textColor: INPUT_SHAPE.DEFAULT_FILL_TEXT, + borderRadius: `${INPUT_SHAPE.DEFAULT_CORNER_RADIUS}`, + disabled: INPUT_SHAPE.DEFAULT_DISABLED, + strokeStyle: [], + }; default: return undefined; } diff --git a/src/pods/canvas/model/shape-size.mapper.ts b/src/pods/canvas/model/shape-size.mapper.ts index c4425897..eb3191f1 100644 --- a/src/pods/canvas/model/shape-size.mapper.ts +++ b/src/pods/canvas/model/shape-size.mapper.ts @@ -156,6 +156,7 @@ const shapeSizeMap: Record ShapeSizeRestrictions> = { gauge: getGaugeShapeSizeRestrictions, imagePlaceholder: getImagePlaceholderShapeSizeRestrictions, chip: getChipShapeSizeRestrictions, + inputWithStepper: getChipShapeSizeRestrictions, }; export default shapeSizeMap; diff --git a/src/pods/canvas/model/transformer.model.ts b/src/pods/canvas/model/transformer.model.ts index f5fdb32a..5bef4ab7 100644 --- a/src/pods/canvas/model/transformer.model.ts +++ b/src/pods/canvas/model/transformer.model.ts @@ -78,6 +78,8 @@ export const generateTypeOfTransformer = (shapeType: ShapeType): string[] => { return []; case 'image': return ['top-left', 'top-right', 'bottom-left', 'bottom-right']; + case 'inputWithStepper': + return ['middle-left', 'middle-right']; default: return [ 'top-left', diff --git a/src/pods/canvas/shape-renderer/index.tsx b/src/pods/canvas/shape-renderer/index.tsx index d2f1f668..5d522ec1 100644 --- a/src/pods/canvas/shape-renderer/index.tsx +++ b/src/pods/canvas/shape-renderer/index.tsx @@ -48,6 +48,7 @@ import { renderCalendar, renderAppBar, renderLoadingIndicator, + renderInputWithStepper, } from './simple-rich-components'; import { renderDiamond, @@ -206,6 +207,8 @@ export const renderShapeComponent = ( return renderImagePlaceHolder(shape, shapeRenderedProps); case 'chip': return renderChip(shape, shapeRenderedProps); + case 'inputWithStepper': + return renderInputWithStepper(shape, shapeRenderedProps); default: return renderNotFound(shape, shapeRenderedProps); } diff --git a/src/pods/canvas/shape-renderer/simple-rich-components/index.ts b/src/pods/canvas/shape-renderer/simple-rich-components/index.ts index 81078779..09ceb073 100644 --- a/src/pods/canvas/shape-renderer/simple-rich-components/index.ts +++ b/src/pods/canvas/shape-renderer/simple-rich-components/index.ts @@ -15,6 +15,7 @@ export * from './pie-chart.renderer'; export * from './gauge.renderer'; export * from './table.renderer'; export * from './tabsbar.renderer'; +export * from './input-with-stepper.renderer'; export * from './vertical-menu.renderer'; export * from './video-player.renderer'; export * from './audio-player.renderer'; diff --git a/src/pods/canvas/shape-renderer/simple-rich-components/input-with-stepper.renderer.tsx b/src/pods/canvas/shape-renderer/simple-rich-components/input-with-stepper.renderer.tsx new file mode 100644 index 00000000..862613ec --- /dev/null +++ b/src/pods/canvas/shape-renderer/simple-rich-components/input-with-stepper.renderer.tsx @@ -0,0 +1,32 @@ +import { InputWithStepperShape } from '@/common/components/mock-components/front-rich-components'; +import { ShapeRendererProps } from '../model'; +import { ShapeModel } from '@/core/model'; + +export const renderInputWithStepper = ( + shape: ShapeModel, + shapeRenderedProps: ShapeRendererProps +) => { + const { handleSelected, shapeRefs, handleDragEnd, handleTransform } = + shapeRenderedProps; + + return ( + + ); +}; diff --git a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts index c8ceb040..f2acef8e 100644 --- a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts +++ b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts @@ -30,6 +30,10 @@ export const mockRichComponentsCollection: ItemInfo[] = [ { thumbnailSrc: '/rich-components/pie.svg', type: 'pie' }, { thumbnailSrc: '/rich-components/table.svg', type: 'table' }, { thumbnailSrc: '/rich-components/tabsbar.svg', type: 'tabsBar' }, + { + thumbnailSrc: '/rich-components/input-with-stepper.svg', + type: 'inputWithStepper', + }, { thumbnailSrc: '/widgets/togglelightdark.svg', type: 'toggleLightDark' }, { thumbnailSrc: '/rich-components/videoPlayer.svg', type: 'videoPlayer' }, {