Parcourir la source

Feature: Markdown Viewer start.

lfabl il y a 1 mois
Parent
commit
e3cf51795a

+ 15 - 1
example/src/pages/home/index.tsx

@@ -27,7 +27,8 @@ import {
     Button,
     Dialog,
     Switch,
-    Text
+    Text,
+    MarkdownViewer
 } from "ncore-ui-kit-mobile";
 import {
     useNavigation
@@ -360,6 +361,19 @@ const Home = () => {
             </Fragment>;
         }}
     >
+        <MarkdownViewer
+            content={`
+# Merhaba Furkan!.
+Nabersin ?
+## Deneme title 2
+iyisindir **umarım.**
+
+* Merhaba madde1.
+- Deneme madde2.
+
+[Alt text için deneme](https://static.vecteezy.com/system/resources/thumbnails/060/843/811/small/close-up-of-raindrops-on-leaves-hd-background-luxury-hd-wallpaper-image-trendy-background-illustration-free-photo.jpg)
+            `}
+        />
         <NotificationIndicator
             type="danger"
             title={53}

+ 5 - 0
src/components/button/index.tsx

@@ -34,6 +34,7 @@ const Button: FC<IButtonProps> = ({
     isDisabled = false,
     type = "primary",
     size = "medium",
+    renderLoading,
     customTheme,
     titleStyle,
     isLoading,
@@ -124,6 +125,10 @@ const Button: FC<IButtonProps> = ({
 
     const renderIcon = () => {
         if (isLoading) {
+            if(renderLoading) {
+                return renderLoading();
+            }
+
             return <Loading
                 {...iconProps}
                 style={[

+ 4 - 0
src/components/button/type.ts

@@ -1,3 +1,6 @@
+import type {
+    ReactNode
+} from "react";
 import {
     type ButtonProps,
     type ViewStyle,
@@ -89,6 +92,7 @@ interface IButtonProps extends Omit<ButtonProps, "title"> {
     };
     spreadBehaviour?: ButtonSpreadBehaviour;
     iconDirection?: "left" | "right";
+    renderLoading?: () => ReactNode;
     isCustomPadding?: boolean;
     variant?: ButtonVariant;
     icon?: NCoreUIKitIcon;

+ 4 - 0
src/components/index.ts

@@ -89,3 +89,7 @@ export {
 export {
     default as NotificationIndicator
 } from "./notificationIndicator";
+
+export {
+    default as MarkdownViewer
+} from "./markdownViewer";

+ 180 - 0
src/components/markdownViewer/index.tsx

@@ -0,0 +1,180 @@
+import type {
+    FC
+} from "react";
+import {
+    Image,
+    Text as NativeText,
+    View
+} from "react-native";
+import type IMarkdownViewerProps from "./type";
+import stylesheet from "./stylesheet";
+import Text from "../text";
+
+type MarkdownKeys = "# " |
+    "## " |
+    "### " |
+    "#### " |
+    "##### " |
+    "###### " |
+    "* " |
+    "- " |
+    "p";
+
+const DEFAULT_VARIANTS: Record<MarkdownKeys, keyof NCoreUIKit.Typography> = {
+    "# ": "displayLargeSize",
+    "## ": "displayMediumSize",
+    "### ": "displaySmallSize",
+    "#### ": "headlineLargeSize",
+    "##### ": "headlineMediumSize",
+    "###### ": "headlineSmallSize",
+    "* ": "bodyLargeSize",
+    "- ": "bodyLargeSize",
+    "p": "bodyMediumSize"
+};
+
+const parseMarkdown = (rawText: string): Array<{
+    variant?: keyof NCoreUIKit.Typography;
+    nativeType: MarkdownKeys | "image";
+    type: "text" | "bullet" | "image";
+    content: string;
+    url?: string;
+    key: number;
+}> => {
+    const lines = rawText.split("\n");
+
+    return lines.map((line, index) => {
+        if (line.startsWith("# ")) return {
+            content: line.replace("# ", ""),
+            variant: DEFAULT_VARIANTS["# "],
+            nativeType: "# ",
+            type: "text",
+            key: index
+        };
+
+        if (line.startsWith("## ")) return {
+            content: line.replace("## ", ""),
+            variant: DEFAULT_VARIANTS["## "],
+            nativeType: "## ",
+            type: "text",
+            key: index
+        };
+
+        const isStartWithImage = line.startsWith("[");
+
+        if(isStartWithImage || line.startsWith("[")) {
+            const contentMatch = line.match(/\[(.*?)\]/);
+
+            const imageContent = (contentMatch && contentMatch[1]) ? contentMatch[1] : "";
+
+            const tmpLine = line.replace(/\[.*?\]/g, "");
+
+            const urlMatch = tmpLine.match(/\((.*?)\)/);
+
+            const imageURL = (urlMatch && urlMatch[1]) ? urlMatch[1] : "";
+
+            return {
+                content: imageContent,
+                nativeType: "image",
+                type: "image",
+                url: imageURL,
+                key: index
+            };
+        }
+
+        const isStarList = line.startsWith("* ");
+
+        if (isStarList || line.startsWith("- ")) {
+            return {
+                variant: DEFAULT_VARIANTS[isStarList ? "* " : "- "],
+                nativeType: isStarList ? "* " : "- ",
+                content: line.substring(2),
+                type: "bullet",
+                key: index
+            };
+        }
+
+        return {
+            variant: DEFAULT_VARIANTS["p"],
+            nativeType: "p",
+            content: line,
+            type: "text",
+            key: index
+        };
+    });
+};
+
+const MarkdownViewer: FC<IMarkdownViewerProps> = ({
+    content
+}) => {
+    const nodes = parseMarkdown(content);
+
+    const renderInlineStyles = (_content: string) => {
+        const parts = _content.split(/(\*\*.*?\*\*)/g);
+
+        return parts.map((part, i) => {
+            if (part.startsWith("**") && part.endsWith("**")) {
+                return (
+                    <NativeText
+                        key={i}
+                        style={{
+                            fontWeight: "bold"
+                        }}
+                    >
+                        {part.replace(/\*\*/g, "")}
+                    </NativeText>
+                );
+            }
+
+            return part;
+        });
+    };
+
+    return <View
+        style={[
+            stylesheet.container
+        ]}
+    >
+        {nodes.map((node) => {
+            if (!node.content.trim() && node.nativeType === "p") return null;
+
+            switch (node.type) {
+                case "text":
+                    return <Text
+                        variant={node.variant}
+                        key={node.key}
+                    >{renderInlineStyles(node.content)}</Text>;
+                case "image":
+                    return <Image
+                        alt={node.content}
+                        key={node.key}
+                        source={{
+                            uri: node.url
+                        }}
+                        resizeMode="contain"
+                        style={{
+                            width: 300,
+                            height: 300
+                        }}
+                    />;
+                case "bullet":
+                    return (
+                        <View
+                            key={node.key}
+                            style={{
+                                flexDirection: "row",
+                                alignItems: "center"
+                            }}
+                        >
+                            <Text variant={node.variant}>• </Text><Text variant={node.variant}>{renderInlineStyles(node.content)}</Text>
+                        </View>
+                    );
+                default:
+                    return <Text
+                        variant={node.variant}
+                        key={node.key}
+                    >{renderInlineStyles(node.content)}</Text>;
+            }
+        })}
+    </View>;
+};
+export default MarkdownViewer;

+ 10 - 0
src/components/markdownViewer/stylesheet.ts

@@ -0,0 +1,10 @@
+import {
+    StyleSheet
+} from "react-native";
+
+const stylesheet = StyleSheet.create({
+    container: {
+
+    }
+});
+export default stylesheet;

+ 6 - 0
src/components/markdownViewer/type.ts

@@ -0,0 +1,6 @@
+interface IMarkdownViewerProps {
+    content: string;
+};
+export type {
+    IMarkdownViewerProps as default
+};

+ 12 - 2
src/components/notificationIndicator/index.tsx

@@ -2,6 +2,7 @@ import {
     type FC
 } from "react";
 import {
+    TouchableOpacity,
     View
 } from "react-native";
 import type INotificationIndicatorProps from "./type";
@@ -21,12 +22,14 @@ const NotificationIndicator: FC<INotificationIndicatorProps> = ({
     titleVariant = "labelSmallSize",
     spreadBehaviour = "baseline",
     isDisabled = false,
+    isVisible = true,
     type = "neutral",
     customTheme,
     titleStyle,
     isLoading,
     location,
     children,
+    onPress,
     title,
     style,
     ...props
@@ -119,6 +122,10 @@ const NotificationIndicator: FC<INotificationIndicatorProps> = ({
     };
 
     const renderIndicatorContainer = () => {
+        if(!isVisible) {
+            return null;
+        }
+
         if (isLoading) {
             return <Loading
                 style={[
@@ -147,8 +154,11 @@ const NotificationIndicator: FC<INotificationIndicatorProps> = ({
         />;
     };
 
-    return <View
+    return <TouchableOpacity
         {...props}
+        onPress={isDisabled ? undefined : () => {
+            if(onPress) onPress();
+        }}
         style={[
             style,
             stylesheet.container,
@@ -157,6 +167,6 @@ const NotificationIndicator: FC<INotificationIndicatorProps> = ({
     >
         {children}
         {renderIndicatorContainer()}
-    </View>;
+    </TouchableOpacity>;
 };
 export default NotificationIndicator;

+ 2 - 0
src/components/notificationIndicator/type.ts

@@ -48,7 +48,9 @@ interface INotificationIndicatorProps {
     spreadBehaviour?: SwitchSpreadBehaviour;
     type?: NotificationIndicatorType;
     isDisabled?: boolean;
+    onPress?: () => void;
     isLoading?: boolean;
+    isVisible?: boolean;
     children: ReactNode;
     title?: number;
     location?: {

+ 8 - 4
src/components/rowCard/index.tsx

@@ -15,6 +15,10 @@ import {
 import Text from "../text";
 
 const RowCard: FC<IRowCardProps> = ({
+    rightSubTitleVariant = "labelLargeSize",
+    rightTitleVariant = "labelLargeSize",
+    subTitleVariant = "labelLargeSize",
+    titleVariant = "labelLargeSize",
     rightContentContainerStyle,
     rightIcon: CustomRightIcon,
     leftContentContainerStyle,
@@ -111,7 +115,7 @@ const RowCard: FC<IRowCardProps> = ({
 
         return <Text
             color={subTitleColor ? subTitleColor : "low"}
-            variant="labelLargeSize"
+            variant={subTitleVariant}
             style={[
                 leftSubTitleStyle,
                 stylesheet.subTitle,
@@ -129,7 +133,7 @@ const RowCard: FC<IRowCardProps> = ({
             ]}
         >
             <Text
-                variant="labelLargeSize"
+                variant={titleVariant}
                 color={titleColor}
                 style={[
                     leftTitleStyle,
@@ -162,7 +166,7 @@ const RowCard: FC<IRowCardProps> = ({
 
         return <Text
             color={rightSubTitleColor? rightSubTitleColor : "low"}
-            variant="labelLargeSize"
+            variant={rightSubTitleVariant}
             style={[
                 rightSubTitleStyle,
                 stylesheet.rightSubTitle,
@@ -180,7 +184,7 @@ const RowCard: FC<IRowCardProps> = ({
             ]}
         >
             <Text
-                variant="labelLargeSize"
+                variant={rightTitleVariant}
                 color={rightTitleColor}
                 style={[
                     rightTitleStyle,

+ 4 - 0
src/components/rowCard/type.ts

@@ -31,8 +31,12 @@ type IRowCardProps = {
     rightSubTitleColor?: keyof NCoreUIKit.TextContentColors;
     style?: StyleProp<ViewStyle>[] | StyleProp<ViewStyle>;
     rightTitleColor?: keyof NCoreUIKit.TextContentColors;
+    rightSubTitleVariant?: keyof NCoreUIKit.Typography;
     subTitleColor?: keyof NCoreUIKit.TextContentColors;
+    rightTitleVariant?: keyof NCoreUIKit.Typography;
     titleColor?: keyof NCoreUIKit.TextContentColors;
+    subTitleVariant?: keyof NCoreUIKit.Typography;
+    titleVariant?: keyof NCoreUIKit.Typography;
     isTransparentBackground?: boolean;
     rightIcon?: NCoreUIKitIcon;
     rightSubTitle?: string;

+ 69 - 0
src/components/snackBar/index.tsx

@@ -6,6 +6,7 @@ import {
 } from "react";
 import {
     TouchableWithoutFeedback,
+    PanResponder,
     Animated,
     Easing,
     View
@@ -123,6 +124,73 @@ const SnackBar: FC<ISnackBarProps> = ({
         }
     }, [isMeasured]);
 
+    const panResponder = useRef(
+        PanResponder.create({
+            onStartShouldSetPanResponder: () => false,
+            onMoveShouldSetPanResponderCapture: (_, gestureState) => {
+                const {
+                    dy
+                } = gestureState;
+
+                if(Math.abs(dy) < 20) {
+                    return false;
+                }
+
+                return true;
+            },
+            onPanResponderMove: (_, gestureState) => {
+                const {
+                    moveY,
+                    y0,
+                    dy
+                } = gestureState;
+
+                if(moveY > y0) {
+                    return;
+                }
+
+                const op = moveY / y0;
+
+                opacityAnim.setValue(op);
+                transformAnim.setValue(dy);
+            },
+            onPanResponderEnd: (_, gestureState) => {
+                const {
+                    moveY,
+                    y0,
+                    vy
+                } = gestureState;
+
+                if(vy < -1) {
+                    closeAnimation();
+                    return;
+                }
+
+                if(y0 - moveY > 25) {
+                    closeAnimation();
+                    return;
+                }
+
+                Animated.parallel([
+                    Animated.timing(opacityAnim, {
+                        duration: openAnimationDelay,
+                        useNativeDriver: true,
+                        easing: Easing.linear,
+                        toValue: 1
+                    }),
+                    Animated.timing(transformAnim, {
+                        duration: openAnimationDelay,
+                        useNativeDriver: true,
+                        easing: Easing.linear,
+                        toValue: 0
+                    })
+                ]).start();
+            },
+            onPanResponderTerminationRequest: () => false,
+            onShouldBlockNativeResponder: () => true
+        })
+    ).current;
+
     const closeAnimation = (_onClosed?: (props: {
         id: string;
     }) => void) => {
@@ -263,6 +331,7 @@ const SnackBar: FC<ISnackBarProps> = ({
 
     return <Portal name="snack-bar-system">
         <Animated.View
+            {...panResponder.panHandlers}
             onLayout={(event) => {
                 const _contentHeight = event.nativeEvent.layout.height;
 

+ 1 - 0
src/index.tsx

@@ -15,6 +15,7 @@ export {
 
 export {
     NotificationIndicator,
+    MarkdownViewer,
     PageContainer,
     TextAreaInput,
     BottomSheet,